66 "fmt"
77 "io"
88 "os"
9- "path"
109 "path/filepath"
10+ "slices"
1111 "strings"
1212
1313 "cloud.google.com/go/storage"
@@ -19,9 +19,20 @@ import (
1919 "github.com/sourcegraph/src-cli/internal/pgdump"
2020)
2121
22+ // Package-level variables
2223const srcSnapshotDir = "./src-snapshot"
2324
24- var srcSnapshotSummaryPath = path .Join (srcSnapshotDir , "summary.json" )
25+ // summaryFile on its own, as it gets handled a little differently
26+ const summaryFile = "summary.json"
27+ var srcSnapshotSummaryPath = filepath .Join (srcSnapshotDir , summaryFile )
28+
29+ // listOfValidFiles defines the valid snapshot filenames (with extensions) that can be uploaded
30+ var listOfValidFiles = []string {
31+ "codeinsights.sql" ,
32+ "codeintel.sql" ,
33+ "pgsql.sql" ,
34+ summaryFile ,
35+ }
2536
2637// Define types
2738type uploadArgs struct {
@@ -31,13 +42,14 @@ type uploadArgs struct {
3142 filesToUpload []string
3243}
3344
45+ // Google Cloud Storage upload client
3446type gcsClient struct {
3547 ctx context.Context
3648 out * output.Output
3749 storageClient * storage.Client
3850}
3951
40- // uploadFile represents a file registered for upload
52+ // uploadFile represents a file opened for upload
4153type uploadFile struct {
4254 file * os.File
4355 stat os.FileInfo
@@ -46,7 +58,7 @@ type uploadFile struct {
4658
4759
4860func init () {
49- usage := `'src snapshot upload' uploads instance snapshot contents generated by 'src snapshot databases' and 'src snapshot summary' to the designated bucket.
61+ usage := fmt . Sprintf ( `'src snapshot upload' uploads instance snapshot contents generated by 'src snapshot databases' and 'src snapshot summary' to the designated bucket.
5062
5163USAGE
5264 src snapshot upload -bucket=$MIGRATION_BUCKET_NAME -credentials=./migration_private_key.json [-file=$FILE]
@@ -55,15 +67,15 @@ BUCKET
5567 In general, a Google Cloud Storage bucket and relevant credentials will be provided by Sourcegraph when using this functionality to share a snapshot with Sourcegraph.
5668
5769FILE
58- Optional: Specify files to upload as a comma-delimited list. Valid values: summary, primary, codeintel, codeinsights . Default: All valid values .
59- Examples: -file=summary,primary or -file=codeintel
60- `
70+ Optional: Specify files to upload as a comma-delimited list (with file extensions) . Valid values: %s . Default: All files .
71+ Examples: -file=summary.json,pgsql.sql or -file=codeintel.sql
72+ ` , strings . Join ( listOfValidFiles , ", " ))
6173
6274 flagSet := flag .NewFlagSet ("upload" , flag .ExitOnError )
6375 bucketName := flagSet .String ("bucket" , "" , "destination Cloud Storage bucket name" )
6476 credentialsPath := flagSet .String ("credentials" , "" , "JSON credentials file for Google Cloud service account" )
77+ fileFilter := flagSet .String ("file" , strings .Join (listOfValidFiles , "," ), "comma-delimited list of files to upload" )
6578 filterSQL := flagSet .Bool ("filter-sql" , true , "filter incompatible SQL statements from database dumps for import to Google Cloud SQL" )
66- fileFilter := flagSet .String ("file" , "summary,primary,codeintel,codeinsights" , "comma-delimited list of files to upload" )
6779
6880 // Register this command with the parent 'src snapshot' command.
6981 // The parent snapshot.go command runs all registered subcommands via snapshotCommands.run().
7688 })
7789}
7890
79- // Helper function to keep init() succinct
91+ // Handler function to keep init() succinct
8092func snapshotUploadHandler (flagSet * flag.FlagSet , bucketName , credentialsPath * string , filterSQL * bool , fileFilter * string ) func ([]string ) error {
8193 return func (args []string ) error {
8294 if err := flagSet .Parse (args ); err != nil {
8395 return err
8496 }
8597
86- // Validate and parse inputs
98+ // Validate and parse inputs into an uploadArgs-type object
8799 uploadArgs , err := validateUploadInputs (* bucketName , * credentialsPath , * fileFilter , * filterSQL )
88100 if err != nil {
89101 return err
@@ -131,43 +143,41 @@ func validateUploadInputs(bucketName, credentialsPath, fileFilter string, filter
131143// Parse the --file arg values, and return a list of strings of the files to upload
132144func parseFileFilter (fileFilter string ) ([]string , error ) {
133145
134- validFiles := map [string ]bool {
135- "summary" : true ,
136- "primary" : true ,
137- "codeintel" : true ,
138- "codeinsights" : true ,
139- }
140-
146+ // Default: all files
141147 if fileFilter == "" {
142- // Default: all files
143- return []string {"summary" , "primary" , "codeintel" , "codeinsights" }, nil
148+ return listOfValidFiles , nil
144149 }
145150
146- // Parse comma-delimited list
147151 var filesToUpload []string
148- parts := strings .Split (fileFilter , "," )
149- for _ , part := range parts {
150152
151- // Normalize: trim spaces and strip file extensions
152- normalized := strings .TrimSpace (part )
153- normalized = strings .TrimSuffix (normalized , ".json" )
154- normalized = strings .TrimSuffix (normalized , ".sql" )
153+ // Parse comma-delimited list
154+ for _ , part := range strings .Split (fileFilter , "," ) {
155155
156- // Validate
157- if ! validFiles [normalized ] {
158- return nil , errors .Newf ("invalid -file value %q. Valid values: summary[.json], primary[.sql], codeintel[.sql], codeinsights[.sql]" , part )
156+ // Trim whitespace
157+ filename := strings .TrimSpace (part )
158+
159+ // Validate against list of valid files
160+ if ! slices .Contains (listOfValidFiles , filename ) {
161+ return nil , errors .Newf ("invalid -file value %q. Valid values: %s" , part , strings .Join (listOfValidFiles , ", " ))
159162 }
160163
161- filesToUpload = append (filesToUpload , normalized )
164+ filesToUpload = append (filesToUpload , filename )
162165 }
163166
167+ // Sort files alphabetically for consistent ordering
168+ slices .Sort (filesToUpload )
169+
170+ // Remove duplicates (works on sorted slices by removing adjacent duplicates)
171+ filesToUpload = slices .Compact (filesToUpload )
172+
164173 return filesToUpload , nil
165174}
166175
167176func createGcsClient (flagSet * flag.FlagSet , credentialsPath string ) (* gcsClient , error ) {
168177
169- out := output .NewOutput (flagSet .Output (), output.OutputOpts {Verbose : * verbose })
170178 ctx := context .Background ()
179+ out := output .NewOutput (flagSet .Output (), output.OutputOpts {Verbose : * verbose })
180+
171181 // https://pkg.go.dev/cloud.google.com/go/storage#section-readme
172182 client , err := storage .NewClient (ctx , option .WithCredentialsFile (credentialsPath ))
173183
@@ -186,22 +196,25 @@ func createGcsClient(flagSet *flag.FlagSet, credentialsPath string) (*gcsClient,
186196// Returns arrays of uploadFile and progress bars (aligned by index).
187197func openFilesAndCreateProgressBars (args * uploadArgs ) ([]uploadFile , []output.ProgressBar , error ) {
188198 var (
189- openedFiles []uploadFile // Files opened from disk, ready for upload (aligned with progressBars)
199+ openedFiles []uploadFile // Files opened from disk, ready for upload (aligned with progressBars)
190200 progressBars []output.ProgressBar // Progress bars for UI (aligned with openedFiles)
191201 )
192202
193203 // addFile opens a file from disk and registers it for upload.
194204 // It adds the file to the openedFiles array and creates a corresponding progress bar.
195205 // For database dumps (!isSummary), SQL filtering is enabled based on args.filterSQL.
196- addFile := func (filePath string , isSummary bool ) error {
206+ addFile := func (filePath string ) error {
207+
208+ isSummary := strings .HasSuffix (filePath , summaryFile )
197209
198210 // Open the file
199211 openFile , err := os .Open (filePath )
212+
200213 if err != nil {
201214 if isSummary {
202- return errors .Wrap (err , "failed to open snapshot summary - generate one with 'src snapshot summary'" )
215+ return errors .Wrap (err , fmt . Sprintf ( "failed to open snapshot summary %s - Please generate it with 'src snapshot summary'" , filePath ) )
203216 }
204- return errors .Wrap (err , "failed to open database dump - generate one with 'src snapshot databases'" )
217+ return errors .Wrap (err , fmt . Sprintf ( "failed to open database dump %s - Please generate them with 'src snapshot databases'" , filePath ) )
205218 }
206219
207220 // Get file metadata (name, size)
@@ -228,17 +241,12 @@ func openFilesAndCreateProgressBars(args *uploadArgs) ([]uploadFile, []output.Pr
228241 // Open files based on user's selection (via --file arg)
229242 // Iterate through the user's selected files and open each one
230243 for _ , selectedFile := range args .filesToUpload {
231- if selectedFile == "summary" {
232- // Open summary.json
233- if err := addFile (srcSnapshotSummaryPath , true ); err != nil {
234- return nil , nil , err
235- }
236- } else {
237- // Open database dump file (primary.sql, codeintel.sql, or codeinsights.sql)
238- dbFilePath := filepath .Join (srcSnapshotDir , selectedFile + ".sql" )
239- if err := addFile (dbFilePath , false ); err != nil {
240- return nil , nil , err
241- }
244+
245+ // Construct full file path
246+ filePath := filepath .Join (srcSnapshotDir , selectedFile )
247+
248+ if err := addFile (filePath ); err != nil {
249+ return nil , nil , err
242250 }
243251 }
244252
@@ -248,6 +256,7 @@ func openFilesAndCreateProgressBars(args *uploadArgs) ([]uploadFile, []output.Pr
248256// uploadFilesToBucket uploads the prepared files to Google Cloud Storage bucket.
249257// Uploads are performed in parallel with progress tracking.
250258func uploadFilesToBucket (client * gcsClient , args * uploadArgs , openedFiles []uploadFile , progressBars []output.ProgressBar ) error {
259+
251260 // Start uploads with progress tracking
252261 progress := client .out .Progress (progressBars , nil )
253262 progress .WriteLine (output .Emoji (output .EmojiHourglass , "Starting uploads..." ))
@@ -256,8 +265,10 @@ func uploadFilesToBucket(client *gcsClient, args *uploadArgs, openedFiles []uplo
256265
257266 // Upload each file in parallel
258267 for fileIndex , openedFile := range openedFiles {
268+
259269 fileIndex := fileIndex
260270 openedFile := openedFile
271+
261272 uploadPool .Go (func (ctx context.Context ) error {
262273 progressFn := func (bytesWritten int64 ) { progress .SetValue (fileIndex , float64 (bytesWritten )) }
263274
@@ -273,11 +284,11 @@ func uploadFilesToBucket(client *gcsClient, args *uploadArgs, openedFiles []uplo
273284 errs := uploadPool .Wait ()
274285 progress .Complete ()
275286 if errs != nil {
276- client .out .WriteLine (output .Line (output .EmojiFailure , output .StyleFailure , "Some snapshot contents failed to upload." ))
287+ client .out .WriteLine (output .Line (output .EmojiFailure , output .StyleFailure , "Some file(s) failed to upload." ))
277288 return errs
278289 }
279290
280- client .out .WriteLine (output .Emoji (output .EmojiSuccess , "Summary contents uploaded!" ))
291+ client .out .WriteLine (output .Emoji (output .EmojiSuccess , "File(s) uploaded successfully !" ))
281292 return nil
282293}
283294
@@ -291,7 +302,7 @@ func streamFileToBucket(ctx context.Context, file *uploadFile, bucket *storage.B
291302 // To assert against actual file size
292303 var totalBytesWritten int64
293304
294- // Do a partial copy, that filters out incompatible statements
305+ // Start a partial copy, that filters out incompatible statements
295306 if file .filterSQL {
296307 bytesWritten , err := pgdump .FilterInvalidLines (writer , file .file , progressFn )
297308 if err != nil {
0 commit comments