'use strict'; // Files, the sturdier parts: signing in again in place, a saved copy of each folder (instant, and still there offline), upload name clashes, // undo and the recycle bin, search in a library, zip downloads, share link options, and text files you can read and edit. // Uses sf, store, ME, jfetch, openSheet, closeSheet, confirmSheet, actionSheet, toast, esc, icon, fmtSize, fmtDate, joinP, libNames, libPerm, cfg from the other scripts. // ---- Seafile sign-in expired: ask again without leaving the page, then the original request carries on let reauthP = null; function sfReauth() { if (reauthP) return reauthP; reauthP = new Promise((ok, no) => { let settled = false; const finish = (good, err) => { if (settled) return; settled = true; reauthP = null; good ? ok() : (store.del('sf_token'), no(err || new Error('Session expired, sign in again'))); }; const el = openSheet('Sign in to Seafile again', 'Your Seafile sign-in expired. You will stay where you are.', `
`); el.querySelector('#sx').onclick = () => { closeSheet(); finish(false); }; el.querySelector('.scrim').addEventListener('click', () => finish(false)); el.querySelector('#rf').onsubmit = async e => { e.preventDefault(); const b = el.querySelector('#rb'); b.disabled = true; el.querySelector('#re').textContent = ''; try { await sf.login(el.querySelector('#ru').value.trim(), el.querySelector('#rp').value, el.querySelector('#ro').value.trim() || undefined); closeSheet(); finish(true); } catch (err) { b.disabled = false; if (err.otp) el.querySelector('#rotp').hidden = false; el.querySelector('#re').textContent = err.message; } }; setTimeout(() => el.querySelector('#ru').focus(), 60); }); return reauthP; } // ---- A saved copy of every folder you open (names, sizes, dates only): shown at once while the real list loads, and when Seafile cannot be reached const dcIndex = () => 'dc:index:' + (ME ? ME.id : ''); const dcKey = (repo, dir) => 'dc:' + (ME ? ME.id : '') + ':' + repo + ':' + dir; function dcGet(repo, dir) { try { const o = JSON.parse(localStorage.getItem(dcKey(repo, dir))); return o && Array.isArray(o.items) ? o : null; } catch { return null; } } function dcPut(repo, dir, items) { const key = dcKey(repo, dir), body = JSON.stringify({ t: Date.now(), items: items.slice(0, 1500).map(x => ({ id: x.id, name: x.name, type: x.type, size: x.size, mtime: x.mtime })) }); const touch = () => { const idx = (JSON.parse(localStorage.getItem(dcIndex()) || '[]')).filter(k => k !== key); idx.unshift(key); while (idx.length > 40) localStorage.removeItem(idx.pop()); localStorage.setItem(dcIndex(), JSON.stringify(idx)); }; try { localStorage.setItem(key, body); touch(); } catch { try { const idx = JSON.parse(localStorage.getItem(dcIndex()) || '[]'); for (const k of idx.splice(Math.ceil(idx.length / 2))) localStorage.removeItem(k); localStorage.setItem(dcIndex(), JSON.stringify(idx)); localStorage.setItem(key, body); touch(); } catch { /* storage is full: go without */ } } } const fScroll = new Map(); // where each folder was scrolled to document.addEventListener('scroll', e => { if (e.target && e.target.id === 'view' && /^#\/files\/./.test(location.hash)) fScroll.set(location.hash, e.target.scrollTop); }, true); // ---- Uploading a file whose name is already there async function resolveUploadConflicts(repo, dir, entries) { let existing; try { existing = new Set((await sf.req(`/api2/repos/${encodeURIComponent(repo)}/dir/?p=${encodeURIComponent(dir)}`)).map(x => x.name.toLowerCase())); } catch { return entries; } const clash = entries.filter(e => !e.rel && existing.has(e.file.name.toLowerCase())); // (files inside dropped folders keep Seafile's own renaming) if (!clash.length) return entries; const choice = await new Promise(ok => { const el = openSheet(clash.length === 1 ? 'Replace “' + clash[0].file.name + '”?' : clash.length + ' files already exist here', 'Replacing keeps the old version in the file\'s history.', `${clash.length > 1 ? `Loading…
'); el.querySelector('.sheet').classList.add('wide'); try { const list = (await trashList(repo, dir || '/', 100)).sort((a, b) => trashWhen(b.deleted_time) - trashWhen(a.deleted_time)); if (!document.body.contains(el)) return; const body = el.querySelector('.empty'); body.outerHTML = list.length ? `Showing the 100 most recent. Seafile empties its recycle bin after a while.
` : 'Nothing here. Deleted files and folders from this library show up here so you can get them back.
'; el.querySelector('#sx').onclick = closeSheet; el.querySelectorAll('[data-rs]').forEach(b => b.onclick = async () => { b.disabled = true; try { await trashRestore(repo, list[+b.dataset.rs]); b.textContent = 'Restored'; if (fv && fv.repo === repo) fv.reload(true); } catch (e) { b.disabled = false; toast(e.message); } }); } catch (e) { const b = el.querySelector('.empty'); if (b) b.textContent = e.message; } } // ---- Search a whole library (names, from the Seafile index when it has one, otherwise by walking the folders) function libSearch(repo) { textSheet('Search this library', 'File or folder name', 'Search', async term => { const el = openSheet('Results for “' + term + '”', libNames[repo] || '', 'Searching…
'); el.querySelector('.sheet').classList.add('wide'); try { const { results } = await searchFiles(term, [{ id: repo, name: libNames[repo] || '' }]); const mine = results.filter(r => r.repo_id === repo).slice(0, 60); if (!document.body.contains(el)) return; el.querySelector('.empty').outerHTML = `${mine.length ? `Nothing found.
'}`; el.querySelector('#sx').onclick = closeSheet; el.querySelectorAll('[data-r]').forEach(b => b.onclick = () => { const r = mine[+b.dataset.r], parent = (r.fullpath || '').replace(/\/[^/]*$/, '') || '/'; closeSheet(); if (r.is_dir) location.hash = '#/files/' + encodeURIComponent(repo) + (r.fullpath || '').split('/').filter(Boolean).map(s => '/' + encodeURIComponent(s)).join(''); else openFile(repo, r.fullpath, r.name, parent); }); } catch (e) { const b = el.querySelector('.empty'); if (b) b.textContent = e.message; } }); } // ---- Download several items, or a whole folder, as one zip (Seafile builds it; we wait for it) async function zipDownload(repo, dir, names) { try { const q = new URLSearchParams({ parent_dir: dir }); names.forEach(n => q.append('dirents', n)); const task = await sf.req(`/api/v2.1/repos/${encodeURIComponent(repo)}/zip-task/?${q}`), token = task.zip_token; if (!token) throw new Error('Seafile could not make a zip of that'); for (let i = 0; i < 300; i++) { const p = await sf.req(`/api/v2.1/query-zip-progress/?token=${encodeURIComponent(token)}`); const total = +p.total || 0, zipped = +p.zipped || 0; if (p.failed || p.failed_reason) throw new Error('Seafile could not make the zip' + (p.failed_reason ? ': ' + p.failed_reason : '')); if (total && zipped >= total) break; toast('Preparing the zip… ' + (total ? Math.round(100 * zipped / total) + '%' : '')); await sleep(i < 5 ? 500 : 1500); if (i === 299) throw new Error('The zip is taking too long. Try fewer files.'); } const a = document.createElement('a'); a.href = '/sf/seafhttp/zip/' + encodeURIComponent(token); a.download = ''; document.body.appendChild(a); a.click(); a.remove(); toast('Download started'); } catch (e) { toast(e.message); } } // ---- Share links with a password and an expiry, and the ones that already exist async function shareSheet(repo, path, isDir) { const p = isDir ? path.replace(/\/?$/, '/') : path, enc = encodeURIComponent, FORMH = { 'Content-Type': 'application/x-www-form-urlencoded' }; const el = openSheet('Share link', p.split('/').filter(Boolean).pop() || (libNames[repo] || ''), 'Loading…
'); const draw = async () => { let links = []; try { links = (await sf.req(`/api/v2.1/share-links/?repo_id=${enc(repo)}&path=${enc(p)}`)) || []; } catch (e) { if (!document.body.contains(el)) return; } if (!document.body.contains(el)) return; const host = el.querySelector('.empty') || el.querySelector('#shbody'); const html = `Loading…
'); el.querySelector('.sheet').classList.add('wide'); const dl = `${icon('dl')}Download`; try { const r = await fetch(url); if (!r.ok) throw new Error('Could not open the file (' + r.status + ')'); const len = +r.headers.get('content-length') || 0; if (len > 1024 * 1024) { await r.body.cancel(); el.querySelector('.empty').outerHTML = `This file is ${esc(fmtSize(len))}, too big to show here.
`; el.querySelector('#sx').onclick = closeSheet; return; } let text = await r.text(); if (text.length > 1024 * 1024) text = text.slice(0, 1024 * 1024); const view = () => { if (!document.body.contains(el)) return; const host = el.querySelector('#tpb') || el.querySelector('.empty'); host.outerHTML = `${esc(text)}