245 lines
8.0 KiB
JavaScript
245 lines
8.0 KiB
JavaScript
// rfiles.online Service Worker
|
|
const CACHE_NAME = 'rfiles-upload-v2';
|
|
const DB_NAME = 'rfiles-upload';
|
|
const DB_VERSION = 2;
|
|
|
|
// Assets to cache for offline use
|
|
const ASSETS_TO_CACHE = [
|
|
'/',
|
|
'/static/portal/manifest.json',
|
|
'/static/portal/icon-192.png',
|
|
'/static/portal/icon-512.png',
|
|
];
|
|
|
|
// Install event - cache assets
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS_TO_CACHE))
|
|
);
|
|
self.skipWaiting();
|
|
});
|
|
|
|
// Activate event - clean old caches
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys().then((cacheNames) =>
|
|
Promise.all(
|
|
cacheNames.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n))
|
|
)
|
|
)
|
|
);
|
|
self.clients.claim();
|
|
});
|
|
|
|
// Fetch event - serve from cache, fallback to network
|
|
self.addEventListener('fetch', (event) => {
|
|
if (event.request.url.includes('/share-target/') || event.request.url.includes('/api/')) {
|
|
return;
|
|
}
|
|
event.respondWith(
|
|
caches.match(event.request).then((r) => r || fetch(event.request))
|
|
);
|
|
});
|
|
|
|
// Handle share target
|
|
self.addEventListener('fetch', (event) => {
|
|
const url = new URL(event.request.url);
|
|
if (url.pathname === '/share-target/' && event.request.method === 'POST') {
|
|
event.respondWith(handleShareTarget(event.request));
|
|
}
|
|
});
|
|
|
|
async function handleShareTarget(request) {
|
|
const formData = await request.formData();
|
|
const title = formData.get('title') || '';
|
|
const text = formData.get('text') || '';
|
|
const url = formData.get('url') || '';
|
|
const files = formData.getAll('files');
|
|
|
|
if (files.length > 0) {
|
|
try {
|
|
const uploadPromises = files.map(async (file) => {
|
|
const fd = new FormData();
|
|
fd.append('file', file);
|
|
fd.append('title', title || file.name);
|
|
fd.append('description', text || url || '');
|
|
return (await fetch('/api/upload/', { method: 'POST', body: fd })).json();
|
|
});
|
|
await Promise.all(uploadPromises);
|
|
const successUrl = new URL('/', self.location.origin);
|
|
successUrl.searchParams.set('shared', 'files');
|
|
successUrl.searchParams.set('count', files.length);
|
|
return Response.redirect(successUrl.toString(), 303);
|
|
} catch (error) {
|
|
const offlineUrl = new URL('/', self.location.origin);
|
|
offlineUrl.searchParams.set('queued', 'true');
|
|
return Response.redirect(offlineUrl.toString(), 303);
|
|
}
|
|
}
|
|
|
|
const redirectUrl = new URL('/', self.location.origin);
|
|
if (url) redirectUrl.searchParams.set('url', url);
|
|
if (text) redirectUrl.searchParams.set('text', text);
|
|
if (title) redirectUrl.searchParams.set('title', title);
|
|
redirectUrl.searchParams.set('shared', 'true');
|
|
return Response.redirect(redirectUrl.toString(), 303);
|
|
}
|
|
|
|
// --- IndexedDB helpers ---
|
|
|
|
function openDB() {
|
|
return new Promise((resolve, reject) => {
|
|
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
req.onerror = () => reject(req.error);
|
|
req.onsuccess = () => resolve(req.result);
|
|
req.onupgradeneeded = (event) => {
|
|
const db = event.target.result;
|
|
if (!db.objectStoreNames.contains('offline-queue')) {
|
|
db.createObjectStore('offline-queue', { keyPath: 'queuedAt' });
|
|
}
|
|
if (!db.objectStoreNames.contains('uploads')) {
|
|
db.createObjectStore('uploads', { keyPath: 'id' });
|
|
}
|
|
if (!db.objectStoreNames.contains('results')) {
|
|
db.createObjectStore('results', { keyPath: 'id' });
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function dbGet(storeName, key) {
|
|
return openDB().then(db => new Promise((resolve, reject) => {
|
|
const tx = db.transaction(storeName, 'readonly');
|
|
const req = tx.objectStore(storeName).get(key);
|
|
req.onsuccess = () => resolve(req.result);
|
|
req.onerror = () => reject(req.error);
|
|
}));
|
|
}
|
|
|
|
function dbPut(storeName, value) {
|
|
return openDB().then(db => new Promise((resolve, reject) => {
|
|
const tx = db.transaction(storeName, 'readwrite');
|
|
const req = tx.objectStore(storeName).put(value);
|
|
req.onsuccess = () => resolve();
|
|
req.onerror = () => reject(req.error);
|
|
}));
|
|
}
|
|
|
|
function dbDelete(storeName, key) {
|
|
return openDB().then(db => new Promise((resolve, reject) => {
|
|
const tx = db.transaction(storeName, 'readwrite');
|
|
const req = tx.objectStore(storeName).delete(key);
|
|
req.onsuccess = () => resolve();
|
|
req.onerror = () => reject(req.error);
|
|
}));
|
|
}
|
|
|
|
function dbGetAll(storeName) {
|
|
return openDB().then(db => new Promise((resolve, reject) => {
|
|
const tx = db.transaction(storeName, 'readonly');
|
|
const req = tx.objectStore(storeName).getAll();
|
|
req.onsuccess = () => resolve(req.result);
|
|
req.onerror = () => reject(req.error);
|
|
}));
|
|
}
|
|
|
|
// --- Background upload handling ---
|
|
|
|
// Broadcast to all open pages
|
|
async function broadcast(msg) {
|
|
const clients = await self.clients.matchAll({ type: 'window' });
|
|
clients.forEach(c => c.postMessage(msg));
|
|
}
|
|
|
|
self.addEventListener('message', (event) => {
|
|
if (event.data && event.data.type === 'START_UPLOAD') {
|
|
event.waitUntil(handleBackgroundUpload(event.data));
|
|
}
|
|
if (event.data && event.data.type === 'GET_RESULTS') {
|
|
event.waitUntil(sendResults(event.source));
|
|
}
|
|
if (event.data && event.data.type === 'CLEAR_RESULT') {
|
|
event.waitUntil(dbDelete('results', event.data.id));
|
|
}
|
|
});
|
|
|
|
async function handleBackgroundUpload(data) {
|
|
const { id, uploadUrl, space, filename, action } = data;
|
|
|
|
try {
|
|
// Read file blob from IndexedDB
|
|
const record = await dbGet('uploads', id);
|
|
if (!record) {
|
|
await broadcast({ type: 'UPLOAD_ERROR', id, error: 'File not found in storage' });
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', record.blob, filename);
|
|
formData.append('space', space);
|
|
if (action) formData.append('action', action);
|
|
|
|
const response = await fetch(uploadUrl, { method: 'POST', body: formData });
|
|
const result = await response.json();
|
|
|
|
// Clean up the stored blob
|
|
await dbDelete('uploads', id);
|
|
|
|
if (response.status === 409 && result.duplicate) {
|
|
// Store duplicate result for the page to handle
|
|
await dbPut('results', { id, status: 'duplicate', data: result, filename, space });
|
|
await broadcast({ type: 'UPLOAD_DUPLICATE', id, data: result, filename });
|
|
} else if (response.ok && result.success) {
|
|
await dbPut('results', { id, status: 'success', data: result, filename });
|
|
await broadcast({ type: 'UPLOAD_COMPLETE', id, data: result });
|
|
// Show notification if no page is focused
|
|
const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: false });
|
|
const hasFocus = clients.some(c => c.focused);
|
|
if (!hasFocus) {
|
|
self.registration.showNotification('Upload complete', {
|
|
body: `${result.file.title} uploaded successfully`,
|
|
icon: '/static/portal/icon-192.png',
|
|
tag: 'upload-' + id,
|
|
});
|
|
}
|
|
} else {
|
|
const error = result.error || 'Upload failed';
|
|
await dbPut('results', { id, status: 'error', error, filename });
|
|
await broadcast({ type: 'UPLOAD_ERROR', id, error });
|
|
}
|
|
} catch (err) {
|
|
await dbPut('results', { id, status: 'error', error: err.message, filename });
|
|
await broadcast({ type: 'UPLOAD_ERROR', id, error: err.message });
|
|
// Keep the blob so user can retry
|
|
}
|
|
}
|
|
|
|
async function sendResults(client) {
|
|
const results = await dbGetAll('results');
|
|
client.postMessage({ type: 'PENDING_RESULTS', results });
|
|
}
|
|
|
|
// Background sync
|
|
self.addEventListener('sync', (event) => {
|
|
if (event.tag === 'upload-queue') {
|
|
event.waitUntil(processOfflineQueue());
|
|
}
|
|
});
|
|
|
|
async function processOfflineQueue() {
|
|
const db = await openDB();
|
|
const tx = db.transaction('offline-queue', 'readonly');
|
|
const req = tx.objectStore('offline-queue').getAll();
|
|
const items = await new Promise((resolve) => {
|
|
req.onsuccess = () => resolve(req.result);
|
|
});
|
|
for (const item of items) {
|
|
try {
|
|
const deleteTx = db.transaction('offline-queue', 'readwrite');
|
|
deleteTx.objectStore('offline-queue').delete(item.queuedAt);
|
|
} catch (error) {
|
|
console.error('Failed to process queued item:', error);
|
|
}
|
|
}
|
|
}
|