Skip to content

Commit d3eaa89

Browse files
authored
feat: support cross-bucket copy and move operations (#18)
1 parent 6bf9b31 commit d3eaa89

11 files changed

Lines changed: 287 additions & 51 deletions

File tree

.github/workflows/checks.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ jobs:
4545
env:
4646
GCS_KEY: ${{ secrets.GCS_KEY }}
4747
GCS_BUCKET: drive-gcs
48+
GCS_OTHER_BUCKET: flydrive-other-bucket
4849
GCS_FINE_GRAINED_ACL_BUCKET: drive-gcs-no-uniform-acl
4950
tests-gcs:
5051
runs-on: ${{ matrix.os }}
@@ -69,6 +70,7 @@ jobs:
6970
env:
7071
GCS_KEY: ${{ secrets.GCS_KEY }}
7172
GCS_BUCKET: drive-gcs
73+
GCS_OTHER_BUCKET: flydrive-other-bucket
7274
GCS_FINE_GRAINED_ACL_BUCKET: drive-gcs-no-uniform-acl
7375
tests-s3:
7476
runs-on: ${{ matrix.os }}
@@ -93,6 +95,7 @@ jobs:
9395
env:
9496
S3_SERVICE: do
9597
S3_BUCKET: testing-flydrive
98+
S3_OTHER_BUCKET: drive-other-bucket
9699
S3_ACCESS_KEY: ${{ secrets.DO_ACCESS_KEY }}
97100
S3_ACCESS_SECRET: ${{ secrets.DO_ACCESS_SECRET }}
98101
S3_ENDPOINT: https://sgp1.digitaloceanspaces.com
@@ -121,6 +124,7 @@ jobs:
121124
env:
122125
S3_SERVICE: r2
123126
S3_BUCKET: testing-flydrive
127+
S3_OTHER_BUCKET: flydrive-other-bucket
124128
S3_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
125129
S3_ACCESS_SECRET: ${{ secrets.R2_ACCESS_SECRET }}
126130
S3_ENDPOINT: https://b7d56a259a224b185a70dd6e6f77d9c3.r2.cloudflarestorage.com

drivers/gcs/driver.ts

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { DriveFile } from '../../src/driver_file.js'
2323
import { DriveDirectory } from '../../src/drive_directory.js'
2424
import type {
2525
WriteOptions,
26+
CopyMoveOptions,
2627
ObjectMetaData,
2728
DriverContract,
2829
SignedURLOptions,
@@ -381,58 +382,69 @@ export class GCSDriver implements DriverContract {
381382
/**
382383
* Copies the source file to the destination. Both paths must
383384
* be within the root location.
385+
*
386+
* Use the "destinationBucket" option to copy the file to a different bucket.
384387
*/
385-
async copy(source: string, destination: string, options?: WriteOptions): Promise<void> {
388+
async copy(source: string, destination: string, options?: CopyMoveOptions): Promise<void> {
389+
const { destinationBucket, ...writeOptions } = options || {}
390+
const targetBucket = destinationBucket || this.options.bucket
391+
386392
debug(
387393
'copying file from %s:%s to %s:%s',
388394
this.options.bucket,
389395
source,
390-
this.options.bucket,
396+
targetBucket,
391397
destination
392398
)
393-
const bucket = this.#storage.bucket(this.options.bucket)
394-
options = options || {}
399+
400+
const sourceBucket = this.#storage.bucket(this.options.bucket)
395401

396402
/**
397403
* Copy visibility from the source file to the
398404
* desintation when no inline visibility is
399405
* defined and not using usingUniformAcl
400406
*/
401-
if (!options.visibility && !this.#usingUniformAcl) {
402-
const [isFilePublic] = await bucket.file(source).isPublic()
403-
options.visibility = isFilePublic ? 'public' : 'private'
407+
if (!writeOptions.visibility && !this.#usingUniformAcl) {
408+
const [isFilePublic] = await sourceBucket.file(source).isPublic()
409+
writeOptions.visibility = isFilePublic ? 'public' : 'private'
404410
}
405411

406-
await bucket.file(source).copy(destination, this.#getSaveOptions(options))
412+
const target = destinationBucket
413+
? this.#storage.bucket(destinationBucket).file(destination)
414+
: destination
415+
416+
await sourceBucket.file(source).copy(target, this.#getSaveOptions(writeOptions))
407417
}
408418

409419
/**
410420
* Moves the source file to the destination. Both paths must
411421
* be within the root location.
422+
*
423+
* Use the "destinationBucket" option to move the file to a different bucket.
412424
*/
413-
async move(source: string, destination: string, options?: WriteOptions): Promise<void> {
414-
debug(
415-
'moving file from %s:%s to %s:%s',
416-
this.options.bucket,
417-
source,
418-
this.options.bucket,
419-
destination
420-
)
425+
async move(source: string, destination: string, options?: CopyMoveOptions): Promise<void> {
426+
const { destinationBucket, ...writeOptions } = options || {}
427+
const targetBucket = destinationBucket || this.options.bucket
421428

422-
const bucket = this.#storage.bucket(this.options.bucket)
423-
options = options || {}
429+
debug('moving file from %s:%s to %s:%s', this.options.bucket, source, targetBucket, destination)
430+
431+
const sourceBucket = this.#storage.bucket(this.options.bucket)
424432

425433
/**
426434
* Copy visibility from the source file to the
427435
* desintation when no inline visibility is
428436
* defined and not using usingUniformAcl
429437
*/
430-
if (!options.visibility && !this.#usingUniformAcl) {
431-
const [isFilePublic] = await bucket.file(source).isPublic()
432-
options.visibility = isFilePublic ? 'public' : 'private'
438+
if (!writeOptions.visibility && !this.#usingUniformAcl) {
439+
const [isFilePublic] = await sourceBucket.file(source).isPublic()
440+
writeOptions.visibility = isFilePublic ? 'public' : 'private'
433441
}
434442

435-
await bucket.file(source).move(destination, this.#getSaveOptions(options))
443+
const target = destinationBucket
444+
? this.#storage.bucket(destinationBucket).file(destination)
445+
: destination
446+
447+
await sourceBucket.file(source).move(target, this.#getSaveOptions(writeOptions))
436448
}
437449

438450
/**

drivers/s3/driver.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { DriveFile } from '../../src/driver_file.js'
3939
import { DriveDirectory } from '../../src/drive_directory.js'
4040
import type {
4141
WriteOptions,
42+
CopyMoveOptions,
4243
DriverContract,
4344
ObjectMetaData,
4445
ObjectVisibility,
@@ -583,49 +584,50 @@ export class S3Driver implements DriverContract {
583584
/**
584585
* Copies the source file to the destination. Both paths must
585586
* be within the root location.
587+
*
588+
* Use the "destinationBucket" option to copy the file to a different bucket.
586589
*/
587-
async copy(source: string, destination: string, options?: WriteOptions): Promise<void> {
590+
async copy(source: string, destination: string, options?: CopyMoveOptions): Promise<void> {
591+
const { destinationBucket, ...writeOptions } = options || {}
592+
const targetBucket = destinationBucket || this.options.bucket
593+
588594
debug(
589595
'copying file from %s:%s to %s:%s',
590596
this.options.bucket,
591597
source,
592-
this.options.bucket,
598+
targetBucket,
593599
destination
594600
)
595601

596-
options = options || {}
597-
598602
/**
599603
* Copy visibility from the source file to the
600604
* destination when no inline visibility is
601605
* defined
602606
*/
603-
if (!options.visibility && this.#supportsACL) {
604-
options.visibility = await this.getVisibility(source)
607+
if (!writeOptions.visibility && this.#supportsACL) {
608+
writeOptions.visibility = await this.getVisibility(source)
605609
}
606610

607611
await this.#client.send(
608612
this.createCopyObjectCommand(this.#client, {
609-
...this.#getSaveOptions(destination, options),
613+
...this.#getSaveOptions(destination, writeOptions),
610614
Key: destination,
611615
CopySource: `/${this.options.bucket}/${source}`,
612-
Bucket: this.options.bucket,
616+
Bucket: targetBucket,
613617
})
614618
)
615619
}
616620

617621
/**
618622
* Moves the source file to the destination. Both paths must
619623
* be within the root location.
624+
*
625+
* Use the "destinationBucket" option to move the file to a different bucket.
620626
*/
621-
async move(source: string, destination: string, options?: WriteOptions): Promise<void> {
622-
debug(
623-
'moving file from %s:%s to %s:%s',
624-
this.options.bucket,
625-
source,
626-
this.options.bucket,
627-
destination
628-
)
627+
async move(source: string, destination: string, options?: CopyMoveOptions): Promise<void> {
628+
const targetBucket = options?.destinationBucket || this.options.bucket
629+
630+
debug('moving file from %s:%s to %s:%s', this.options.bucket, source, targetBucket, destination)
629631

630632
await this.copy(source, destination, options)
631633
await this.delete(source)

src/disk.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
DriverContract,
2323
ObjectVisibility,
2424
SignedURLOptions,
25+
CopyMoveOptions,
2526
} from './types.js'
2627

2728
/**
@@ -169,13 +170,13 @@ export class Disk {
169170
}
170171

171172
/**
172-
* Copies file from the "source" to the "destination" within the
173-
* same bucket or the root location of local filesystem.
173+
* Copies file from the "source" to the "destination". Use the "destinationBucket"
174+
* option to copy the file to a different bucket.
174175
*
175176
* Use "copyFromFs" method to copy files from local filesystem to
176177
* a cloud provider
177178
*/
178-
async copy(source: string, destination: string, options?: WriteOptions): Promise<void> {
179+
async copy(source: string, destination: string, options?: CopyMoveOptions): Promise<void> {
179180
source = this.#normalizer.normalize(source)
180181
destination = this.#normalizer.normalize(destination)
181182
try {
@@ -193,13 +194,13 @@ export class Disk {
193194
}
194195

195196
/**
196-
* Moves file from the "source" to the "destination" within the
197-
* same bucket or the root location of local filesystem.
197+
* Moves file from the "source" to the "destination". Use the "destinationBucket"
198+
* option to move the file to a different bucket.
198199
*
199200
* Use "moveFromFs" method to move files from local filesystem to
200201
* a cloud provider
201202
*/
202-
async move(source: string, destination: string, options?: WriteOptions): Promise<void> {
203+
async move(source: string, destination: string, options?: CopyMoveOptions): Promise<void> {
203204
source = this.#normalizer.normalize(source)
204205
destination = this.#normalizer.normalize(destination)
205206
try {

src/types.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ export type WriteOptions = {
4242
[key: string]: any
4343
}
4444

45+
/**
46+
* Options accepted by the copy and move operations.
47+
*/
48+
export type CopyMoveOptions = WriteOptions & {
49+
destinationBucket?: string
50+
}
51+
4552
/**
4653
* Options accepted during the creation of a signed URL.
4754
*/
@@ -155,15 +162,19 @@ export interface DriverContract {
155162
* Copy the file from within the disk root location. Both
156163
* the "source" and "destination" will be the key names
157164
* and not absolute paths.
165+
*
166+
* Use the "destinationBucket" option to copy the file to a different bucket.
158167
*/
159-
copy(source: string, destination: string, options?: WriteOptions): Promise<void>
168+
copy(source: string, destination: string, options?: CopyMoveOptions): Promise<void>
160169

161170
/**
162171
* Move the file from within the disk root location. Both
163172
* the "source" and "destination" will be the key names
164173
* and not absolute paths.
174+
*
175+
* Use the "destinationBucket" option to move the file to a different bucket.
165176
*/
166-
move(source: string, destination: string, options?: WriteOptions): Promise<void>
177+
move(source: string, destination: string, options?: CopyMoveOptions): Promise<void>
167178

168179
/**
169180
* Delete the file for the given key. Should not throw

tests/drivers/gcs/copy.spec.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { test } from '@japa/runner'
1111
import string from '@poppinss/utils/string'
1212
import { Storage } from '@google-cloud/storage'
1313
import { GCSDriver } from '../../../drivers/gcs/driver.js'
14-
import { GCS_BUCKET, GCS_FINE_GRAINED_ACL_BUCKET, GCS_KEY } from './env.js'
14+
import { GCS_BUCKET, GCS_FINE_GRAINED_ACL_BUCKET, GCS_KEY, GCS_OTHER_BUCKET } from './env.js'
1515

1616
/**
1717
* Direct access to Google cloud storage bucket
@@ -20,6 +20,9 @@ import { GCS_BUCKET, GCS_FINE_GRAINED_ACL_BUCKET, GCS_KEY } from './env.js'
2020
const bucket = new Storage({
2121
credentials: GCS_KEY,
2222
}).bucket(GCS_BUCKET)
23+
const otherBucket = new Storage({
24+
credentials: GCS_KEY,
25+
}).bucket(GCS_OTHER_BUCKET)
2326
const noUniformedAclBucket = new Storage({
2427
credentials: GCS_KEY,
2528
}).bucket(GCS_FINE_GRAINED_ACL_BUCKET)
@@ -28,6 +31,7 @@ test.group('GCS Driver | copy', (group) => {
2831
group.each.setup(() => {
2932
return async () => {
3033
await bucket.deleteFiles()
34+
await otherBucket.deleteFiles()
3135
await noUniformedAclBucket.deleteFiles()
3236
}
3337
})
@@ -109,4 +113,53 @@ test.group('GCS Driver | copy', (group) => {
109113
const existsResponse = await noUniformedAclBucket.file(source).exists()
110114
assert.isTrue(existsResponse[0])
111115
})
116+
117+
test('copy file with explicit bucket option', async ({ assert }) => {
118+
const source = `${string.random(6)}.txt`
119+
const destination = `${string.random(6)}.txt`
120+
const contents = 'Hello world'
121+
122+
const fdgcs = new GCSDriver({
123+
visibility: 'public',
124+
bucket: GCS_BUCKET,
125+
credentials: GCS_KEY,
126+
usingUniformAcl: true,
127+
})
128+
await fdgcs.put(source, contents)
129+
await fdgcs.copy(source, destination, { destinationBucket: GCS_BUCKET })
130+
131+
assert.equal(await fdgcs.get(destination), contents)
132+
const [exists] = await bucket.file(source).exists()
133+
assert.isTrue(exists)
134+
})
135+
136+
test('copy file to another bucket with explicit bucket option', async ({ assert }) => {
137+
const source = `${string.random(6)}.txt`
138+
const destination = `${string.random(6)}.txt`
139+
const contents = 'Hello world'
140+
141+
const sourceDriver = new GCSDriver({
142+
visibility: 'public',
143+
bucket: GCS_BUCKET,
144+
credentials: GCS_KEY,
145+
usingUniformAcl: true,
146+
})
147+
const destinationDriver = new GCSDriver({
148+
visibility: 'public',
149+
bucket: GCS_OTHER_BUCKET,
150+
credentials: GCS_KEY,
151+
usingUniformAcl: true,
152+
})
153+
154+
await sourceDriver.put(source, contents)
155+
await sourceDriver.copy(source, destination, { destinationBucket: GCS_OTHER_BUCKET })
156+
157+
assert.equal(await destinationDriver.get(destination), contents)
158+
159+
const [sourceExists] = await bucket.file(source).exists()
160+
const [destinationExists] = await otherBucket.file(destination).exists()
161+
162+
assert.isTrue(sourceExists)
163+
assert.isTrue(destinationExists)
164+
})
112165
})

tests/drivers/gcs/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import { Env } from '@adonisjs/env'
1212
const env = await Env.create(new URL('../../../', import.meta.url), {
1313
GCS_KEY: Env.schema.string(),
1414
GCS_BUCKET: Env.schema.string(),
15+
GCS_OTHER_BUCKET: Env.schema.string(),
1516
GCS_FINE_GRAINED_ACL_BUCKET: Env.schema.string(),
1617
})
1718

1819
export const GCS_BUCKET = env.get('GCS_BUCKET')
20+
export const GCS_OTHER_BUCKET = env.get('GCS_OTHER_BUCKET')
1921
export const GCS_KEY = JSON.parse(env.get('GCS_KEY'))
2022
export const GCS_FINE_GRAINED_ACL_BUCKET = env.get('GCS_FINE_GRAINED_ACL_BUCKET')

0 commit comments

Comments
 (0)