-
Notifications
You must be signed in to change notification settings - Fork 0
Chunked + background uploading #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
a0a0da8
327a27b
2172796
683d11d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
importScripts('https://cdnjs.cloudflare.com/ajax/libs/dexie/4.0.10/dexie.min.js'); | ||
|
||
class SWDB extends Dexie { | ||
constructor() { | ||
super('UploadDB'); | ||
this.version(1).stores({ | ||
uploads: '++id, challengeId, attemptId, chunkIndex', | ||
token: 'id, token' | ||
}); | ||
|
||
this.uploads = this.table('uploads'); | ||
this.token = this.table('token'); | ||
} | ||
|
||
async saveToken(token) { | ||
await this.token.put({ id: 1, token }); | ||
} | ||
|
||
async getToken() { | ||
const tokenRecord = await this.token.get(1); | ||
return tokenRecord ? tokenRecord.token : null; | ||
} | ||
} | ||
|
||
const db = new SWDB(); | ||
|
||
self.addEventListener('message', (event) => { | ||
if (event.data.type === 'SET_TOKEN') { | ||
db.saveToken(event.data.token) | ||
// const token = event.data.token; | ||
// // Now you can store the token in the service worker's context or use it | ||
// // Example: Store it in a variable or send it with network requests | ||
// self.token = token; | ||
} | ||
}); | ||
|
||
self.addEventListener('sync', (event) => { | ||
if (event.tag === 'sync-uploads') { | ||
event.waitUntil(retryFailedUploads()); | ||
} | ||
}); | ||
|
||
// Retry failed uploads using Dexie.js | ||
async function retryFailedUploads() { | ||
try { | ||
const authToken = await db.getToken() | ||
// const db = new UploadDatabase(); // Instantiate your Dexie database class | ||
|
||
// Fetch all failed uploads from the Dexie DB | ||
const uploads = await db.uploads.toArray(); | ||
|
||
for (const chunk of uploads) { | ||
try { | ||
// Recreate FormData voor deze chunk | ||
const formData = new FormData(); | ||
const blob = new Blob([ chunk.data ], { type: chunk.fileType }); | ||
|
||
formData.append('chunk', blob); | ||
formData.append('fileName', chunk.fileName); | ||
formData.append('fileType', chunk.fileType); | ||
formData.append('chunkIndex', chunk.chunkIndex); | ||
|
||
// Upload chunk | ||
const response = await fetch(`/api/challenges/${chunk.challengeId}/attempt/${chunk.attemptId}`, { | ||
method: 'POST', | ||
headers: { | ||
Authorization: `Bearer ${authToken}` | ||
}, | ||
body: formData | ||
}); | ||
|
||
if (response.ok) { | ||
console.log('Upload successful, removing from IndexedDB'); | ||
await db.uploads.delete(chunk.id); // Delete the entry from Dexie DB after successful upload | ||
} else if (response.status === 423) { // currently in review or completed. aka upload no longer needed | ||
console.log('resource is locked, removing from IndexedDB'); | ||
await db.uploads.delete(chunk.id); // Delete the entry from Dexie DB after successful upload | ||
} else { | ||
console.error(`Upload failed for challenge ${chunk.challengeId}`); | ||
await db.uploads.delete(chunk.id); | ||
} | ||
} catch (err) { | ||
console.error('Upload retry failed, will retry later', err); | ||
} | ||
} | ||
} catch (error) { | ||
console.error('Failed to retrieve uploads from IndexedDB', error); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import axios, { AxiosProgressEvent, AxiosRequestConfig } from "axios"; | ||
import axios, { AxiosProgressEvent, AxiosRequestConfig, AxiosResponse } from "axios"; | ||
import { Challenge } from "../model/challenge.tsx"; | ||
import { db } from "../utils/DexieDB.ts"; | ||
|
||
export const getChallenges = () => { | ||
const config: AxiosRequestConfig = { | ||
|
@@ -11,16 +12,114 @@ export const getChallenges = () => { | |
return axios.get<Challenge[]>("/api/challenges", config); | ||
} | ||
|
||
export const uploadChallenge = (challenge: Challenge, formData: FormData, setUploadPercentage: (percentage: number) => void) => { | ||
export const uploadChallenge = async ( | ||
challenge: Challenge, | ||
formData: FormData, | ||
setUploadPercentage: (percentage: number) => void | ||
) => { | ||
const config: AxiosRequestConfig = { | ||
headers: { | ||
'content-type': 'multipart/form-data', | ||
Authorization: `Bearer ${localStorage.getItem("token")}` | ||
}, | ||
onUploadProgress: (progressEvent: AxiosProgressEvent) => { | ||
setUploadPercentage(Math.round((progressEvent.loaded / (progressEvent.total ?? 1) * 100))) | ||
Authorization: `Bearer ${localStorage.getItem('token')}` | ||
} | ||
}; | ||
|
||
const chunkSize = 1024 * 1024; // 1MB | ||
|
||
|
||
const result = await axios.post(`/api/challenges/${challenge.id}/attempt`, | ||
Object.fromEntries((formData.getAll("files") as File[]).map((file: File) => [ file.name, Math.ceil(file.size / chunkSize) ])) | ||
, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }) | ||
|
||
const attemptId = result.data | ||
|
||
await uploadFilesInChunks( | ||
formData, | ||
challenge.id, | ||
attemptId, | ||
config, | ||
setUploadPercentage | ||
) | ||
}; | ||
|
||
async function uploadFilesInChunks(formData: FormData, challengeId: number, attemptId: any, config: AxiosRequestConfig, setUploadPercentage: (percentage: number) => void) { | ||
const uploads: Promise<void>[] = [] | ||
const files = formData.getAll('files') as File[]; // Get all files from the 'files' input | ||
|
||
// Bereken totaal aantal chunks | ||
const chunkSize = 1024 * 1024; // 1MB | ||
const totalChunks = files.reduce((total, file) => { | ||
return total + Math.ceil(file.size / chunkSize); | ||
}, 0); | ||
|
||
let completedChunks = 0; | ||
|
||
if (files.length > 0) { | ||
files.forEach(file => { | ||
const fileName = file.name; | ||
const fileType = file.type | ||
const chunkSize = 1024 * 1024; // 1MB | ||
const amountOfChunks = Math.ceil(file.size / chunkSize); | ||
|
||
let start = 0; | ||
let end = chunkSize; | ||
|
||
for (let chunkIndex = 0; chunkIndex < amountOfChunks; chunkIndex++) { | ||
const chunk = file.slice(start, end); | ||
const upload = uploadChunk(challengeId, attemptId, config, chunk, fileName, fileType, chunkIndex) | ||
.then(() => { | ||
completedChunks++; | ||
const percentage = Math.round((completedChunks / totalChunks) * 100); | ||
setUploadPercentage(percentage); | ||
}); | ||
uploads.push(upload); | ||
|
||
start = end; | ||
end = Math.min(start + chunkSize, file.size); | ||
} | ||
}); | ||
} else { | ||
console.error('No files found in FormData'); | ||
} | ||
await Promise.all(uploads) | ||
} | ||
|
||
return axios.post(`/api/challenges/${challenge.id}`, formData, config); | ||
async function uploadChunk(challengeId: number, attemptId: any, config: AxiosRequestConfig, chunk: Blob, fileName: string, fileType: string, chunkIndex: number) { | ||
const chunkFormData = new FormData(); | ||
chunkFormData.append('chunk', chunk); | ||
chunkFormData.append('fileName', fileName); // Send the file name for reference | ||
chunkFormData.append('fileType', fileType); // Send the file name for reference | ||
chunkFormData.append('chunkIndex', String(chunkIndex)); | ||
|
||
try { | ||
const response = await axios.post(`/api/challenges/${challengeId}/attempt/${attemptId}`, chunkFormData, config) | ||
return response | ||
|
||
} catch (err) { | ||
console.warn('Upload failed, saving to IndexedDB for retry later'); | ||
|
||
|
||
await db.uploads.add({ | ||
challengeId, | ||
attemptId, | ||
chunkIndex: String(chunkIndex), | ||
fileName, | ||
fileType, | ||
data: await chunk.arrayBuffer(), | ||
}); | ||
Comment on lines
+102
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another typescript error somewhere along here.
|
||
|
||
// Check if sync is supported | ||
|
||
const registration = await navigator.serviceWorker.ready; | ||
|
||
if ('sync' in registration) { | ||
registration.sync.register('sync-uploads'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TS error here:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not all browser support background uploading. See article: https://developer.mozilla.org/en-US/docs/Web/API/SyncManager Added own definition based on mdn docs |
||
} else { | ||
console.info('No background uploading detected') | ||
} | ||
|
||
throw err; | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import Dexie from 'dexie'; | ||
|
||
interface UploadEntry { | ||
id?: number; | ||
challengeId: number; | ||
attemptId: string; | ||
chunkIndex: string; | ||
fileName: string; | ||
fileType: string; | ||
data: ArrayBuffer; | ||
} | ||
|
||
class UploadDatabase extends Dexie { | ||
uploads: Dexie.Table<UploadEntry, number>; | ||
|
||
constructor() { | ||
super('UploadDB'); | ||
this.version(1).stores({ | ||
uploads: '++id, challengeId, attemptId, chunkIndex', | ||
}); | ||
|
||
this.uploads = this.table('uploads'); | ||
} | ||
} | ||
|
||
export const db = new UploadDatabase(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vercel geeft de volgende error op deze line:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not all browser support background uploading. See article: https://developer.mozilla.org/en-US/docs/Web/API/SyncManager
Added own definition based on mdn docs