Skip to content

Commit 65cee30

Browse files
committed
Review comments:
- 416 in case of invalid Range header - Fast hash in upload key instead of ID - Fixing tests
1 parent e77a8ca commit 65cee30

File tree

4 files changed

+244
-195
lines changed

4 files changed

+244
-195
lines changed

static/js/file-transfer.js

Lines changed: 171 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const CHUNK_SIZE_MOBILE = 32 * 1024; // 32KiB for mobile
22
const CHUNK_SIZE_DESKTOP = 64 * 1024; // 64KiB for desktop devices
33
const BUFFER_THRESHOLD_MOBILE = CHUNK_SIZE_MOBILE * 16; // 512KiB buffer threshold for mobile
44
const 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
56
const BUFFER_CHECK_INTERVAL = 200; // 200ms interval for buffer checks
67
const SHARE_LINK_FOCUS_DELAY = 300; // 300ms delay before focusing share link
78
const 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+
119160
function 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 /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/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+
131300
function 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-
256382
async 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 /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/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

Comments
 (0)