diff --git a/portal/static/portal/sw.js b/portal/static/portal/sw.js index 3c8840f..51a651f 100644 --- a/portal/static/portal/sw.js +++ b/portal/static/portal/sw.js @@ -1,17 +1,12 @@ -// rfiles.online Service Worker -const CACHE_NAME = 'rfiles-upload-v2'; -const DB_NAME = 'rfiles-upload'; -const DB_VERSION = 2; +// rfiles.online Service Worker — minimal cache-only +const CACHE_NAME = 'rfiles-v3'; -// 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)) @@ -19,226 +14,18 @@ self.addEventListener('install', (event) => { 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)) - ) + caches.keys().then((names) => + Promise.all(names.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; - } + if (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); - } - } -} diff --git a/portal/templates/portal/shared_space/base.html b/portal/templates/portal/shared_space/base.html index 7beb1d1..5faf136 100644 --- a/portal/templates/portal/shared_space/base.html +++ b/portal/templates/portal/shared_space/base.html @@ -161,18 +161,6 @@ {% block content %}{% endblock %} - {% block extra_js %}{% endblock %} diff --git a/portal/templates/portal/shared_space/home.html b/portal/templates/portal/shared_space/home.html index dfbb2df..ee7d3fe 100644 --- a/portal/templates/portal/shared_space/home.html +++ b/portal/templates/portal/shared_space/home.html @@ -246,15 +246,10 @@