Skip to content

Commit d68eb9e

Browse files
authored
feat: allow overwriting objects when copying (#530)
1 parent 5a2ffbd commit d68eb9e

File tree

5 files changed

+63
-24
lines changed

5 files changed

+63
-24
lines changed

src/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ type StorageConfigType = {
101101
rateLimiterRedisCommandTimeout: number
102102
uploadSignedUrlExpirationTime: number
103103
tusUrlExpiryMs: number
104+
tusMaxConcurrentUploads: number
104105
tusPath: string
105106
tusPartSize: number
106107
tusUseFileVersionSeparator: boolean
@@ -231,6 +232,10 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
231232
getOptionalConfigFromEnv('TUS_URL_EXPIRY_MS') || (1000 * 60 * 60).toString(),
232233
10
233234
),
235+
tusMaxConcurrentUploads: parseInt(
236+
getOptionalConfigFromEnv('TUS_MAX_CONCURRENT_UPLOADS') || '500',
237+
10
238+
),
234239
tusUseFileVersionSeparator:
235240
getOptionalConfigFromEnv('TUS_USE_FILE_VERSION_SEPARATOR') === 'true',
236241

src/http/routes/tus/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ function createTusStore() {
6161
partSize: tusPartSize * 1024 * 1024, // Each uploaded part will have ${tusPartSize}MB,
6262
expirationPeriodInMilliseconds: tusUrlExpiryMs,
6363
cache: new AlsMemoryKV(),
64-
maxConcurrentPartUploads: 100,
64+
maxConcurrentPartUploads: 500,
6565
s3ClientConfig: {
6666
requestHandler: new NodeHttpHandler({
6767
...agent,

src/storage/object.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ interface CopyObjectParams {
3333
destinationKey: string
3434
owner?: string
3535
copyMetadata?: boolean
36+
upsert?: boolean
3637
conditions?: {
3738
ifMatch?: string
3839
ifNoneMatch?: string
@@ -280,6 +281,7 @@ export class ObjectStorage {
280281
owner,
281282
conditions,
282283
copyMetadata,
284+
upsert,
283285
}: CopyObjectParams) {
284286
mustBeValidKey(destinationKey)
285287

@@ -310,7 +312,7 @@ export class ObjectStorage {
310312
bucketId: destinationBucket,
311313
objectName: destinationKey,
312314
owner,
313-
isUpsert: false,
315+
isUpsert: upsert,
314316
})
315317

316318
try {
@@ -325,14 +327,42 @@ export class ObjectStorage {
325327

326328
const metadata = await this.backend.headObject(storageS3Bucket, s3DestinationKey, newVersion)
327329

328-
const destObject = await this.db.createObject({
329-
...originObject,
330-
bucket_id: destinationBucket,
331-
name: destinationKey,
332-
owner,
333-
metadata,
334-
user_metadata: copyMetadata ? originObject.user_metadata : undefined,
335-
version: newVersion,
330+
const destinationObject = await this.db.asSuperUser().withTransaction(async (db) => {
331+
await db.waitObjectLock(destinationBucket, destinationKey, undefined, {
332+
timeout: 3000,
333+
})
334+
335+
const existingDestObject = await db.findObject(
336+
this.bucketId,
337+
destinationKey,
338+
'id,name,metadata,version,bucket_id',
339+
{
340+
dontErrorOnEmpty: true,
341+
forUpdate: true,
342+
}
343+
)
344+
345+
const destinationObject = await db.upsertObject({
346+
...originObject,
347+
bucket_id: destinationBucket,
348+
name: destinationKey,
349+
owner,
350+
metadata,
351+
user_metadata: copyMetadata ? originObject.user_metadata : undefined,
352+
version: newVersion,
353+
})
354+
355+
if (existingDestObject) {
356+
await ObjectAdminDelete.send({
357+
name: existingDestObject.name,
358+
bucketId: existingDestObject.bucket_id,
359+
tenant: this.db.tenant(),
360+
version: existingDestObject.version,
361+
reqId: this.db.reqId,
362+
})
363+
}
364+
365+
return destinationObject
336366
})
337367

338368
await ObjectCreatedCopyEvent.sendWebhook({
@@ -345,7 +375,7 @@ export class ObjectStorage {
345375
})
346376

347377
return {
348-
destObject,
378+
destObject: destinationObject,
349379
httpStatusCode: copyResult.httpStatusCode,
350380
eTag: copyResult.eTag,
351381
lastModified: copyResult.lastModified,

src/storage/protocols/s3/s3-handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,7 @@ export class S3ProtocolHandler {
996996
destinationBucket: Bucket,
997997
destinationKey: Key,
998998
owner: this.owner,
999+
upsert: true,
9991000
conditions: {
10001001
ifMatch: command.CopySourceIfMatch,
10011002
ifNoneMatch: command.CopySourceIfNoneMatch,

src/storage/protocols/tus/postgres-locker.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,24 @@ export class PgLock implements Lock {
5353
this.db
5454
.withTransaction(async (db) => {
5555
const abortController = new AbortController()
56-
const acquired = await Promise.race([
57-
this.waitTimeout(15000, abortController.signal),
58-
this.acquireLock(db, this.id, abortController.signal),
59-
])
6056

61-
abortController.abort()
62-
63-
if (!acquired) {
64-
throw ERRORS.LockTimeout()
57+
try {
58+
const acquired = await Promise.race([
59+
this.waitTimeout(5000, abortController.signal),
60+
this.acquireLock(db, this.id, abortController.signal),
61+
])
62+
63+
if (!acquired) {
64+
throw ERRORS.LockTimeout()
65+
}
66+
67+
await new Promise<void>((innerResolve) => {
68+
this.tnxResolver = innerResolve
69+
resolve()
70+
})
71+
} finally {
72+
abortController.abort()
6573
}
66-
67-
await new Promise<void>((innerResolve) => {
68-
this.tnxResolver = innerResolve
69-
resolve()
70-
})
7174
})
7275
.catch(reject)
7376
})

0 commit comments

Comments
 (0)