@@ -2,6 +2,7 @@ const CHUNK_SIZE_MOBILE = 32 * 1024; // 32KiB for mobile
22const CHUNK_SIZE_DESKTOP = 64 * 1024 ; // 64KiB for desktop devices
33const BUFFER_THRESHOLD_MOBILE = CHUNK_SIZE_MOBILE * 16 ; // 512KiB buffer threshold for mobile
44const BUFFER_THRESHOLD_DESKTOP = CHUNK_SIZE_DESKTOP * 16 ; // 1MiB buffer threshold for desktop
5+ const MAX_HASH_SAMPLING = 2 * 1024 ** 2 ; // Sample up to 2MiB for file hash
56const BUFFER_CHECK_INTERVAL = 200 ; // 200ms interval for buffer checks
67const SHARE_LINK_FOCUS_DELAY = 300 ; // 300ms delay before focusing share link
78const TRANSFER_FINALIZE_DELAY = 500 ; // 500ms delay before finalizing transfer
@@ -116,6 +117,46 @@ function updateProgress(elements, progress) {
116117 }
117118}
118119
120+ function saveUploadProgress ( key , bytesUploaded , transferId ) {
121+ try {
122+ const progress = {
123+ bytesUploaded : bytesUploaded ,
124+ transferId : transferId ,
125+ timestamp : Date . now ( )
126+ } ;
127+ localStorage . setItem ( key , JSON . stringify ( progress ) ) ;
128+ log . debug ( 'Progress saved:' , progress ) ;
129+ } catch ( e ) {
130+ log . warn ( 'Failed to save progress:' , e ) ;
131+ }
132+ }
133+
134+ function getUploadProgress ( key ) {
135+ try {
136+ const saved = localStorage . getItem ( key ) ;
137+ if ( saved ) {
138+ const progress = JSON . parse ( saved ) ;
139+ // Only use progress if less than 1 hour old
140+ if ( Date . now ( ) - progress . timestamp < 3600000 ) {
141+ return progress ;
142+ }
143+ localStorage . removeItem ( key ) ;
144+ }
145+ } catch ( e ) {
146+ log . warn ( 'Failed to load progress:' , e ) ;
147+ }
148+ return null ;
149+ }
150+
151+ function clearUploadProgress ( key ) {
152+ try {
153+ localStorage . removeItem ( key ) ;
154+ log . debug ( 'Progress cleared' ) ;
155+ } catch ( e ) {
156+ log . warn ( 'Failed to clear progress:' , e ) ;
157+ }
158+ }
159+
119160function displayShareLink ( elements , transferId ) {
120161 const { shareUrl, shareLink, dropArea } = elements ;
121162 shareUrl . value = `${ window . location . origin } /${ transferId } ` ;
@@ -128,9 +169,138 @@ function displayShareLink(elements, transferId) {
128169 } , SHARE_LINK_FOCUS_DELAY ) ;
129170}
130171
172+ function handleWsOpen ( ws , file , transferId , elements ) {
173+ log . info ( 'WebSocket connection opened' ) ;
174+ const metadata = {
175+ file_name : file . name ,
176+ file_size : file . size ,
177+ file_type : file . type || 'application/octet-stream'
178+ } ;
179+ log . info ( 'Sending file metadata:' , metadata ) ;
180+ ws . send ( JSON . stringify ( metadata ) ) ;
181+ elements . statusText . textContent = 'Waiting for the receiver to start the download... (max. 5 minutes)' ;
182+ displayShareLink ( elements , transferId ) ;
183+ }
184+
185+ function handleWsMessage ( event , ws , file , elements , abortController , uploadState ) {
186+ log . debug ( 'WebSocket message received:' , event . data ) ;
187+ if ( event . data === 'Go for file chunks' ) {
188+ log . info ( 'Receiver connected, starting file transfer' ) ;
189+ elements . statusText . textContent = 'Peer connected. Transferring file...' ;
190+ uploadState . isUploading = true ;
191+ sendFileInChunks ( ws , file , elements , abortController , uploadState ) ;
192+ } else if ( event . data . startsWith ( 'Resume from:' ) ) {
193+ const resumeBytes = parseInt ( event . data . split ( ':' ) [ 1 ] . trim ( ) ) ;
194+ log . info ( 'Resuming from byte:' , resumeBytes ) ;
195+ elements . statusText . textContent = `Resuming transfer from ${ Math . round ( resumeBytes / file . size * 100 ) } %...` ;
196+ uploadState . isUploading = true ;
197+ uploadState . resumePosition = resumeBytes ;
198+ sendFileInChunks ( ws , file , elements , abortController , uploadState ) ;
199+ } else if ( event . data . startsWith ( 'Error' ) ) {
200+ log . error ( 'Server error:' , event . data ) ;
201+ elements . statusText . textContent = event . data ;
202+ elements . statusText . style . color = 'var(--error)' ;
203+ clearUploadProgress ( uploadState . uploadKey ) ;
204+ cleanupTransfer ( abortController , uploadState ) ;
205+ } else {
206+ log . warn ( 'Unexpected message:' , event . data ) ;
207+ }
208+ }
209+
210+ function handleWsError ( error , statusText ) {
211+ log . error ( 'WebSocket error:' , error ) ;
212+ statusText . textContent = 'Error: ' + ( error . message || 'Connection failed' ) ;
213+ statusText . style . color = 'var(--error)' ;
214+ }
215+
216+ function isMobileDevice ( ) {
217+ return / A n d r o i d | w e b O S | i P h o n e | i P a d | i P o d | B l a c k B e r r y | I E M o b i l e | O p e r a M i n i / i. test ( navigator . userAgent ) ||
218+ ( window . matchMedia && window . matchMedia ( `(max-width: ${ MOBILE_BREAKPOINT } px)` ) . matches ) ;
219+ }
220+
221+ async function requestWakeLock ( uploadState ) {
222+ try {
223+ uploadState . wakeLock = await navigator . wakeLock . request ( 'screen' ) ;
224+ log . info ( 'Wake lock acquired to prevent screen sleep' ) ;
225+ uploadState . wakeLock . addEventListener ( 'release' , ( ) => log . debug ( 'Wake lock released' ) ) ;
226+ } catch ( err ) {
227+ log . warn ( 'Wake lock request failed:' , err . message ) ;
228+ }
229+ }
230+
231+ function generateTransferId ( ) {
232+ const uuid = self . crypto . randomUUID ( ) ;
233+ const hex = uuid . replace ( / - / g, '' ) ;
234+ const consonants = 'bcdfghjklmnpqrstvwxyz' ;
235+ const vowels = 'aeiou' ;
236+
237+ const createWord = ( hexSegment ) => {
238+ let word = '' ;
239+ for ( let i = 0 ; i < hexSegment . length ; i ++ ) {
240+ const charCode = parseInt ( hexSegment [ i ] , 16 ) ;
241+ word += ( i % 2 === 0 ) ? consonants [ charCode % consonants . length ] : vowels [ charCode % vowels . length ] ;
242+ }
243+ return word ;
244+ } ;
245+
246+ const word1 = createWord ( hex . substring ( 0 , 6 ) ) ;
247+ const word2 = createWord ( hex . substring ( 6 , 12 ) ) ;
248+ const num = parseInt ( hex . substring ( 12 , 15 ) , 16 ) % TRANSFER_ID_MAX_NUMBER ;
249+
250+ const transferId = `${ word1 } -${ word2 } -${ num } ` ;
251+ log . debug ( 'Generated transfer ID:' , transferId ) ;
252+ return transferId ;
253+ }
254+
255+ function calculateFileHash ( file ) {
256+ const sample_size = Math . min ( file . size , MAX_HASH_SAMPLING ) ;
257+ const reader = new FileReader ( ) ;
258+ let hash = 0 ;
259+
260+ return new Promise ( ( resolve , reject ) => {
261+ const processChunk = ( offset ) => {
262+ if ( offset >= sample_size ) {
263+ // Include file size and name in hash for uniqueness
264+ hash = hash ^ file . size ^ simpleStringHash ( file . name ) ;
265+ resolve ( Math . abs ( hash ) . toString ( 16 ) ) ;
266+ return ;
267+ }
268+ reader . onerror = ( ) => reject ( new Error ( 'Failed to read file chunk' ) ) ;
269+ reader . onload = ( e ) => {
270+ const chunk = new Uint8Array ( e . target . result ) ;
271+ // Fast hash algorithm (FNV-1a variant)
272+ for ( let i = 0 ; i < chunk . length ; i ++ ) {
273+ hash = hash ^ chunk [ i ] ;
274+ hash = hash * 16777619 ;
275+ hash = hash >>> 0 ;
276+ }
277+
278+ processChunk ( offset + CHUNK_SIZE_DESKTOP ) ;
279+ } ;
280+
281+ const end = Math . min ( offset + CHUNK_SIZE_DESKTOP , sample_size ) ;
282+ const slice = file . slice ( offset , end ) ;
283+ reader . readAsArrayBuffer ( slice ) ;
284+ } ;
285+
286+ processChunk ( 0 ) ;
287+ } ) ;
288+ }
289+
290+ function simpleStringHash ( str ) {
291+ let hash = 0 ;
292+ for ( let i = 0 ; i < str . length ; i ++ ) {
293+ const char = str . charCodeAt ( i ) ;
294+ hash = ( ( hash << 5 ) - hash ) + char ;
295+ hash = hash >>> 0 ; // Convert to 32-bit unsigned
296+ }
297+ return hash ;
298+ }
299+
131300function uploadFile ( file , elements ) {
132301 const transferId = generateTransferId ( ) ;
133- const uploadKey = `upload_${ transferId } _${ file . name } _${ file . size } ` ;
302+ const fileHash = calculateFileHash ( file ) ;
303+ const uploadKey = `upload_${ fileHash } ` ;
134304 const savedProgress = getUploadProgress ( uploadKey ) ;
135305 const isResume = savedProgress && savedProgress . bytesUploaded > 0 ;
136306
@@ -209,50 +379,6 @@ function uploadFile(file, elements) {
209379 }
210380}
211381
212- function handleWsOpen ( ws , file , transferId , elements ) {
213- log . info ( 'WebSocket connection opened' ) ;
214- const metadata = {
215- file_name : file . name ,
216- file_size : file . size ,
217- file_type : file . type || 'application/octet-stream'
218- } ;
219- log . info ( 'Sending file metadata:' , metadata ) ;
220- ws . send ( JSON . stringify ( metadata ) ) ;
221- elements . statusText . textContent = 'Waiting for the receiver to start the download... (max. 5 minutes)' ;
222- displayShareLink ( elements , transferId ) ;
223- }
224-
225- function handleWsMessage ( event , ws , file , elements , abortController , uploadState ) {
226- log . debug ( 'WebSocket message received:' , event . data ) ;
227- if ( event . data === 'Go for file chunks' ) {
228- log . info ( 'Receiver connected, starting file transfer' ) ;
229- elements . statusText . textContent = 'Peer connected. Transferring file...' ;
230- uploadState . isUploading = true ;
231- sendFileInChunks ( ws , file , elements , abortController , uploadState ) ;
232- } else if ( event . data . startsWith ( 'Resume from:' ) ) {
233- const resumeBytes = parseInt ( event . data . split ( ':' ) [ 1 ] . trim ( ) ) ;
234- log . info ( 'Resuming from byte:' , resumeBytes ) ;
235- elements . statusText . textContent = `Resuming transfer from ${ Math . round ( resumeBytes / file . size * 100 ) } %...` ;
236- uploadState . isUploading = true ;
237- uploadState . resumePosition = resumeBytes ;
238- sendFileInChunks ( ws , file , elements , abortController , uploadState ) ;
239- } else if ( event . data . startsWith ( 'Error' ) ) {
240- log . error ( 'Server error:' , event . data ) ;
241- elements . statusText . textContent = event . data ;
242- elements . statusText . style . color = 'var(--error)' ;
243- clearUploadProgress ( uploadState . uploadKey ) ;
244- cleanupTransfer ( abortController , uploadState ) ;
245- } else {
246- log . warn ( 'Unexpected message:' , event . data ) ;
247- }
248- }
249-
250- function handleWsError ( error , statusText ) {
251- log . error ( 'WebSocket error:' , error ) ;
252- statusText . textContent = 'Error: ' + ( error . message || 'Connection failed' ) ;
253- statusText . style . color = 'var(--error)' ;
254- }
255-
256382async function sendFileInChunks ( ws , file , elements , abortController , uploadState ) {
257383 const chunkSize = isMobileDevice ( ) ? CHUNK_SIZE_MOBILE : CHUNK_SIZE_DESKTOP ;
258384 const startOffset = uploadState . resumePosition || 0 ;
@@ -363,83 +489,3 @@ function cleanupTransfer(abortController, uploadState) {
363489 uploadState . wakeLock = null ;
364490 }
365491}
366-
367- function isMobileDevice ( ) {
368- return / A n d r o i d | w e b O S | i P h o n e | i P a d | i P o d | B l a c k B e r r y | I E M o b i l e | O p e r a M i n i / i. test ( navigator . userAgent ) ||
369- ( window . matchMedia && window . matchMedia ( `(max-width: ${ MOBILE_BREAKPOINT } px)` ) . matches ) ;
370- }
371-
372- // Progress persistence functions
373- function saveUploadProgress ( key , bytesUploaded , transferId ) {
374- try {
375- const progress = {
376- bytesUploaded : bytesUploaded ,
377- transferId : transferId ,
378- timestamp : Date . now ( )
379- } ;
380- localStorage . setItem ( key , JSON . stringify ( progress ) ) ;
381- log . debug ( 'Progress saved:' , progress ) ;
382- } catch ( e ) {
383- log . warn ( 'Failed to save progress:' , e ) ;
384- }
385- }
386-
387- function getUploadProgress ( key ) {
388- try {
389- const saved = localStorage . getItem ( key ) ;
390- if ( saved ) {
391- const progress = JSON . parse ( saved ) ;
392- // Only use progress if less than 1 hour old
393- if ( Date . now ( ) - progress . timestamp < 3600000 ) {
394- return progress ;
395- }
396- localStorage . removeItem ( key ) ;
397- }
398- } catch ( e ) {
399- log . warn ( 'Failed to load progress:' , e ) ;
400- }
401- return null ;
402- }
403-
404- function clearUploadProgress ( key ) {
405- try {
406- localStorage . removeItem ( key ) ;
407- log . debug ( 'Progress cleared' ) ;
408- } catch ( e ) {
409- log . warn ( 'Failed to clear progress:' , e ) ;
410- }
411- }
412-
413- function generateTransferId ( ) {
414- const uuid = self . crypto . randomUUID ( ) ;
415- const hex = uuid . replace ( / - / g, '' ) ;
416- const consonants = 'bcdfghjklmnpqrstvwxyz' ;
417- const vowels = 'aeiou' ;
418-
419- const createWord = ( hexSegment ) => {
420- let word = '' ;
421- for ( let i = 0 ; i < hexSegment . length ; i ++ ) {
422- const charCode = parseInt ( hexSegment [ i ] , 16 ) ;
423- word += ( i % 2 === 0 ) ? consonants [ charCode % consonants . length ] : vowels [ charCode % vowels . length ] ;
424- }
425- return word ;
426- } ;
427-
428- const word1 = createWord ( hex . substring ( 0 , 6 ) ) ;
429- const word2 = createWord ( hex . substring ( 6 , 12 ) ) ;
430- const num = parseInt ( hex . substring ( 12 , 15 ) , 16 ) % TRANSFER_ID_MAX_NUMBER ;
431-
432- const transferId = `${ word1 } -${ word2 } -${ num } ` ;
433- log . debug ( 'Generated transfer ID:' , transferId ) ;
434- return transferId ;
435- }
436-
437- async function requestWakeLock ( uploadState ) {
438- try {
439- uploadState . wakeLock = await navigator . wakeLock . request ( 'screen' ) ;
440- log . info ( 'Wake lock acquired to prevent screen sleep' ) ;
441- uploadState . wakeLock . addEventListener ( 'release' , ( ) => log . debug ( 'Wake lock released' ) ) ;
442- } catch ( err ) {
443- log . warn ( 'Wake lock request failed:' , err . message ) ;
444- }
445- }
0 commit comments