Skip to content

Commit 080b97e

Browse files
committed
feat: direct downloads
1 parent ff6d8c8 commit 080b97e

4 files changed

Lines changed: 64 additions & 7 deletions

File tree

lib/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const envBaseSchema = type({
4848
'CACHE_CLEANUP_OLDER_THAN_DAYS': 'number = 30',
4949
'DISABLE_CLEANUP_JOBS?': 'boolean',
5050
'DEBUG?': 'boolean',
51+
'ENABLE_DIRECT_DOWNLOADS': 'boolean = false',
5152
})
5253

5354
export const envSchema = envBaseSchema.and(envStorageDriverSchema).and(envDbDriverSchema)

lib/storage.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
S3Client,
2020
} from '@aws-sdk/client-s3'
2121
import { Upload as S3Upload } from '@aws-sdk/lib-storage'
22+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
2223
import { Storage as GcsClient } from '@google-cloud/storage'
2324
import { chunk } from 'remeda'
2425
import { match } from 'ts-pattern'
@@ -262,7 +263,7 @@ class Storage {
262263
return { id: uploadId }
263264
}
264265

265-
async getCacheEntry({
266+
private async getCacheEntryByKeys({
266267
keys: [primaryKey, ...restoreKeys],
267268
version,
268269
}: {
@@ -310,6 +311,35 @@ class Storage {
310311
if (prefixedMatch) return prefixedMatch
311312
}
312313
}
314+
315+
async getCacheEntryWithDownloadUrl(args: Parameters<typeof this.getCacheEntryByKeys>[0]) {
316+
const cacheEntry = await this.getCacheEntryByKeys(args)
317+
if (!cacheEntry) return
318+
319+
const defaultUrl = `${env.API_BASE_URL}/download/${cacheEntry.id}`
320+
321+
if (!env.ENABLE_DIRECT_DOWNLOADS || !this.adapter.createDownloadUrl)
322+
return {
323+
downloadUrl: defaultUrl,
324+
cacheEntry,
325+
}
326+
327+
const location = await this.db
328+
.selectFrom('storage_locations')
329+
.where('id', '=', cacheEntry.locationId)
330+
.select(['folderName', 'mergedAt'])
331+
.executeTakeFirst()
332+
if (!location) throw new Error('Storage location not found')
333+
334+
const downloadUrl = location.mergedAt
335+
? await this.adapter.createDownloadUrl(`${location.folderName}/merged`)
336+
: defaultUrl
337+
338+
return {
339+
downloadUrl,
340+
cacheEntry,
341+
}
342+
}
313343
}
314344

315345
export const getStorage = createSingletonPromise(async () => Storage.fromEnv())
@@ -319,6 +349,7 @@ interface StorageAdapter {
319349
uploadStream(objectName: string, stream: ReadableStream): Promise<void>
320350
deleteFolder(folderName: string): Promise<void>
321351
countFilesInFolder(folderName: string): Promise<number>
352+
createDownloadUrl?(objectName: string): Promise<string>
322353
}
323354

324355
class S3Adapter implements StorageAdapter {
@@ -417,6 +448,19 @@ class S3Adapter implements StorageAdapter {
417448

418449
return listResponse.KeyCount ?? 0
419450
}
451+
452+
async createDownloadUrl(objectName: string) {
453+
return getSignedUrl(
454+
this.s3,
455+
new GetObjectCommand({
456+
Bucket: this.bucket,
457+
Key: `${this.keyPrefix}/${objectName}`,
458+
}),
459+
{
460+
expiresIn: 10 * 60 * 1000, // 10min
461+
},
462+
)
463+
}
420464
}
421465

422466
class FileSystemAdapter implements StorageAdapter {
@@ -513,4 +557,14 @@ class GcsAdapter implements StorageAdapter {
513557
})
514558
.then((res) => res[0].length)
515559
}
560+
561+
async createDownloadUrl(objectName: string) {
562+
return this.bucket
563+
.file(`${this.keyPrefix}/${objectName}`)
564+
.getSignedUrl({
565+
action: 'read',
566+
expires: Date.now() + 10 * 60 * 1000, // 10min
567+
})
568+
.then((res) => res[0])
569+
}
516570
}

routes/twirp/github.actions.results.api.v1.CacheService/GetCacheEntryDownloadURL.post.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { z } from 'zod'
2-
import { env } from '~/lib/env'
32
import { getStorage } from '~/lib/storage'
43

54
const bodySchema = z.object({
@@ -19,18 +18,18 @@ export default defineEventHandler(async (event) => {
1918
const { key, restore_keys, version } = parsedBody.data
2019

2120
const storage = await getStorage()
22-
const cacheEntry = await storage.getCacheEntry({
21+
const match = await storage.getCacheEntryWithDownloadUrl({
2322
keys: [key, ...(restore_keys ?? [])],
2423
version,
2524
})
26-
if (!cacheEntry)
25+
if (!match)
2726
return {
2827
ok: false,
2928
}
3029

3130
return {
3231
ok: true,
33-
signed_download_url: `${env.API_BASE_URL}/download/${cacheEntry.id}`,
34-
matched_key: cacheEntry.key,
32+
signed_download_url: match.downloadUrl,
33+
matched_key: match.cacheEntry.key,
3534
}
3635
})

tests/setup.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ const TESTING_ENV_BASE = {
2929
RUNNER_TEMP: path.join(TEST_TEMP_DIR, 'runner-temp'),
3030
ACTIONS_RESULTS_URL: 'http://localhost:3000/',
3131
ACTIONS_CACHE_URL: 'http://localhost:3000/',
32-
} satisfies Omit<typeof envBaseSchema.infer, 'CACHE_CLEANUP_OLDER_THAN_DAYS'> &
32+
} satisfies Omit<
33+
typeof envBaseSchema.infer,
34+
'CACHE_CLEANUP_OLDER_THAN_DAYS' | 'ENABLE_DIRECT_DOWNLOADS'
35+
> &
3336
Record<string, string>
3437

3538
const TESTING_ENV_BY_DB_DRIVER = {

0 commit comments

Comments
 (0)