@@ -22,6 +22,15 @@ const { AWS_REGION, S3_BUCKET, WEBSOCKET_URL, LOCAL_DIR } =
22
22
"LOCAL_DIR" ,
23
23
) ;
24
24
25
+ const recentLocalDeletions = new Set < string > ( ) ;
26
+ const recentDownloads = new Set < string > ( ) ;
27
+ const recentDeletions = new Set < string > ( ) ;
28
+ const recentUploads = new Set < string > ( ) ;
29
+ // Time between remote operations have finished and the resulting S3 event that we will get - which is hopefully earlier than this timeout.
30
+ const RECENT_REMOTE_TIMEOUT = 2000 ;
31
+ // Time between writing the download has finished and chokidar hopefully getting triggered earlier than this.
32
+ const RECENT_LOCAL_TIMEOUT = 500 ;
33
+
25
34
const s3Client = new S3Client ( { region : AWS_REGION } ) ;
26
35
27
36
// Ensure the local sync directory exists
@@ -30,7 +39,7 @@ fs.mkdir(LOCAL_DIR, { recursive: true });
30
39
const ws = new WebSocket ( WEBSOCKET_URL ) ;
31
40
32
41
ws . on ( "open" , ( ) => {
33
- console . log ( " Connected to WebSocket server" ) ;
42
+ console . log ( ` Connected to ${ WEBSOCKET_URL } ` ) ;
34
43
} ) ;
35
44
36
45
ws . on ( "message" , async ( data ) => {
@@ -60,10 +69,17 @@ ws.on("message", async (data) => {
60
69
61
70
ws . on ( "close" , ( ) => {
62
71
console . log ( "Disconnected from WebSocket server" ) ;
72
+ process . exit ( 1 ) ;
63
73
} ) ;
64
74
65
75
async function downloadFile ( key : string ) {
66
76
const localPath = path . join ( LOCAL_DIR , key ) ;
77
+ if ( recentUploads . has ( localPath ) ) {
78
+ console . log (
79
+ `Skipping download for file recently uploaded to S3: ${ localPath } ` ,
80
+ ) ;
81
+ return ;
82
+ }
67
83
68
84
try {
69
85
const { Body } = await s3Client . send (
@@ -74,9 +90,16 @@ async function downloadFile(key: string) {
74
90
) ;
75
91
76
92
if ( Body ) {
93
+ recentDownloads . add ( localPath ) ;
94
+
77
95
await fs . mkdir ( path . dirname ( localPath ) , { recursive : true } ) ;
78
96
await fs . writeFile ( localPath , await Body . transformToByteArray ( ) ) ;
79
97
console . log ( `Downloaded: ${ key } ` ) ;
98
+
99
+ // Start timeout only after writing the file has finished
100
+ setTimeout ( ( ) => {
101
+ recentDownloads . delete ( localPath ) ;
102
+ } , RECENT_LOCAL_TIMEOUT ) ;
80
103
}
81
104
} catch ( error ) {
82
105
console . error ( `Error downloading file ${ key } :` , error ) ;
@@ -85,10 +108,22 @@ async function downloadFile(key: string) {
85
108
86
109
async function removeLocalFile ( key : string ) {
87
110
const localPath = path . join ( LOCAL_DIR , key ) ;
111
+ if ( recentDeletions . has ( localPath ) ) {
112
+ console . log (
113
+ `Skipping local removal for file recently deleted on S3: ${ localPath } ` ,
114
+ ) ;
115
+ return ;
116
+ }
88
117
89
118
try {
119
+ recentLocalDeletions . add ( localPath ) ;
120
+
90
121
await fs . unlink ( localPath ) ;
91
122
console . log ( `Removed local file: ${ localPath } ` ) ;
123
+
124
+ setTimeout ( ( ) => {
125
+ recentLocalDeletions . delete ( localPath ) ;
126
+ } , RECENT_LOCAL_TIMEOUT ) ;
92
127
} catch ( error ) {
93
128
if ( ( error as NodeJS . ErrnoException ) . code !== "ENOENT" ) {
94
129
console . error ( `Error removing local file ${ localPath } :` , error ) ;
@@ -98,11 +133,47 @@ async function removeLocalFile(key: string) {
98
133
}
99
134
}
100
135
136
+ async function removeFile ( localPath : string ) {
137
+ if ( recentLocalDeletions . has ( localPath ) ) {
138
+ console . log (
139
+ `Skipping repeated S3 removal for recently deleted file: ${ localPath } ` ,
140
+ ) ;
141
+ return ;
142
+ }
143
+
144
+ const key = path . relative ( LOCAL_DIR , localPath ) ;
145
+
146
+ try {
147
+ recentDeletions . add ( localPath ) ;
148
+
149
+ await s3Client . send (
150
+ new DeleteObjectCommand ( {
151
+ Bucket : S3_BUCKET ,
152
+ Key : key ,
153
+ } ) ,
154
+ ) ;
155
+ console . log ( `Deleted from S3: ${ key } ` ) ;
156
+
157
+ setTimeout ( ( ) => {
158
+ recentDeletions . delete ( localPath ) ;
159
+ } , RECENT_REMOTE_TIMEOUT ) ;
160
+ } catch ( error ) {
161
+ console . error ( `Error deleting file ${ key } from S3:` , error ) ;
162
+ }
163
+ }
164
+
101
165
async function syncFile ( localPath : string ) {
166
+ if ( recentDownloads . has ( localPath ) ) {
167
+ console . log ( `Skipping upload for recently downloaded file: ${ localPath } ` ) ;
168
+ return ;
169
+ }
170
+
102
171
const key = path . relative ( LOCAL_DIR , localPath ) ;
103
172
104
173
async function uploadFile ( ) {
105
174
try {
175
+ recentUploads . add ( localPath ) ;
176
+
106
177
const fileContent = await fs . readFile ( localPath ) ;
107
178
await s3Client . send (
108
179
new PutObjectCommand ( {
@@ -112,6 +183,10 @@ async function syncFile(localPath: string) {
112
183
} ) ,
113
184
) ;
114
185
console . log ( `Uploaded: ${ key } ` ) ;
186
+
187
+ setTimeout ( ( ) => {
188
+ recentUploads . delete ( localPath ) ;
189
+ } , RECENT_REMOTE_TIMEOUT ) ;
115
190
} catch ( error ) {
116
191
console . error ( `Error uploading file ${ key } :` , error ) ;
117
192
}
@@ -140,25 +215,10 @@ async function syncFile(localPath: string) {
140
215
}
141
216
}
142
217
143
- // Watch for local file changes
144
- const watcher = chokidar . watch ( LOCAL_DIR ) ;
145
-
146
- watcher
147
- . on ( "add" , syncFile )
148
- . on ( "change" , syncFile )
149
- . on ( "unlink" , async ( localPath ) => {
150
- const key = path . relative ( LOCAL_DIR , localPath ) ;
151
- try {
152
- await s3Client . send (
153
- new DeleteObjectCommand ( {
154
- Bucket : S3_BUCKET ,
155
- Key : key ,
156
- } ) ,
157
- ) ;
158
- console . log ( `Deleted from S3: ${ key } ` ) ;
159
- } catch ( error ) {
160
- console . error ( `Error deleting file ${ key } from S3:` , error ) ;
161
- }
162
- } ) ;
218
+ // Don't use for`awaitWriteFinish` because that would cause conflicts with the RECENT_TIMEOUT. Because that time only starts once writing has finished, potential `add` events during writing are ignored anyway.
219
+ const watcher = chokidar . watch ( LOCAL_DIR , {
220
+ ignoreInitial : true ,
221
+ } ) ;
222
+ watcher . on ( "add" , syncFile ) . on ( "change" , syncFile ) . on ( "unlink" , removeFile ) ;
163
223
164
224
console . log ( `Watching for changes in ${ LOCAL_DIR } ` ) ;
0 commit comments