Files
archy/neode-ui/mock/cloud-sources.js
T
yaya 18f9830800 Complete unified Cloud file apps and polish mobile UX
Connect installed Nextcloud and Immich accounts automatically after app setup, integrate their folders and indexed files into Cloud, and support private uploads, permission-checked deletion, and explicit sharing snapshots. Persist disconnect choices and retain advanced account setup.

Refine floating navigation, app cards, container icons, scroll fades, private-key dialogs, and federation graph request visibility across desktop and mobile.

Validation: cargo check and UI typecheck passed; 38 focused UI tests, 25 adapter tests including disposable Nextcloud/Immich integration, and persistent opt-out test passed. Chromium, Firefox, and WebKit desktop/mobile workflows passed. Requires UI/UX UAT approval from the maintainer before release.
2026-11-10 02:46:29 +00:00

140 lines
9.5 KiB
JavaScript

import crypto from 'node:crypto'
import { statSync, existsSync, mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
// Connected-account fixtures for the mock server only. Never reads credentials
// or contacts Nextcloud/Immich; each demo visitor has their own connection state.
export function createCloudSourcesMock(library) {
const files = library.filter(item => item.disk && existsSync(item.disk))
const identity = kind => ({
connection_id: kind === 'nextcloud' ? 'demo-nextcloud' : 'demo-immich',
kind, installation: `demo-${kind}-installation`, account_id: 'demo',
})
const stateFor = store => store.cloudSources ??= {
sources: ['nextcloud', 'immich'].map(kind => {
const source = identity(kind)
return { connection_id: source.connection_id, identity: source, error: null }
}),
handles: {}, shares: [], deleted: [], uploads: [],
}
const category = item => item.mime_type.startsWith('image/') ? 'Photos' : item.mime_type.startsWith('audio/') ? 'Music' : 'Documents'
const revision = value => crypto.createHash('sha256').update(value).digest('hex')
const albums = [{ id: 'landscapes', name: 'Landscapes & travels' }, { id: 'city', name: 'City nights' }]
const fileItem = (item, source) => ({
source, upstream_id: item.id,
locator: source.kind === 'nextcloud' ? (item.locator ?? [category(item), item.filename]) : null,
name: item.filename, mime_type: item.mime_type, size: statSync(item.disk).size,
revision: revision(item.id), directory: false,
preview: item.mime_type.startsWith('image/'),
preview_kind: item.mime_type.startsWith('image/') ? 'image' : null, download: true, deletable: true,
})
return {
rpc(method, params = {}, store) {
const state = stateFor(store)
if (method === 'content.list-mine') return { items: state.shares }
if (method === 'content.remove') { state.shares = state.shares.filter(item => item.id !== params.id); return {} }
if (method === 'content.configure') {
const item = state.shares.find(item => item.id === params.id)
if (!item) throw new Error('Share not found.')
Object.assign(item, { access: params.access === 'paid' ? { paid: { price_sats: params.price_sats, accepted: params.accepted_methods } } : params.access, availability: params.availability })
return { item }
}
if (method === 'cloud.sources.publish') {
const descriptor = params.source
const source = state.sources.find(item => item.connection_id === descriptor?.connection_id)
const file = [...files, ...state.uploads.filter(item => item.connection_id === descriptor?.connection_id)].find(item => item.id === descriptor?.upstream_id && !state.deleted.includes(descriptor?.connection_id + ':' + item.id))
if (!source || !file || descriptor.revision !== revision(file.id)) throw new Error('Refresh the source file before sharing.')
if (!params.access || !params.availability) throw new Error('Sharing policy required.')
const item = { id: crypto.randomUUID(), filename: params.filename, mime_type: file.mime_type, size_bytes: statSync(file.disk).size, access: params.access === 'paid' ? { paid: { price_sats: params.price_sats, accepted: params.accepted_methods } } : params.access, availability: params.availability }
state.shares = state.shares.filter(saved => saved.filename !== item.filename); state.shares.push(item)
return { item }
}
if (method === 'cloud.sources.discover') {
if (params.kind && ['nextcloud', 'immich'].includes(params.kind)) {
const source = identity(params.kind)
if (!state.sources.some(item => item.connection_id === source.connection_id)) state.sources.push({ connection_id: source.connection_id, identity: source, error: null })
}
return { sources: state.sources, setup_issues: [] }
}
if (method === 'cloud.sources.list') return { sources: state.sources }
if (method === 'cloud.sources.connect') {
const kind = params.credential?.kind
if (!['nextcloud', 'immich'].includes(kind)) throw new Error('Choose Nextcloud or Immich.')
const source = identity(kind)
if (!state.sources.some(item => item.connection_id === source.connection_id)) {
state.sources.push({ connection_id: source.connection_id, identity: source, error: null })
}
return { source }
}
if (method === 'cloud.sources.release-stream') {
delete state.handles[params.handle]
return {}
}
const source = state.sources.find(item => item.connection_id === params.connection_id)?.identity
if (!source) throw new Error('This demo connection is no longer available.')
if (method === 'cloud.sources.disconnect') {
state.sources = state.sources.filter(item => item.connection_id !== source.connection_id)
for (const [handle, entry] of Object.entries(state.handles)) {
if (entry.connection_id === source.connection_id) delete state.handles[handle]
}
return {}
}
const available = [...files, ...state.uploads.filter(item => item.connection_id === source.connection_id)].filter(item => !state.deleted.includes(source.connection_id + ':' + item.id))
if (method === 'cloud.sources.delete') {
const item = available.find(item => item.id === params.upstream_id)
if (!item || params.revision !== revision(item.id)) throw new Error('Refresh this file before deleting.')
state.deleted.push(source.connection_id + ':' + item.id)
for (const [handle, entry] of Object.entries(state.handles)) if (entry.connection_id === source.connection_id && entry.item.id === item.id) delete state.handles[handle]
return { deleted: true }
}
if (method === 'cloud.sources.browse') {
let items
if (source.kind === 'nextcloud') {
const folder = params.folder ?? []
items = folder.length === 0
? ['Documents', 'Photos', 'Music'].map(name => ({ source, upstream_id: `folder-${name}`, locator: [name], name, mime_type: null, size: null, revision: revision(name), directory: true, preview: false, download: false }))
: []
items.push(...available.filter(item => JSON.stringify((item.locator ?? [category(item), item.filename]).slice(0, -1)) === JSON.stringify(folder)).map(item => fileItem(item, source)))
} else {
items = available.filter(item => item.mime_type.startsWith('image/') || item.mime_type.startsWith('video/'))
.filter(item => !params.album_id || (params.album_id === 'city' ? item.id === 'photo-city-neon' : params.album_id === 'landscapes' && item.id !== 'photo-city-neon'))
.map(item => fileItem(item, source))
}
const page = Math.max(1, Number(params.page) || 1), pageSize = 6
return { upload: true, items: items.slice((page - 1) * pageSize, page * pageSize), next_page: items.length > page * pageSize ? page + 1 : null, listing_revision: revision(items.map(item => item.upstream_id).join(',')), albums: source.kind === 'immich' ? albums : null }
}
if (method === 'cloud.sources.prepare-stream') {
const item = available.find(item => item.id === params.upstream_id && (source.kind === 'nextcloud' || item.mime_type.startsWith('image/')))
if (!item || params.revision !== revision(item.id)) throw new Error('Refresh this demo library and try again.')
const handle = crypto.randomUUID()
state.handles[handle] = { item, connection_id: source.connection_id, download: !!params.download, expires: Date.now() + 5 * 60 * 1000 }
return { url: `/api/blob/cloud-source/${handle}` }
}
throw new Error('Unknown demo cloud source method.')
},
upload(req, res, store) {
try {
const request = JSON.parse(req.get('x-cloud-upload') || '{}'), state = stateFor(store)
const source = state.sources.find(item => item.connection_id === request.connection_id)?.identity
if (!source || !Array.isArray(request.folder) || typeof request.name !== 'string' || !request.name || /[\/\\\x00-\x1f]/.test(request.name)) throw new Error('Invalid upload')
if (source.kind === 'immich' && !/^(image|video)\//.test(req.get('content-type') || '')) throw new Error('Immich accepts photos and videos.')
const locator = [...request.folder, request.name]
const available = [...files, ...state.uploads.filter(item => item.connection_id === source.connection_id)].filter(item => !state.deleted.includes(source.connection_id + ':' + item.id))
if (source.kind === 'nextcloud' && available.some(item => JSON.stringify(item.locator ?? [category(item), item.filename]) === JSON.stringify(locator))) throw new Error('A file with this name already exists.')
const directory = mkdtempSync(join(tmpdir(), 'archy-cloud-demo-')), disk = join(directory, 'file')
writeFileSync(disk, req.body, { mode: 0o600 })
state.uploads.push({ id: crypto.randomUUID(), connection_id: source.connection_id, filename: request.name, mime_type: req.get('content-type') || 'application/octet-stream', disk, locator })
res.status(201).json({ uploaded: true })
} catch (error) { res.status(409).send(error.message) }
},
stream(req, res, store) {
const state = stateFor(store), entry = state.handles[req.params.handle]
if (!entry || entry.expires < Date.now()) return res.sendStatus(404)
res.set('Cache-Control', 'private, no-store')
if (entry.download) res.attachment(entry.item.filename)
res.type(entry.item.mime_type).sendFile(entry.item.disk)
},
}
}