@@ -22,10 +22,11 @@ import {
2222 randomFilepath ,
2323 inParallel ,
2424 toPlatformPath ,
25+ toPosixPath ,
2526} from '@google-github-actions/actions-utils' ;
2627
2728import { 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 } ) ;
0 commit comments