Skip to content

Commit 285ce3b

Browse files
authored
bug: fix parent detection for single files (#269)
1 parent 079b885 commit 285ce3b

8 files changed

Lines changed: 445 additions & 198 deletions

File tree

dist/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/client.ts

Lines changed: 93 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ import {
2222
randomFilepath,
2323
inParallel,
2424
toPlatformPath,
25+
toPosixPath,
2526
} from '@google-github-actions/actions-utils';
2627

2728
import { Metadata } from './headers';
28-
import { deepClone, parseBucketNameAndPrefix } from './util';
29+
import { deepClone } from './util';
2930

3031
// Do not listen to the linter - this can NOT be rewritten as an ES6 import statement.
3132
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -39,47 +40,49 @@ const userAgent = `google-github-actions:upload-cloud-storage/${appVersion}`;
3940
*
4041
* @param credentials GCP JSON credentials (default uses ADC).
4142
*/
42-
type ClientOptions = {
43+
export type ClientOptions = {
4344
credentials?: string;
4445
projectID?: string;
4546
};
4647

4748
/**
48-
* ClientUploadOptions is the list of available options during file upload.
49+
* ClientFileUpload represents a file to upload. It keeps track of the local
50+
* source path and remote destination.
4951
*/
50-
export interface ClientUploadOptions {
52+
export type ClientFileUpload = {
5153
/**
52-
* destination is the name of the bucket and optionally the path within the
53-
* bucket in which to upload. This value is split on the first instance of a
54-
* slash character. Everything preceeding of the first slash is the bucket
55-
* name, everything following the first slash is the path.
54+
* source is the absolute, local path on disk to the file.
55+
*/
56+
source: string;
57+
58+
/**
59+
* destination is the remote location for the file, relative to the bucket
60+
* root.
5661
*/
5762
destination: string;
63+
};
5864

65+
/**
66+
* ClientUploadOptions is the list of available options during file upload.
67+
*/
68+
export interface ClientUploadOptions {
5969
/**
60-
* root is the parent directory from which all files originated on local disk.
61-
* This must be the platform-specific path separators.
70+
* bucket is the name of the bucket in which to upload.
6271
*/
63-
root: string;
72+
bucket: string;
6473

6574
/**
6675
* files is the list of absolute file paths on local disk to upload. This list
6776
* must use posix path separators for files.
6877
*/
69-
files: string[];
78+
files: ClientFileUpload[];
7079

7180
/**
7281
* concurrency is the maximum number of parallel upload operations that will
7382
* take place.
7483
*/
7584
concurrency?: number;
7685

77-
/**
78-
* includeParent indicates whether the local directory parent name (dirname of
79-
* root) should be included in the destination path in the bucket.
80-
*/
81-
includeParent?: boolean;
82-
8386
/**
8487
* metadata is object metadata to set. These are usually populated from
8588
* headers.
@@ -110,10 +113,43 @@ export interface ClientUploadOptions {
110113
/**
111114
* FOnUploadObject is the function interface for the upload callback signature.
112115
*/
113-
interface FOnUploadObject {
116+
export interface FOnUploadObject {
114117
(source: string, destination: string, opts: Record<string, unknown>): void;
115118
}
116119

120+
/**
121+
* ClientComputeDestinationOptions is the list of options to compute file
122+
* destinations in a target bucket.
123+
*/
124+
export interface ClientComputeDestinationOptions {
125+
/**
126+
* givenRoot is the root given by the input to the function.
127+
*/
128+
givenRoot: string;
129+
130+
/**
131+
* absoluteRoot is the absolute root path, used for resolving the files.
132+
*/
133+
absoluteRoot: string;
134+
135+
/**
136+
* files is a list of filenames, for a glob expansion. All files are relative
137+
* to absoluteRoot.
138+
*/
139+
files: string[];
140+
141+
/**
142+
* prefix is an optional prefix to predicate on all paths.
143+
*/
144+
prefix?: string;
145+
146+
/**
147+
* includeParent indicates whether the local directory parent name (dirname of
148+
* givenRoot) should be included in the destination path in the bucket.
149+
*/
150+
includeParent?: boolean;
151+
}
152+
117153
/**
118154
* Handles credential lookup, registration and wraps interactions with the GCS
119155
* Helper.
@@ -136,6 +172,37 @@ export class Client {
136172
this.storage = new Storage(options);
137173
}
138174

175+
/**
176+
* computeDestinations builds a collection of files to their intended upload
177+
* paths in a Cloud Storage bucket, based on the given options.
178+
*
179+
* @param opts List of inputs and files to compute.
180+
* @return List of files to upload with the source as a local file path and
181+
* the remote destination path.
182+
*/
183+
static computeDestinations(opts: ClientComputeDestinationOptions): ClientFileUpload[] {
184+
const list: ClientFileUpload[] = [];
185+
for (let i = 0; i < opts.files.length; i++) {
186+
const name = opts.files[i];
187+
188+
// Calculate destination by joining the prefix (if one exists), the parent
189+
// directory name (if includeParent is true), and the file name. path.join
190+
// ignores empty strings. We only want to do this if
191+
const base = opts.includeParent ? path.posix.basename(toPosixPath(opts.givenRoot)) : '';
192+
const destination = path.posix.join(opts.prefix || '', base, name);
193+
194+
// Compute the absolute path of the file.
195+
const source = path.resolve(opts.absoluteRoot, toPlatformPath(name));
196+
197+
list.push({
198+
source: source,
199+
destination: destination,
200+
});
201+
}
202+
203+
return list;
204+
}
205+
139206
/**
140207
* upload puts the given collection of files into the bucket. It will
141208
* overwrite any existing objects with the same name and create any new
@@ -146,19 +213,12 @@ export class Client {
146213
* @return The list of files uploaded.
147214
*/
148215
async upload(opts: ClientUploadOptions): Promise<string[]> {
149-
const [bucket, prefix] = parseBucketNameAndPrefix(opts.destination);
150-
216+
const bucket = opts.bucket;
151217
const storageBucket = this.storage.bucket(bucket);
152218

153-
const uploadOne = async (file: string): Promise<string> => {
154-
// Calculate destination by joining the prefix (if one exists), the parent
155-
// directory name (if includeParent is true), and the file name. path.join
156-
// ignores empty strings.
157-
const base = opts.includeParent ? path.basename(opts.root) : '';
158-
const destination = path.posix.join(prefix, base, file);
159-
160-
// Build options.
161-
const abs = path.resolve(opts.root, toPlatformPath(file));
219+
const uploadOne = async (file: ClientFileUpload): Promise<string> => {
220+
const source = file.source;
221+
const destination = file.destination;
162222

163223
// Apparently the Cloud Storage SDK modifies this object, so we need to
164224
// make our own deep copy before passing it to upload. See #258 for more
@@ -174,16 +234,16 @@ export class Client {
174234

175235
// Execute callback if defined
176236
if (opts.onUploadObject) {
177-
opts.onUploadObject(abs, path.posix.join(bucket, destination), uploadOpts);
237+
opts.onUploadObject(source, path.posix.join(bucket, destination), uploadOpts);
178238
}
179239

180240
// Do the upload
181-
const response = await storageBucket.upload(abs, uploadOpts);
241+
const response = await storageBucket.upload(source, uploadOpts);
182242
const name = response[0].name;
183243
return name;
184244
};
185245

186-
const args: [file: string][] = opts.files.map((file) => [file]);
246+
const args: [file: ClientFileUpload][] = opts.files.map((file) => [file]);
187247
const results = await inParallel(uploadOne, args, {
188248
concurrency: opts.concurrency,
189249
});

src/main.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ import * as path from 'path';
2828

2929
import { Client } from './client';
3030
import { parseHeadersInput } from './headers';
31-
import { absoluteRootAndComputedGlob, deepClone, expandGlob } from './util';
31+
import {
32+
absoluteRootAndComputedGlob,
33+
deepClone,
34+
parseBucketNameAndPrefix,
35+
expandGlob,
36+
} from './util';
3237

3338
const NO_FILES_WARNING =
3439
`There are no files to upload! Make sure the workflow uses the "checkout"` +
@@ -53,7 +58,7 @@ export async function run(): Promise<void> {
5358
const destination = core.getInput('destination', { required: true });
5459
const gzip = core.getBooleanInput('gzip');
5560
const resumable = core.getBooleanInput('resumable');
56-
const parent = core.getBooleanInput('parent');
61+
const includeParent = core.getBooleanInput('parent');
5762
const glob = core.getInput('glob');
5863
const concurrency = Number(core.getInput('concurrency'));
5964
const predefinedAclInput = core.getInput('predefinedAcl');
@@ -75,8 +80,8 @@ export async function run(): Promise<void> {
7580
}
7681

7782
// Compute the absolute root and compute the glob.
78-
const [absoluteRoot, computedGlob] = await absoluteRootAndComputedGlob(root, glob);
79-
core.debug(`Computed absoluteRoot from "${root}" to "${absoluteRoot}"`);
83+
const [absoluteRoot, computedGlob, rootIsDir] = await absoluteRootAndComputedGlob(root, glob);
84+
core.debug(`Computed absoluteRoot from "${root}" to "${absoluteRoot}" (isDir: ${rootIsDir})`);
8085
core.debug(`Computed computedGlob from "${glob}" to "${computedGlob}"`);
8186

8287
// Build complete file list.
@@ -128,18 +133,35 @@ export async function run(): Promise<void> {
128133
core.warning(NO_FILES_WARNING);
129134
}
130135

136+
// Compute the bucket and prefix.
137+
const [bucket, prefix] = parseBucketNameAndPrefix(destination);
138+
core.debug(`Computed bucket as "${bucket}"`);
139+
core.debug(`Computed prefix as "${prefix}"`);
140+
141+
// Compute the list of file destinations in the bucket based on given
142+
// parameters.
143+
const destinations = Client.computeDestinations({
144+
givenRoot: root,
145+
absoluteRoot: absoluteRoot,
146+
files: files,
147+
prefix: prefix,
148+
149+
// Only include the parent if the given root was a directory. Without
150+
// this, uploading a single object will cause the object to be nested in
151+
// its own name: google-github-actions/upload-cloud-storage#259.
152+
includeParent: includeParent && rootIsDir,
153+
});
154+
131155
// Create the client and upload files.
132156
core.startGroup('Upload files');
133157
const client = new Client({
134158
credentials: credentials,
135159
projectID: projectID,
136160
});
137161
const uploadResponses = await client.upload({
138-
destination: destination,
139-
root: absoluteRoot,
140-
files: files,
162+
bucket: bucket,
163+
files: destinations,
141164
concurrency: concurrency,
142-
includeParent: parent,
143165
metadata: metadata,
144166
gzip: gzip,
145167
resumable: resumable,

src/util.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ import { toPlatformPath, toPosixPath } from '@google-github-actions/actions-util
3333
*
3434
* @param root The root path to expand.
3535
* @param glob The glob to compute.
36-
* @return [string, string] The absolute and expanded root and computed glob.
36+
* @return [string, string, boolean] The absolute and expanded root, the
37+
* computed glob, and a boolean indicating whether the given root was a
38+
* directory.
3739
*/
3840
export async function absoluteRootAndComputedGlob(
3941
root: string,
4042
glob: string,
41-
): Promise<[absoluteRoot: string, computedGlob: string]> {
43+
): Promise<[absoluteRoot: string, computedGlob: string, isFile: boolean]> {
4244
// Resolve the root input path, relative to the active workspace. If the
4345
// value was already an absolute path, this has no effect.
4446
const githubWorkspace = process.env.GITHUB_WORKSPACE;
@@ -57,10 +59,10 @@ export async function absoluteRootAndComputedGlob(
5759

5860
const computedGlob = path.basename(resolvedRoot);
5961
const absoluteRoot = path.dirname(resolvedRoot);
60-
return [absoluteRoot, toPosixPath(computedGlob)];
62+
return [absoluteRoot, toPosixPath(computedGlob), false];
6163
}
6264

63-
return [resolvedRoot, toPosixPath(glob)];
65+
return [resolvedRoot, toPosixPath(glob), true];
6466
}
6567

6668
/**

0 commit comments

Comments
 (0)