1- import { createReadStream } from "node:fs" ;
2- import { GetObjectCommand , S3 } from "@aws-sdk/client-s3" ;
1+ import { exists , mkdir , writeFile } from "node:fs/promises" ;
2+ import { dirname , join } from "node:path" ;
3+ import {
4+ DeleteObjectCommand ,
5+ GetObjectCommand ,
6+ paginateListObjectsV2 ,
7+ S3 ,
8+ } from "@aws-sdk/client-s3" ;
39import { Upload } from "@aws-sdk/lib-storage" ;
410import { getSignedUrl } from "@aws-sdk/s3-request-presigner" ;
511import { ConfiguredRetryStrategy } from "@smithy/util-retry" ;
12+ import { Glob } from "bun" ;
613import { lookup } from "mime-types" ;
7- import { S3SyncClient } from "s3-sync-client " ;
14+ import { assert } from "shared/assert " ;
815import { env } from "../env" ;
9- import type { PutObjectCommandInput } from "@aws-sdk/client-s3" ;
10- import type { CommandInput } from "s3-sync-client" ;
1116
12- const retryStrategy = new ConfiguredRetryStrategy ( 5 , 60_000 ) ;
17+ const retryStrategy = new ConfiguredRetryStrategy (
18+ 5 ,
19+ ( attempt ) => attempt ** 2 * 1000 ,
20+ ) ;
1321
1422const client = new S3 ( {
1523 endpoint : env . S3_ENDPOINT ,
@@ -18,88 +26,136 @@ const client = new S3({
1826 accessKeyId : env . S3_ACCESS_KEY ,
1927 secretAccessKey : env . S3_SECRET_KEY ,
2028 } ,
21- logger : console ,
2229 retryStrategy,
2330} ) ;
2431
25- const { sync } = new S3SyncClient ( { client, retryStrategy } ) ;
26-
27- export async function syncFromS3 ( remotePath : string , localPath : string ) {
28- await sync ( `s3://${ env . S3_BUCKET } /${ remotePath } ` , localPath ) ;
29- }
30-
31- export async function syncToS3 (
32+ export async function s3UploadFile (
3233 localPath : string ,
3334 remotePath : string ,
34- options ?: {
35- del ?: boolean ;
36- public ?: boolean ;
37- concurrency ?: number ;
38- } ,
35+ aclPublic : boolean ,
3936) {
40- const commandInput : CommandInput < PutObjectCommandInput > = ( input ) => {
41- let contentType : string | undefined ;
42- if ( input . Key ) {
43- contentType = lookup ( input . Key ) || "binary/octet-stream" ;
44- }
45- return {
46- ContentType : contentType ,
47- ACL : options ?. public ? "public-read" : "private" ,
48- } ;
49- } ;
50-
51- await sync ( localPath , `s3://${ env . S3_BUCKET } /${ remotePath } ` , {
52- del : options ?. del ,
53- commandInput,
54- maxConcurrentTransfers : options ?. concurrency ,
37+ const upload = new Upload ( {
38+ client,
39+ params : {
40+ Body : Bun . file ( localPath ) . stream ( ) ,
41+ ContentType : lookup ( localPath ) || "binary/octet-stream" ,
42+ Bucket : env . S3_BUCKET ,
43+ Key : remotePath ,
44+ ACL : aclPublic ? "public-read" : "private" ,
45+ } ,
5546 } ) ;
47+ await upload . done ( ) ;
5648}
5749
58- type UploadToS3File =
59- | { type : "json" ; data : object }
60- | { type : "local" ; path : string } ;
50+ async function s3DownloadFile ( remotePath : string , localPath : string ) {
51+ const command = new GetObjectCommand ( {
52+ Bucket : env . S3_BUCKET ,
53+ Key : remotePath ,
54+ } ) ;
6155
62- export async function uploadToS3 (
63- remoteFilePath : string ,
64- file : UploadToS3File ,
65- onProgress ?: ( value : number ) => void ,
56+ const { Body } = await client . send ( command ) ;
57+ assert ( Body ) ;
58+
59+ await writeFile ( localPath , Body . transformToWebStream ( ) ) ;
60+ }
61+
62+ export async function s3DownloadFolder ( remotePath : string , localPath : string ) {
63+ const paginatedListObjects = paginateListObjectsV2 (
64+ { client } ,
65+ {
66+ Bucket : env . S3_BUCKET ,
67+ Prefix : remotePath ,
68+ } ,
69+ ) ;
70+
71+ const filePaths : string [ ] = [ ] ;
72+
73+ for await ( const data of paginatedListObjects ) {
74+ data . Contents ?. forEach ( ( content ) => {
75+ if ( content . Key ) {
76+ filePaths . push ( content . Key ) ;
77+ }
78+ } ) ;
79+ }
80+
81+ for ( const filePath of filePaths ) {
82+ const localFilePath = join (
83+ localPath ,
84+ filePath . substring ( remotePath . length + 1 ) ,
85+ ) ;
86+
87+ const localFilePathDir = dirname ( localFilePath ) ;
88+ const folderExists = await exists ( localFilePathDir ) ;
89+ if ( ! folderExists ) {
90+ await mkdir ( localFilePathDir , { recursive : true } ) ;
91+ }
92+
93+ await s3DownloadFile ( filePath , localFilePath ) ;
94+ }
95+ }
96+
97+ export async function s3UploadFolder (
98+ localPath : string ,
99+ remotePath : string ,
100+ aclPublic : boolean ,
66101) {
67- let params : Omit < PutObjectCommandInput , "Bucket" | "Key" > | undefined ;
68-
69- switch ( file . type ) {
70- case "json" :
71- params = {
72- Body : JSON . stringify ( file . data , null , 2 ) ,
73- ContentType : "application/json" ,
74- } ;
75- break ;
76- case "local" :
77- params = {
78- Body : createReadStream ( file . path ) ,
79- } ;
80- break ;
81- default :
82- return ;
102+ await s3DeleteFolder ( remotePath ) ;
103+
104+ const glob = new Glob ( "**/*" ) ;
105+
106+ const files : string [ ] = [ ] ;
107+ for await ( const file of glob . scan ( localPath ) ) {
108+ files . push ( file ) ;
83109 }
84110
111+ for ( const file of files ) {
112+ await s3UploadFile (
113+ `${ localPath } /${ file } ` ,
114+ `${ remotePath } /${ file } ` ,
115+ aclPublic ,
116+ ) ;
117+ }
118+ }
119+
120+ export async function s3UploadJson ( data : object , remotePath : string ) {
85121 const upload = new Upload ( {
86122 client,
87123 params : {
88- ...params ,
124+ Body : JSON . stringify ( data , null , 2 ) ,
125+ ContentType : "application/json" ,
89126 Bucket : env . S3_BUCKET ,
90- Key : remoteFilePath ,
127+ Key : remotePath ,
91128 } ,
92129 } ) ;
130+ await upload . done ( ) ;
131+ }
93132
94- upload . on ( "httpUploadProgress" , ( event ) => {
95- if ( event . loaded === undefined || event . total === undefined ) {
96- return ;
97- }
98- const value = Math . round ( ( event . loaded / event . total ) * 100 ) ;
99- onProgress ?.( value ) ;
100- } ) ;
133+ async function s3DeleteFolder ( remotePath : string ) {
134+ const paginatedListObjects = paginateListObjectsV2 (
135+ { client } ,
136+ {
137+ Bucket : env . S3_BUCKET ,
138+ Prefix : remotePath ,
139+ } ,
140+ ) ;
101141
102- await upload . done ( ) ;
142+ const filePaths : string [ ] = [ ] ;
143+
144+ for await ( const data of paginatedListObjects ) {
145+ data . Contents ?. forEach ( ( content ) => {
146+ if ( content . Key ) {
147+ filePaths . push ( content . Key ) ;
148+ }
149+ } ) ;
150+ }
151+
152+ for ( const filePath of filePaths ) {
153+ const command = new DeleteObjectCommand ( {
154+ Bucket : env . S3_BUCKET ,
155+ Key : filePath ,
156+ } ) ;
157+ await client . send ( command ) ;
158+ }
103159}
104160
105161export async function getS3SignedUrl (
@@ -110,8 +166,6 @@ export async function getS3SignedUrl(
110166 Bucket : env . S3_BUCKET ,
111167 Key : remoteFilePath ,
112168 } ) ;
113- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
114- // @ts -ignore https://github.com/aws/aws-sdk-js-v3/issues/4451
115169 const url = await getSignedUrl ( client , command , {
116170 expiresIn,
117171 } ) ;
0 commit comments