@@ -13,31 +13,40 @@ import (
1313
1414var copyLockTable = NewLockTable ()
1515
16- // Copy copies a file from primary storage to a target storage
16+ // Copy copies a file from primary storage to a target storage, or within primary
17+ // storage if no target is specified. If DestKey is set, the file is written to
18+ // that key; otherwise it uses the source Key.
1719func (o * Operations ) Copy (ctx context.Context , params CopyParams ) CopyResult {
18- storageTargetConfig := o .config .GetStorageTargetByName (params .TargetName )
19- if storageTargetConfig == nil {
20- return CopyResult {Err : fmt .Errorf ("invalid target: %s" , params .TargetName )}
20+ if err := params .Validate (o .config ); err != nil {
21+ return CopyResult {Err : err }
2122 }
2223
23- if storageTargetConfig .Readonly {
24- return CopyResult {Err : fmt .Errorf ("target %s is readonly" , params .TargetName )}
25- }
26-
27- targetBucket := storageTargetConfig .Bucket
28-
29- if params .ExpectedBucket != "" && params .ExpectedBucket != targetBucket {
30- return CopyResult {Err : fmt .Errorf ("expected bucket does not match target bucket: %s != %s" , params .ExpectedBucket , targetBucket )}
31- }
24+ destKey := params .DestKeyOrKey ()
3225
26+ // Create source storage (always primary)
3327 storage , err := NewGcsStorage (o .config )
3428 if err != nil {
3529 return CopyResult {Err : fmt .Errorf ("failed to create source storage: %v" , err )}
3630 }
3731
38- targetStorage , err := storageTargetConfig .NewStorageClient ()
39- if err != nil {
40- return CopyResult {Err : fmt .Errorf ("failed to create target storage: %v" , err )}
32+ var targetStorage Storage
33+ var targetBucket string
34+ var targetLabel string
35+
36+ if params .TargetName == "" {
37+ // Same-storage copy: reuse source storage for both read and write
38+ targetStorage = storage
39+ targetBucket = o .config .Bucket
40+ targetLabel = "primary"
41+ } else {
42+ // Cross-storage copy: read from primary, write to target
43+ storageTargetConfig := o .config .GetStorageTargetByName (params .TargetName )
44+ targetBucket = storageTargetConfig .Bucket
45+ targetStorage , err = storageTargetConfig .NewStorageClient ()
46+ if err != nil {
47+ return CopyResult {Err : fmt .Errorf ("failed to create target storage: %v" , err )}
48+ }
49+ targetLabel = params .TargetName
4150 }
4251
4352 startTime := time .Now ()
@@ -73,23 +82,23 @@ func (o *Operations) Copy(ctx context.Context, params CopyParams) CopyResult {
7382 }
7483
7584 if injected {
76- log .Print ("Starting transfer (injected): [" , params . TargetName , "] " , targetBucket , "/" , params . Key , " " , opts )
85+ log .Print ("Starting transfer (injected): [" , targetLabel , "] " , targetBucket , "/" , destKey , " " , opts )
7786 } else {
78- log .Print ("Starting transfer: [" , params . TargetName , "] " , targetBucket , "/" , params . Key , " " , opts )
87+ log .Print ("Starting transfer: [" , targetLabel , "] " , targetBucket , "/" , destKey , " " , opts )
7988 }
80- result , err := targetStorage .PutFile (ctx , targetBucket , params . Key , mReader , opts )
89+ result , err := targetStorage .PutFile (ctx , targetBucket , destKey , mReader , opts )
8190 if err != nil {
8291 return CopyResult {Err : fmt .Errorf ("failed to copy file: %v" , err )}
8392 }
8493
8594 globalMetrics .TotalCopiedFiles .Add (1 )
86- log .Print ("Transfer complete: [" , params . TargetName , "] " , targetBucket , "/" , params . Key ,
95+ log .Print ("Transfer complete: [" , targetLabel , "] " , targetBucket , "/" , destKey ,
8796 ", bytes read: " , formatBytes (float64 (mReader .BytesRead )),
8897 ", duration: " , mReader .Duration .Seconds (),
8998 ", speed: " , formatBytes (mReader .TransferSpeed ()), "/s" )
9099
91100 return CopyResult {
92- Key : params . Key ,
101+ Key : destKey ,
93102 Duration : fmt .Sprintf ("%.4fs" , time .Since (startTime ).Seconds ()),
94103 Size : mReader .BytesRead ,
95104 Md5 : result .MD5 ,
@@ -153,8 +162,9 @@ func notifyError(callbackURL string, err error) error {
153162 return notifyCallback (callbackURL , message )
154163}
155164
156- // The copy handler will asynchronously copy a file from primary storage to the
157- // storage specified by target
165+ // The copy handler asynchronously copies a file from primary storage to either:
166+ // - A target storage (when target is specified)
167+ // - A different key within primary storage (when only dest_key is specified)
158168func copyHandler (w http.ResponseWriter , r * http.Request ) error {
159169 if err := r .ParseForm (); err != nil {
160170 return fmt .Errorf ("failed to parse form: %w" , err )
@@ -166,21 +176,30 @@ func copyHandler(w http.ResponseWriter, r *http.Request) error {
166176 }
167177
168178 callbackURL := params .Get ("callback" )
179+ targetName := params .Get ("target" )
180+ destKey := params .Get ("dest_key" )
169181
170- targetName , err := getParam (params , "target" )
171- if err != nil {
182+ expectedBucket , _ := getParam (params , "bucket" )
183+ htmlFooter := params .Get ("html_footer" )
184+
185+ copyParams := CopyParams {
186+ Key : key ,
187+ DestKey : destKey ,
188+ TargetName : targetName ,
189+ ExpectedBucket : expectedBucket ,
190+ HtmlFooter : htmlFooter ,
191+ }
192+ if err := copyParams .Validate (globalConfig ); err != nil {
172193 return err
173194 }
174195
175- storageTargetConfig := globalConfig .GetStorageTargetByName (targetName )
176- if storageTargetConfig == nil {
177- return fmt .Errorf ("invalid target: %s" , targetName )
196+ // Use dest_key for lock if provided, otherwise use key
197+ lockDestKey := copyParams .DestKeyOrKey ()
198+ lockTargetName := targetName
199+ if lockTargetName == "" {
200+ lockTargetName = "primary"
178201 }
179-
180- expectedBucket , _ := getParam (params , "bucket" )
181- htmlFooter := params .Get ("html_footer" )
182-
183- lockKey := fmt .Sprintf ("%s:%s" , targetName , key )
202+ lockKey := fmt .Sprintf ("%s:%s" , lockTargetName , lockDestKey )
184203
185204 hasLock := copyLockTable .tryLockKey (lockKey )
186205
@@ -190,13 +209,6 @@ func copyHandler(w http.ResponseWriter, r *http.Request) error {
190209 }
191210
192211 ops := NewOperations (globalConfig )
193- copyParams := CopyParams {
194- Key : key ,
195- TargetName : targetName ,
196- ExpectedBucket : expectedBucket ,
197- HtmlFooter : htmlFooter ,
198- }
199-
200212 // sync codepath
201213 if callbackURL == "" {
202214 defer copyLockTable .releaseKey (lockKey )
0 commit comments