From 311f6070dc8950a75133567a9eff724e975593b6 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:23:10 +0200 Subject: [PATCH 1/7] cloud: upload new owner files through encrypted core fragment storage --- .github/workflows/quality.yml | 2 + README.md | 7 + docs/ARCHITECTURE.md | 7 + docs/OWNER_UPLOADS.md | 114 ++++++++ docs/RECOVERY_WEB.md | 5 + patches/opencloud-web-owner-recovery.patch | 142 ++++++++-- scripts/cloud-serve.mjs | 23 +- scripts/private_file.py | 31 ++- scripts/recovery-web-assets.mjs | 11 +- scripts/smoke_owner_upload_ui.py | 271 +++++++++++++++++++ scripts/upload_lock.py | 25 ++ src/owner-uploads.mjs | 263 +++++++++++++++++++ src/private-catalog.mjs | 39 ++- src/private-dav-server.mjs | 41 ++- src/private-file.mjs | 20 +- src/recovery-web-metadata.mjs | 13 +- test/cloud-serve.test.mjs | 34 +++ test/owner-uploads.test.mjs | 291 +++++++++++++++++++++ test/recovery-web-assets.test.mjs | 23 ++ test/test_owner_upload_ui.py | 117 +++++++++ 20 files changed, 1422 insertions(+), 57 deletions(-) create mode 100644 docs/OWNER_UPLOADS.md create mode 100644 scripts/smoke_owner_upload_ui.py create mode 100644 scripts/upload_lock.py create mode 100644 src/owner-uploads.mjs create mode 100644 test/owner-uploads.test.mjs create mode 100644 test/test_owner_upload_ui.py diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index dd803d7..dc8cdf8 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -22,6 +22,8 @@ jobs: node-version: '24.19.0' - name: Check the actual DAV adapter against synthetic HTTP fixtures run: node --test test/*.test.mjs + - name: Check owner-upload driver boundaries without starting a browser + run: python3 -B test/test_owner_upload_ui.py - name: Check provenance and whitespace run: | python3 -m json.tool third_party/opencloud-source.json > /dev/null diff --git a/README.md b/README.md index 48e77ed..dece4c9 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,13 @@ trial also passes: original Files navigation and two verified downloads with the source and one provider offline. This is not a replacement account service, writable synchronization or second-device recovery. +The next executable slice adds an explicitly enabled **owner-private upload +space** beside the unchanged read-only imports. The connector now supports new +file upload, encryption, core fragment deposit, durable catalog publication and +restart/download; its targeted tests use real HTTP/GPG with an explicit storage +fixture. The updated original Files/Uppy connection still needs a new native +build and joined live-peer upload proof. [Upload setup and limits →](docs/OWNER_UPLOADS.md) + ## One core, private files OpenCloud supplies the familiar file experience; VOLPAROSSA supplies shared diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ac0e03b..8dced1f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -49,6 +49,13 @@ substitute for the live file-access path. [Encrypted catalog and source-off read service](OFFLINE_READ.md) +The next [owner-private upload slice](OWNER_UPLOADS.md) retains that immutable +selection and adds a separate, explicitly enrolled new-file space. Its existing +Uppy/DAV PUT path encrypts before core fragment deposit and publishes a durable +catalog entry only after confirmation. This is not permission to modify the +original account or an implementation of shared synchronization; native UI/peer +upload evidence is still open. + ## Backend functions that must not disappear OpenCloud storage and application services have separate responsibilities: diff --git a/docs/OWNER_UPLOADS.md b/docs/OWNER_UPLOADS.md new file mode 100644 index 0000000..4f2a87a --- /dev/null +++ b/docs/OWNER_UPLOADS.md @@ -0,0 +1,114 @@ +# Owner-private uploads while the original server is off + +This developing slice adds a **separate, explicitly enabled owner upload space** +to the existing recovery service. It does not turn imported read-only files into +writable OpenCloud accounts. It accepts new files only: no overwrites, folder +creation, rename, delete, sharing or synchronization back to the original server. + +The original OpenCloud Files **Files Upload** action and Uppy raw DAV `PUT` path +are reused. A local authenticated upload is privately staged, encrypted with the +existing GPG helper, then submitted to the shared core's fragment create/deposit +operations. The core still chooses fragment placement and uniform redundancy; +Cloud adds neither a copy-count option nor a second storage ledger. + +```text +Original Files Upload / authenticated DAV PUT + -> owner-private temporary file -> authenticated encrypted bundle + -> core fragment journal -> confirmed redundant deposit + -> durable catalog commit -> visible new file + -> later GET -> core fragment restore -> verified local decrypt -> download +``` + +## Explicit setup + +Keep the existing [read-service configuration](OFFLINE_READ.md), selected catalog +and owner token. Add an upload space whose name does not collide with an imported +space: + +```json +"ownerUploads": { + "directory": "/absolute/private/owner-uploads", + "space": "My uploads", + "storageConfig": "/absolute/private/upload-storage-template.json" +} +``` + +Create the upload directory mode **0700**, separate from the existing private work +directory. Keep the storage template mode **0600** in an owner-only directory. +It uses the existing [core storage configuration](PRIVATE_FILES.md), but **omit +`stateDirectory`**: each uploaded file receives an opaque owner journal directory. +Keep `copies` absent. Configure the existing core binary, protected control socket, +owner identity/passphrase file, fragment size/lifetime/deadline and **3–8 distinct +provider keys with their explicit grants**. Invalid grants or insufficient core +capacity remain visible failures, not admission to other peers. + +Start with the existing `cloud-serve.mjs --config …` command. `ownerUploads` is +absent by default and cannot enable cross-origin browser writes. The listener +remains loopback-only; every private operation needs the configured owner bearer. +Neither the configuration nor DAV input can supply executable hooks or arbitrary +local source/destination paths. Original Files requires a **new source build of +the updated pinned patch**; an older build report is correctly refused. + +The new UI label distinguishes owner-private uploads from imported read-only +files. Uploads use the existing Uppy engine, one file at a time; unsupported +folder/shortcut creation is hidden in this mode. The upload-enabled service and +UI use a bounded 30-minute default deadline. The original read-only service keeps +its two-minute default. Explicit service deadlines may still cancel an upload. + +## Publication, retry and recovery + +`PUT` returns **201 only after confirmed storage and durable catalog publication**. +The owner directory holds opaque object names, private recovery bundles, encrypted +single-file catalogs and core journals. File names and plaintext hashes remain +inside authenticated encryption, not filenames or public receipts. The new +file receipt is version 2 with `owner-upload-snapshot` provenance; old version-1 +DAV import receipts keep their original source/version checks. Relabeling an +upload as a DAV import fails authenticated restoration. + +After an incomplete deposit, the file is not listed and its journal/ciphertext +remain retained. An **explicit retry of the same name and exact file bytes** +resumes that existing operation; different bytes are refused. There is no automatic +fresh archive or origin fallback. A committed name cannot be overwritten, even +when a client requests overwrite. If a response is lost after commit, re-list the +space: a repeated `PUT` gets 412 rather than creating another stored copy. + +After service restart, committed catalogs become visible again. Every download +still restores through the core, verifies the encrypted object and authenticated +content, then removes temporary plaintext. Reads neither consume a backup nor +substitute local ciphertext. Retained grants, charges and recovery state must not +be deleted to make an incomplete operation look finished. + +Only one service may mutate an upload workspace at once, enforced by a supervised +owner lock. At most 256 upload objects are retained, including interrupted stages. +The existing `maxOpenBytes` bounds each encrypted file and concurrent restored +ciphertext; small encryption/metadata overhead is included. It is not a disk quota: +upload staging, ciphertext, restore staging and owner journals use additional local +space. Forced process termination may leave private staging; this slice does not +claim secure erasure or automatic abandoned-operation retirement. + +## Evidence and remaining scope + +Targeted tests exercise real HTTP, GPG encryption, encrypted catalogs, durable +publication, restart, repeated verified downloads after local ciphertext removal, +incomplete-deposit resumption, cancellation and unchanged imported permissions. +The explicitly staged, hash-verified OpenCloud Web8 SDK also passes actual `PUT`, +returned file-ID resolution, listing, verified read and overwrite refusal against +this service. Only the storage-provider boundary is an explicit in-memory +core-contract fixture. The updated patch applies to pinned OpenCloud Web8 source. **A newly built native +Files upload joined to real protected peers has not yet been demonstrated.** + +The prepared disposable-guest driver `scripts/smoke_owner_upload_ui.py` uses the +original Files file input and Uppy upload, then reloads and reauthenticates. Its +separate download phase opens a fresh browser and verifies two native downloads. +The core acceptance parent must supply the real service and independently prove +the source is off, encrypted fragment deposits, service restart, provider +withdrawal, charges and final retirement. The driver reports only closed UI and +browser-cleanup observations; its boundary tests are **not an executed native +upload or peer-storage result**. Ordinary read-only mode still hides the upload +button; the explicit owner-upload mode retains the original per-space upload +permission check, without granting writes to imported spaces. + +Owner keys/catalogs/journals still live on the owner's device. Cross-device recovery, +shared accounts, concurrent editors, automatic repair/renewal and general writable +synchronization remain separate work. Existing successful read-only UI/peer trials +do not prove this new upload path or full server-independent OpenCloud operation. diff --git a/docs/RECOVERY_WEB.md b/docs/RECOVERY_WEB.md index 6c47d52..9dd9c33 100644 --- a/docs/RECOVERY_WEB.md +++ b/docs/RECOVERY_WEB.md @@ -6,6 +6,11 @@ new OpenCloud account, an OIDC impersonation or a replacement for writable OpenCloud synchronization. The normal upstream authentication path stays intact when this mode is not selected. +The optional [owner-upload extension](OWNER_UPLOADS.md) adds a separate writable +space for new private files. Imported selections remain read only. The native +upload patch requires a new source build and joined upload proof; the successful +read-only trials below remain bound to their original sources and patch. + The browser receives only the owner's selected catalog. Storage peers still receive encrypted fragments, not filenames, private tokens or decryption keys. The recovery token remains in browser memory, not a URL or local/session storage. diff --git a/patches/opencloud-web-owner-recovery.patch b/patches/opencloud-web-owner-recovery.patch index aa59509..34b23ee 100644 --- a/patches/opencloud-web-owner-recovery.patch +++ b/patches/opencloud-web-owner-recovery.patch @@ -10,19 +10,19 @@ index abfb288..e605f4c 100644 +import { useConfigStore, useExtensionRegistry, useMessages, useUserStore } from '../piniaStores' import { Resource, SpaceResource } from '@opencloud-eu/web-client' import { useGetMatchingSpace } from '../spaces' - + @@ -17,6 +17,7 @@ export const useDownloadFile = (options?: DownloadFileOptions) => { const { $gettext } = useGettext() const userStore = useUserStore() const extensionRegistry = useExtensionRegistry() + const configStore = useConfigStore() - + const downloadFile = async ( space: SpaceResource = null, @@ -28,6 +29,19 @@ export const useDownloadFile = (options?: DownloadFileOptions) => { space = getMatchingSpace(file) } - + + if (configStore.options.volparossaOwnerRecovery === true) { + if (version || file.isFolder) throw new Error('Recovery supports selected current files only') + const response = await clientService.webdav.getFileContents( @@ -57,12 +57,13 @@ diff --git a/packages/web-pkg/src/composables/piniaStores/config/types.ts b/pack index 23c6c51..37be332 100644 --- a/packages/web-pkg/src/composables/piniaStores/config/types.ts +++ b/packages/web-pkg/src/composables/piniaStores/config/types.ts -@@ -26,6 +26,8 @@ const ScriptConfigSchema = z.object({ +@@ -26,6 +26,9 @@ const ScriptConfigSchema = z.object({ export type ScriptConfig = z.infer - + const OptionsConfigSchema = z.object({ + // Explicit owner-local recovery mode; never an upstream OIDC/account identity. + volparossaOwnerRecovery: z.boolean().optional(), ++ volparossaOwnerUploads: z.boolean().optional(), announcement: z .object({ bannerText: z.string().optional(), @@ -76,7 +77,7 @@ index ca69b9d..b48d6e2 100644 -