{"id":47872448,"url":"https://github.com/ernolf/files_sharing_raw","last_synced_at":"2026-04-04T00:56:55.337Z","repository":{"id":340832827,"uuid":"1166897444","full_name":"ernolf/files_sharing_raw","owner":"ernolf","description":"Nextcloud app — serves shared files as raw HTTP responses, no UI","archived":false,"fork":false,"pushed_at":"2026-03-12T02:58:12.000Z","size":7220,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-04T00:56:50.515Z","etag":null,"topics":["content-security-policy","contentsecuritypolicy","csp","file-sharing","filesharing","nextcloud","nextcloud-app","php","raw","rss","self-hosted","selfhosted"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ernolf.png","metadata":{"files":{"readme":"Readme.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"COPYING","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-25T18:19:34.000Z","updated_at":"2026-03-12T02:58:16.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ernolf/files_sharing_raw","commit_stats":null,"previous_names":["ernolf/files_sharing_raw"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ernolf/files_sharing_raw","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ernolf%2Ffiles_sharing_raw","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ernolf%2Ffiles_sharing_raw/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ernolf%2Ffiles_sharing_raw/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ernolf%2Ffiles_sharing_raw/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ernolf","download_url":"https://codeload.github.com/ernolf/files_sharing_raw/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ernolf%2Ffiles_sharing_raw/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31383636,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-03T23:20:52.058Z","status":"ssl_error","status_checked_at":"2026-04-03T23:20:51.675Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["content-security-policy","contentsecuritypolicy","csp","file-sharing","filesharing","nextcloud","nextcloud-app","php","raw","rss","self-hosted","selfhosted"],"created_at":"2026-04-04T00:56:54.647Z","updated_at":"2026-04-04T00:56:55.327Z","avatar_url":"https://github.com/ernolf.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `files_sharing_raw` — **Nextcloud raw file server**\n\n---\n---\n**`files_sharing_raw`** serves files **as-is** so you can link directly to the file itself (i.e. without any of Nextcloud's UI). This makes it easy to host static web pages, RSS feeds, images, or other assets and embed/link them elsewhere.\n\n**Design goals**\n\n* **Minimal**: deliver bytes, not UI.\n* **Fast**: keep server work low (good for assets).\n* **Quiet failures**: plain 404 Not found (text/plain) for invalid/missing public shares (no Nextcloud HTML error pages), ideal for asset fetches.\n* **Privacy-friendly**: **cookie-free responses** (best effort).\n* **Allowlist-gated:** public raw access is opt-in — only explicitly enabled public share tokens are served.\n* **Secure by default**: strict CSP with optional per-scope overrides. *)\n* **Streaming by default**: for normal `GET` (`200`) responses, the body is streamed whenever possible instead of loading the entire file into memory.\n\n*) For security and privacy, the content is served with a configurable [Content-Security-Policy][] (CSP) header, allowing different policies per share token, path, file extension, or MIME type (with a safe hardcoded fallback).\n\n\u003e [!NOTE]\n\u003e **`files_sharing_raw`** is the actively maintained successor to [`ernolf/raw`](https://github.com/ernolf/raw), which stopped working with Nextcloud 32 due to breaking API changes (`OCP\\Share` was removed). `files_sharing_raw` was rebuilt from the ground up to be compatible with Nextcloud 32 and later, while adding a proper database registry, a Files sidebar UI, per-share CSP overrides, webserver offload support, and more.  \n\u003e The longer app ID was chosen deliberately: from the outset, a [pull request to Nextcloud core](https://github.com/nextcloud/server/pull/58648) was planned to register `files_sharing_raw` in the `rootUrlApps` list — which is what enables the short, clean `/raw/{token}` URLs. That PR has been merged and the change ships with **Nextcloud 32.0.7+ and 33.0.1+**. On older patch releases a one-time manual patch is required (see [Activating root alias URLs](#activating-root-alias-urls-raw)).\n\n---\n\n## Table of contents\n\n* [Quickstart](#quickstart)\n\n* [URL forms](#url-forms)\n\n  * [Public shares](#public-shares)\n  * [Private user files](#private-user-files)\n  * [Root aliases (`/raw` and `/rss`)](#root-aliases-raw-and-rss)\n  * [Fallback URLs (without `rootUrlApps`)](#fallback-urls-without-rooturlapps)\n\n* [Enabling raw access](#enabling-raw-access)\n\n  * [Via the Files sidebar](#via-the-files-sidebar)\n  * [Via config: `allowed_raw_tokens` and wildcards](#via-config-allowed_raw_tokens-and-wildcards)\n\n    * [`allowed_raw_tokens`](#allowed_raw_tokens)\n    * [`allowed_raw_token_wildcards`](#allowed_raw_token_wildcards)\n\n  * [Usage with human-readable tokens](#usage-with-human-readable-tokens)\n\n* [Raw-only mode](#raw-only-mode)\n\n  * [Via the Files sidebar](#via-the-files-sidebar-1)\n  * [`raw_only_tokens`](#raw_only_tokens)\n  * [`raw_only_token_wildcards`](#raw_only_token_wildcards)\n  * [Example configuration](#example-configuration)\n\n* [Content Security Policy](#content-security-policy)\n\n  * [Matching priority](#matching-priority)\n  * [Per-share CSP (Files sidebar)](#per-share-csp-files-sidebar)\n  * [Config-based CSP (`raw_csp`)](#config-based-csp-raw_csp)\n\n    * [Policy formats accepted](#policy-formats-accepted)\n    * [Allowed directives](#allowed-directives)\n    * [Config examples](#config-examples)\n    * [Testing](#testing)\n\n* [Performance \u0026 caching](#performance--caching)\n\n  * [Cache-Control](#cache-control)\n  * [Webserver offload](#webserver-offload)\n\n    * [Offload debug header](#offload-debug-header)\n\n  * [HTTP behavior](#http-behavior)\n\n    * [Cookie-free responses](#cookie-free-responses)\n    * [ETags and Last-Modified](#etags-and-last-modified)\n    * [Directory handling (`index.html`)](#directory-handling-indexhtml)\n    * [HEAD requests](#head-requests)\n    * [Plain 404 for invalid public shares](#plain-404-for-invalid-public-shares)\n\n* [Notes \u0026 best practices](#notes--best-practices)\n\n  * [Keep `raw` settings in a dedicated config file](#keep-raw-settings-in-a-dedicated-config-file)\n\n* [Installation](#installation)\n\n  * [From the Nextcloud App Store](#from-the-nextcloud-app-store)\n  * [Manual installation (release tarball)](#manual-installation-release-tarball)\n  * [Developer setup (from source)](#developer-setup-from-source)\n  * [Activating root alias URLs (`/raw/`)](#activating-root-alias-urls-raw)\n  * [Migrating from the `raw` app](#migrating-from-the-raw-app)\n\n* [Updating](#updating)\n\n  * [Via the Nextcloud App Store](#via-the-nextcloud-app-store)\n  * [Manual update](#manual-update)\n\n---\n\n## Quickstart\n\n1. [Install/enable the app.](#installation)\n2. Create a **public share link** for a file or folder in Nextcloud.\n3. In the share's **Advanced settings** panel (Files sidebar), enable the **\"Enable raw link\"** toggle.\n4. Access the raw URL:\n   * `https://my-nextcloud/raw/\u003ctoken\u003e`\n\n   and for folders:\n   * `https://my-nextcloud/raw/\u003ctoken\u003e/\u003cpath/to/file\u003e`\n\n5. (Optional) Alternatively or additionally, allowlist tokens in [`config/{raw.}config.php`](#via-config-allowed_raw_tokens-and-wildcards) — useful for automation or custom link names.\n6. (Optional) Configure CSP policies via `raw_csp`.\n\n\u003e [!NOTE]\n\u003e The short `/raw/{token}` URLs require the `rootUrlApps` entry described in [Installation](#activating-root-alias-urls-raw). Without it, the app automatically falls back to longer `/apps/files_sharing_raw/{token}` URLs.\n\n---\n\n## URL forms\n\n### Public shares\n\nIf the share link is:\n\n```\nhttps://my-nextcloud/s/aBc123DeF456xyZ\n```\n\nthen this app will serve the raw file at:\n\n```\nhttps://my-nextcloud/raw/aBc123DeF456xyZ\n```\n\nIf the share is a folder, files within it are accessible as:\n\n```\nhttps://my-nextcloud/raw/aBc123DeF456xyZ/path/to/file\n```\n\n### Private user files\n\nA user can access their own private files (they must be logged in as that user). For example, a file named `test.html` in anansi's Documents folder would be available at:\n\n```\nhttps://my-nextcloud/raw/u/anansi/Documents/test.html\n```\n\nThe `/u/` prefix is **required** and cannot be omitted.\n\n\u003e [!NOTE]\n\u003e Private files are served without any additional token allowlist check — the logged-in user's identity is the authorization gate.\n\n### Root aliases (`/raw` and `/rss`)\n\nWhen the `rootUrlApps` entry is active (see [Activating root alias URLs](#activating-root-alias-urls-raw)), the app uses short root alias URLs:\n\n| Purpose | URL |\n|---|---|\n| Public share | `/raw/{token}` |\n| Public share + path | `/raw/{token}/{path}` |\n| Private file | `/raw/u/{userId}/{path}` |\n| RSS alias | `/rss` or `/rss/{path}` |\n\n\u003e [!NOTE]\n\u003e `/rss` and `/rss/{path}` are convenience shortcuts that internally behave exactly like `/raw/rss` and `/raw/rss/{path}`. The underlying share token is `rss` — it must be enabled like any other token (UI toggle or config allowlist).\n\n### Fallback URLs (without `rootUrlApps`)\n\nIf the `rootUrlApps` entry is not yet active (see [Installation](#activating-root-alias-urls-raw), the app falls back to longer URLs:\n\n| Purpose | URL |\n|---|---|\n| Public share | `/apps/files_sharing_raw/{token}` |\n| Public share + path | `/apps/files_sharing_raw/{token}/{path}` |\n| Private file | `/apps/files_sharing_raw/u/{userId}/{path}` |\n\nThe sidebar UI automatically shows the correct URL depending on whether root aliases are active. When root aliases are active, requests to fallback URLs are automatically **307-redirected** to the canonical `/raw/...` form.\n\n---\n\n## Enabling raw access\n\nPublic raw access is **opt-in**: a token must be explicitly allowed before the app will serve it. There are two ways to allow tokens — they can be combined freely, and the config allowlist always takes priority.\n\n### Via the Files sidebar\n\nOpen the share in the Files app (right sidebar → Advanced settings). Enable the **\"Enable raw link\"** toggle and click **Update share**. The share is immediately raw-accessible under `/raw/{token}`.\n\nThis toggle stores the enabled state per share in the database. The DB entry is automatically removed when the share is deleted.\n\nOnce the raw link is enabled, additional options become available via the **three-dot menu (⋯)** next to the raw link row:\n\n* **Raw only** — see [Raw-only mode](#raw-only-mode).\n* **Edit CSP** — see [Per-share CSP (Files sidebar)](#per-share-csp-files-sidebar).\n\n\u003e [!WARNING]\n\u003e **Password-protected shares are served without a password check.** Raw delivery is intentionally headless — there is no browser UI, no login form, and no place for a password prompt. As a result, `/raw/{token}` bypasses any password that is set on the Nextcloud share and delivers the content directly to anyone who has the raw URL.\n\u003e\n\u003e Only enable raw access on shares whose content you are comfortable making publicly accessible. Do not enable raw access on password-protected shares unless you explicitly intend the content to be reachable without the password.\n\n### Via config: `allowed_raw_tokens` and wildcards\n\nOne or both of the following arrays in [`config/{raw.}config.php`](#keep-raw-settings-in-a-dedicated-config-file) can be defined. **Config always takes priority over the DB registry.**\n\n#### `allowed_raw_tokens`\n\nAn array of explicitly allowed tokens. These tokens must exactly match the share token used in raw links.\n\n#### `allowed_raw_token_wildcards`\n\nAn array of wildcard patterns (`*`) matched against the share token. Wildcards are translated into regular expressions for dynamic validation.\n\n#### Example configuration\n\n```php\n\u003c?php\n$CONFIG = array (\n// -\n  'allowed_raw_tokens' =\u003e\n  array (\n    0 =\u003e 'scripts',\n    1 =\u003e 'aBc123DeF456xyZ',\n    2 =\u003e 'includes',\n    3 =\u003e 'html',\n  ),\n  'allowed_raw_token_wildcards' =\u003e\n  array (\n    0 =\u003e '*suffix',\n    1 =\u003e 'prefix*',\n    2 =\u003e 'prefix*suffix',\n    3 =\u003e '*infix*',\n    4 =\u003e 'prefix*infix*',\n  ),\n// -\n);\n```\n\nIn this configuration:\n\n* Tokens such as `scripts`, `aBc123DeF456xyZ`, `includes`, and `html` are explicitly allowed.\n* Wildcards match the share token and can be used as:\n\n  * suffix: `*_json` → `data_json`\n  * prefix: `nc-*` → `nc-assets`\n  * infix: `*holiday_img*` → `2026-02-10-holiday_img.jpg`, `2026-02-12-holiday_img.png`\n  * combined: `site-*_asset_*` → `site-example.com_asset_script.js`, `site-other.example.com_asset_style.css`\n\n### Usage with human-readable tokens\n\nGenerating human-readable tokens (instead of randomly generated ones) makes links more meaningful and easier to manage in both the UI toggle and the config allowlist.\n\nFor example: instead of a random token like `aBc123DeF456xyZ`, use a meaningful token such as `html`, `javascript` or `data_json` for shared directories, or apply prefixes/suffixes to enable wildcard matching.\n\n---\n\n## Raw-only mode\n\nWhen a share is flagged as **raw-only**, Nextcloud's standard share page (`/s/\u003ctoken\u003e`) returns **404 Not Found**. The file (or folder) is then only accessible via the raw URL (`/raw/\u003ctoken\u003e[/\u003cpath\u003e]`).\n\nThis is particularly useful for **shared folders**: without raw-only, anyone who has the share link can open the folder in Nextcloud's browser UI, navigate the directory tree, and discover all contained files. Enabling raw-only prevents any folder browsing entirely — only direct requests to known, explicit file paths via `/raw/` will succeed.\n\n\u003e [!IMPORTANT]\n\u003e Raw-only does **not** grant `/raw/` access on its own. The token must also be enabled for raw serving — either via the **\"Enable raw link\"** UI toggle or listed in `allowed_raw_tokens` / `allowed_raw_token_wildcards`. Raw-only only controls whether the standard `/s/\u003ctoken\u003e` share page is additionally blocked.\n\n### Via the Files sidebar\n\nIn the Files sidebar, once **\"Enable raw link\"** is active for a share, open the **three-dot menu (⋯)** in the raw link row and tick **\"Raw only\"**. From that moment on, `/s/\u003ctoken\u003e` returns 404 for that share — the file or folder is only reachable via `/raw/\u003ctoken\u003e`.\n\n### `raw_only_tokens`\n\nAn array of tokens for which the standard Nextcloud share page (`/s/\u003ctoken\u003e`) is blocked. Accepts the same format as `allowed_raw_tokens` (exact token strings).\n\n### `raw_only_token_wildcards`\n\nAn array of wildcard patterns (`*`) matched against the share token. Accepts the same format and wildcard syntax as `allowed_raw_token_wildcards`.\n\n### Example configuration\n\n```php\n\u003c?php\n$CONFIG = array (\n// -\n  // Grant /raw/ access to these tokens:\n  'allowed_raw_tokens' =\u003e\n  array (\n    0 =\u003e 'html',\n    1 =\u003e 'platform',\n  ),\n  'allowed_raw_token_wildcards' =\u003e\n  array (\n    0 =\u003e 'nc-*',\n  ),\n\n  // Additionally block /s/ for these tokens (raw-only):\n  'raw_only_tokens' =\u003e\n  array (\n    0 =\u003e 'html',\n    1 =\u003e 'platform',\n  ),\n  'raw_only_token_wildcards' =\u003e\n  array (\n    0 =\u003e 'nc-*',\n  ),\n// -\n);\n```\n\nIn this configuration `html`, `platform`, and any `nc-*` token are accessible via `/raw/` but their Nextcloud share pages (`/s/html`, `/s/platform`, `/s/nc-assets`, …) all return 404.\n\n\u003e [!NOTE]\n\u003e A token may appear in `raw_only_tokens` without being in `allowed_raw_tokens`. In that case, both `/s/\u003ctoken\u003e` and `/raw/\u003ctoken\u003e` return 404 — which is a valid but unusual setup (e.g. for tokens managed exclusively via the DB/UI toggle while still enforcing raw-only via config).\n\n---\n\n## Content Security Policy\n\n`files_sharing_raw` sends a `Content-Security-Policy` header with every raw response. Policies can be set **per share** via the Files sidebar or **globally** via the system config key `raw_csp` in `config/{raw.}config.php`. Both methods share a common evaluation order — the most specific matching rule wins.\n\n\u003e [!NOTE]\n\u003e If no CSP matches a request (no per-share override, no matching config rule), the app falls back to this safe, very restrictive default:\n\u003e ```\n\u003e \"sandbox; default-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src data:; font-src data:; frame-src data:\"\n\u003e ```\n\u003e This fallback is hardcoded inside the app (not in `config.php`).\n\n### Matching priority\n\nWhen deciding which CSP to send, the app evaluates selectors in this order:\n\n* `token` (config) — exact match for a public share token in `raw_csp['token']` (highest priority).\n* **Per-share CSP** — custom CSP stored via the UI or REST API (applies if the share is raw-enabled and a custom CSP is set; lower priority than config token, higher than path rules).\n* `path_prefix` — longest matching prefix. Supports absolute prefixes (starting with `/apps/files_sharing_raw`) and relative prefixes (matched against the path after the app prefix and token).\n* `path_contains` — substring match. Checked against both the full request path and the path after the app prefix, so public and private URLs are covered.\n* `extension` — file extension match (e.g. `html`, `json`).\n* `mimetype` — MIME type match (e.g. `text/html`, `application/json`).\n* hard-coded fallback (if nothing matches).\n\n\u003e [!NOTE]\n\u003e `token` (config) is the share token that appears in public URLs. Private user paths (`/raw/u/...`) do not carry a share token — `token` and per-share CSP overrides cannot match on private URLs.\n\n### Per-share CSP (Files sidebar)\n\n\u003e [!NOTE]\n\u003e **Edit CSP** is restricted to the `admin` group by default. To delegate this to a custom group, create the group, add the permitted users, then point the app to it:\n\u003e ```bash\n\u003e occ group:add raw_csp_allowed\n\u003e occ group:adduser raw_csp_allowed \u003cuid\u003e\n\u003e occ config:app:set files_sharing_raw csp_editor_group --value=\"raw_csp_allowed\"\n\u003e ```\n\u003e Users outside the configured group (`raw_csp_allowed` is just an example name) see no **Edit CSP** entry in the menu and cannot change a share's CSP via the API.\n\nIn the **three-dot menu (⋯)** next to the raw link row, **Edit CSP** opens an inline panel for setting a per-share Content-Security-Policy override. The value is stored in the database and takes effect immediately for all subsequent raw requests to this share. Its priority in the matching chain is: below the config `token` rule, above all path/extension/mimetype rules.\n\nThe panel contains a **preset dropdown** and an **editable text field**:\n\n| Preset | Stored CSP value | Suited for |\n|---|---|---|\n| Server default | *(empty — falls back to server-wide rules)* | General use; no override |\n| Sandbox (strict) | `sandbox; default-src 'none'; form-action 'none'` | Maximum isolation (no sub-resources at all) |\n| Images only | `default-src 'none'; img-src 'self' data: blob:; form-action 'none'` | Image files |\n| Documents (PDF / text) | `default-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; form-action 'none'` | PDFs, text documents |\n| Audio / Video | `default-src 'none'; media-src 'self' data: blob:; img-src 'self' data:; form-action 'none'` | Audio / video files |\n| Custom | *(keeps current text unchanged)* | Any hand-crafted policy |\n\nSelecting a preset fills the text field with the corresponding CSP string. The presets are **starting points, not fixed values** — the text field is always freely editable. Refine or extend the preset value before hitting Save, and the modified string is what gets stored.\n\nWhen the text field is edited manually and its content no longer matches any preset, the dropdown automatically switches to **Custom** — a visual indicator that the stored value is user-defined.\n\n\u003e [!NOTE]\n\u003e Setting the per-share CSP to **\"Server default\"** (empty string) removes the override — the server-wide `raw_csp` rules apply as usual.\n\n### Config-based CSP (`raw_csp`)\n\nThe `raw_csp` system config key lets admins define CSP rules for different paths, file extensions, MIME types, or share tokens. These rules apply globally and are evaluated after any per-share CSP override (see [matching priority](#matching-priority) above).\n\n#### Policy formats accepted\n\nA policy value for a selector may be one of:\n\n* *String* — a full, single-line CSP header value (passed through and sanitized).\n* *Indexed array* — list of directive strings; entries are joined with `;`.\n* *Associative array* (recommended) — `'directive' =\u003e sources`. `sources` may be a string (space separated) or an array of strings. The manager normalizes values, deduplicates and outputs a canonical single-line header.\n\n#### Allowed directives\n\nAllowed directive names are deliberately limited (to keep policies sane and safe):\n\n* Fetch directives:\n\n  * Fallbacks: [`default-src`][], [`script-src`][], [`style-src`][], [`child-src`][]\n  * Common: [`connect-src`][], [`font-src`][], [`frame-src`][], [`img-src`][], [`manifest-src`][], [`media-src`][], [`object-src`][], [`worker-src`][]\n* Document directives: [`base-uri`][], [`sandbox`][]\n* Navigation directives: [`form-action`][], [`frame-ancestors`][]\n* Other directives: [`upgrade-insecure-requests`][]\n\nUnknown/unsupported directives are ignored by the manager.\n\n#### Config examples\n\n1. Extension rule — make all `.html` permissive\n\n```php\n\u003c?php\n$CONFIG = array (\n// -\n  // Example: make all .html files more permissive\n  'raw_csp' =\u003e\n  array(\n    'extension' =\u003e\n    array(\n      'html' =\u003e\n      array(\n        'default-src' =\u003e [\"'self'\"],\n        'script-src'  =\u003e [\"'self'\", \"'unsafe-inline'\"],\n        'img-src'     =\u003e [\"'self'\", \"data:\"],\n        'media-src'   =\u003e [\"data:\"],\n        'style-src'   =\u003e [\"'self'\", \"'unsafe-inline'\"],\n        'font-src'    =\u003e [\"data:\"],\n        'frame-src'   =\u003e [\"'none'\"],\n      ),\n    ),\n  ),\n// -\n);\n```\n\n2. Path prefix — relative and absolute\n\n```php\n\u003c?php\n$CONFIG = array (\n// -\n  // Example: an absolute prefix and a relative prefix\n  'raw_csp' =\u003e\n  array(\n    'path_prefix' =\u003e\n    // absolute prefix: matches full URI starting at /apps/files_sharing_raw/...\n    array(\n      '/apps/files_sharing_raw/s/special-html/' =\u003e\n      array(\n        'default-src' =\u003e [\"'self'\"],\n        'script-src'  =\u003e [\"'self'\"],\n      ),\n      // relative prefix: matched against the path AFTER /apps/files_sharing_raw[/{token}] or /apps/files_sharing_raw/u/{user}/\n      'html/' =\u003e\n      array(\n        'default-src' =\u003e [\"'self'\"],\n        'script-src'  =\u003e [\"'self'\", \"'unsafe-inline'\"],\n        'img-src'     =\u003e [\"'self'\", \"data:\"],\n      ),\n    ),\n  ),\n// -\n);\n```\n\n3. Path contains — substring match (public + private)\n\n```php\n\u003c?php\n$CONFIG = array (\n// -\n  // Example: apply when '/html/' appears anywhere in the path\n  'raw_csp' =\u003e\n  array(\n    'path_contains' =\u003e\n    array(\n      '/html/' =\u003e\n      array(\n        'default-src' =\u003e [\"'self'\"],\n        'script-src'  =\u003e [\"'self'\"],\n        'img-src'     =\u003e [\"'self'\", \"data:\"],\n        'style-src'   =\u003e [\"'self'\", \"'unsafe-inline'\"],\n      ),\n    ),\n  ),\n// -\n);\n```\n\n4. Token — per share-token policy (optional)\n\n```php\n\u003c?php\n$CONFIG = array (\n// -\n  // Example: apply a policy only for the public share token 'abc123'\n  // This only applies when the public URL contains the token 'abc123'.\n  'raw_csp' =\u003e\n  array(\n    'token' =\u003e\n    array(\n      'abc123' =\u003e\n      array(\n        'default-src' =\u003e [\"'self'\"],\n        'img-src'     =\u003e [\"'self'\", \"data:\"],\n      ),\n    ),\n  ),\n// -\n);\n```\n\n5. Combined example\n\n```php\n\u003c?php\n$CONFIG = array (\n// -\n  'raw_csp' =\u003e\n  array(\n    'path_prefix' =\u003e\n    array(\n      'html/' =\u003e\n      array(\n        'default-src' =\u003e [\"'self'\"],\n        'script-src'  =\u003e [\"'self'\", \"'unsafe-inline'\"],\n      ),\n    ),\n    'path_contains' =\u003e\n    array(\n      '/public/static/' =\u003e\n      array(\n        'default-src' =\u003e [\"'self'\"],\n        'img-src'     =\u003e [\"'self'\", \"data:\"],\n      ),\n    ),\n    'extension' =\u003e\n    array(\n      'json' =\u003e\n      array(\n        'default-src' =\u003e [\"'none'\"],\n        'img-src'     =\u003e [\"data:\"],\n      ),\n    ),\n  ),\n// -\n);\n```\n\n**Important note about `path_contains` matching:**\n\nIf a pattern starts with a slash (for example '`/html/`'), the pattern is used verbatim as a substring search. '`/html/`' only matches when the exact sequence \"`/html/`\" appears in the request path (use this to target a folder segment precisely).\n\nIf a pattern does not start with a slash (for example '`html`'), the pattern is used as a plain substring (no leading slash is added). '`html`' therefore matches anywhere the characters `html` appear — e.g. `/some_html_text/`, `/some-html-data/`, `/htmlfile` and `/html/`.\n\nConsequence: `some-html-data` will match the pattern '`html`' but will not match '`/html/`'.\n\nRecommendation: use '`/folder/`' when you need to match a folder segment exactly; use a plain token like '`foo`' when you intentionally want a broad substring match.\n\nThe manager checks `path_contains` against both the full request path and the path portion after the app prefix, so public and private URLs are covered.\n\n#### Testing\n\nAfter you update [`config/{raw.}config.php`](#keep-raw-settings-in-a-dedicated-config-file) (or deploy changes), test with curl:\n\n- **Public share (root alias URL)**:\n  ```sh\n  curl -I 'https://your-instance.example/raw/html/calc.html'\n  ```\n\n- **Private user URL**:\n  ```sh\n  curl -I 'https://your-instance.example/raw/u/alice/Documents/html/calc.html'\n  ```\n\nInspect the `Content-Security-Policy:` response header. If you do not get the expected policy:\n\n* make sure the selector matches your URL form (token vs path vs extension),\n* check `nextcloud.log` for exceptions from `CspManager` or syntax errors in your config array,\n* remember that `token` only matches explicit share tokens (not private URLs).\n\n---\n\n## Performance \u0026 caching\n\n### Cache-Control\n\nPublic responses use a configurable Cache-Control header:\n\n- `raw_cache_public_max_age` (int seconds, default: 300)\n- `raw_cache_public_stale_while_revalidate` (int seconds, default: 30; 0 disables)\n- `raw_cache_public_stale_if_error` (int seconds, default: 86400; 0 disables)\n\nPrivate raw URLs (`/raw/u/...`) default to `private, max-age=0`.\n\nOptionally enforce no-store for private URLs:\n- `raw_cache_private_no_store` (bool, default: false)\n\n\u003e [!NOTE]\n\u003e `304 Not Modified` responses apply the same Cache-Control policy (public vs. private) as normal `200` responses, so caches behave consistently across conditional requests.\n\n### Webserver offload\n\nFor large files you can optionally let the webserver send the file body (PHP returns early):\n\n- `raw_sendfile_backend` (off|apache|nginx default: off)\n- `raw_sendfile_allow_private` (bool, default: false) *)\n- `raw_sendfile_min_size_mb` (int, default: 0) **)\n- `raw_sendfile_nginx_prefix` (string, default: /_raw_sendfile)\n\n\u003e [!WARNING]\n\u003e **Webserver offload requires correct webserver configuration. The app enforces its own path restriction (files must be inside Nextcloud's `datadirectory`), but the webserver-side configuration is entirely the administrator's responsibility.**\n\u003e - **Nginx — `internal;` is mandatory.** Without it, the `/_raw_sendfile/` location is reachable directly from the internet, bypassing all PHP authorization checks. Any file inside the Nextcloud data directory would be accessible to anyone without authentication. Always verify that the location block carries the `internal;` directive before enabling offload.\n\u003e - **Apache — `XSendFilePath` is recommended defense-in-depth.** The app only ever sends paths within the datadirectory via `X-Sendfile`, so a missing `XSendFilePath` does not open an independent attack path. However, configuring it explicitly limits the blast radius should any unexpected behavior occur in the module itself.\n\n\u003e [!NOTE]\n\u003e *) By default, offload is disabled for private raw URLs (`/raw/u/...`) to keep authenticated endpoints conservative by default. Enable `raw_sendfile_allow_private` to allow webserver offload for private raw responses too.\n\n\u003e [!NOTE]\n\u003e **) If `raw_sendfile_min_size_mb` is set, offload is only attempted when the file size is known and meets the threshold. If the size cannot be determined (e.g. certain storage backends), offload is skipped.\n\n**Prerequisites** (webserver configuration required):\n\n- **Apache**:\n  - Requires `mod_xsendfile` *) (or an equivalent X-Sendfile implementation) to be installed and enabled.\n  - Enable it and configure the allowed path(s) to include your Nextcloud datadirectory:\n    ```apacheconf\n    XSendFile On\n    # Use the *real* Nextcloud datadirectory from `config/config.php` -\u003e 'datadirectory'\n    XSendFilePath /path/to/nextcloud/data\n    ```\n\u003e [!NOTE]\n\u003e *) Module naming varies by distribution; the key requirement is that your Apache build supports `X-Sendfile` and that the module is enabled for the vhost serving Nextcloud.\n\n- **Nginx**:\n  - Uses `X-Accel-Redirect` (built into nginx, no extra module needed).\n  - Requires an `internal` location that maps the configured prefix (default `/_raw_sendfile`) to Nextcloud's data directory via `alias`.\n  - Example (must match your `raw_sendfile_nginx_prefix` and Nextcloud datadirectory):\n    ```nginx\n    location /_raw_sendfile/ {\n        internal;\n        alias /path/to/nextcloud/data/;\n    }\n    ```\n\u003e [!TIP]\n\u003e The app builds the Nginx `X-Accel-Redirect` target by stripping the resolved (`realpath`) Nextcloud `datadirectory` prefix from the local file path. Ensure your Nginx `alias` uses the same resolved datadirectory path (and includes a trailing `/`). If `datadirectory` is a symlink but Nginx points to the symlink path (or vice versa), the mapping can mismatch and offload will be skipped.\n\n\u003e [!WARNING]\n\u003e **Nginx offload bypasses PHP-set `Content-Security-Policy` headers.** When `X-Accel-Redirect` is used, nginx serves the file body directly — it does not forward custom response headers set by PHP, including `Content-Security-Policy`. As a result, offloaded responses carry **no CSP header at all**.\n\u003e\n\u003e **Recommendation:** set `raw_sendfile_min_size_mb` to a meaningful threshold (e.g. `10`) so that small files — HTML pages, text files, RSS feeds, images — where a CSP is security-relevant are served by PHP (with full CSP enforcement), while only large binary files — videos, archives, large data blobs — where a CSP is not meaningful are offloaded to nginx.\n\u003e\n\u003e If you require CSP on all responses regardless of file size, do not use `raw_sendfile_backend = 'nginx'`.\n\nExample offload configuration in [`config/{raw.}config.php`](#keep-raw-settings-in-a-dedicated-config-file) (for apache2):\n\n```php\n\u003c?php\n$CONFIG = array (\n// -\n  // Private raw caching\n  'raw_cache_private_no_store' =\u003e false, // true = Never save in browser\n\n  // apache2\n  'raw_sendfile_backend' =\u003e 'apache',\n/*\n  // nginx\n  'raw_sendfile_backend' =\u003e 'nginx',\n  'raw_sendfile_nginx_prefix' =\u003e '/_raw_sendfile',\n*/\n\n  // allow offload also for /raw/u/... (default false)\n  'raw_sendfile_allow_private' =\u003e false,\n\n  // only offload for files \u003e= X MB (default 0 = no threshold)\n  'raw_sendfile_min_size_mb' =\u003e 5,\n// -\n);\n```\n\n#### Offload debug header\n\nTo debug whether offload/streaming was used, send this request header:\n\n- `X-Raw-Offload-Debug: 1`\n\nThe response may include:\n- `X-Raw-Offload: \u003cstatus\u003e; reason=\u003creason\u003e`\n\n\u003e [!NOTE]\n\u003e When offload is active and actually used, the response may include an `X-Raw-Offload` header (e.g. `apache-xsendfile` / `nginx-x-accel`) even without debug enabled.  \n\u003e If you send `X-Raw-Offload-Debug: 1`, the app adds `reason=...` and can also emit a \"not offloaded\" reason, which is useful to validate your config and thresholds.\n\n### HTTP behavior\n\n#### Cookie-free responses\n\n`files_sharing_raw` intentionally aims to be **cookie-free**. It will best-effort prevent `Set-Cookie` from being emitted for raw responses (e.g. by closing any active session, disabling session cookies for the remainder of the request, and removing already queued `Set-Cookie` headers).\n\nThis keeps endpoints \"naked\" for asset serving and reduces overhead. (Best effort: a reverse proxy could still add cookies afterwards.)\n\n#### ETags and Last-Modified\n\n`files_sharing_raw` supports conditional requests (cache validation) using ETags together with the `If-None-Match` header and also supports `Last-Modified` / `If-Modified-Since` semantics.\n\n\u003e [!NOTE]\n\u003e The app prefers \"fast\" validators (mtime + size) for ETag generation and only falls back to a content hash when needed.\n\n* **ETag / If-None-Match**: The server sends an `ETag` header identifying the current representation of the file. If the client sends `If-None-Match: \"\u003cETag\u003e\"` and the value matches, the server responds with `304 Not Modified` and no response body. The wildcard `If-None-Match: *` is also supported.\n* **Last-Modified / If-Modified-Since**: When the server can read file modification time (mtime) it sets a `Last-Modified` header. The server will honor `If-Modified-Since` when `If-None-Match` is not present. If the client date is equal to or newer than the file mtime, the server responds with `304 Not Modified`.\n* **Unix timestamp convenience**: For convenience, `If-Modified-Since` accepts either an RFC-style HTTP-date (recommended) **or** a plain Unix timestamp (seconds). The server will trim optional quotes.\n\nExamples:\n\n- Get file and see headers + body (returns ETag and Last-Modified):\n\n   ```bash\n   curl -i 'https://your.nextcloud/raw/.../file.ext'\n   ```\n\n- Conditional GET using ETag (replace `\u003cETag\u003e` with the ETag value returned by the server):\n\n   ```bash\n   curl -i -H 'If-None-Match: \"\u003cETag\u003e\"' 'https://your.nextcloud/raw/.../file.ext'\n   ```\n\n- Conditional GET using HTTP-date:\n\n   ```bash\n   curl -i -H 'If-Modified-Since: \"Sun, 25 May 2025 21:40:02 GMT\"' 'https://your.nextcloud/raw/.../file.ext'\n   ```\n\n- Conditional GET using Unix timestamp (convenience):\n\n   ```bash\n   curl -i -H 'If-Modified-Since: \"1748209203\"' 'https://your.nextcloud/raw/.../file.ext'\n   ```\n\n- The wildcard `If-None-Match: *` is also supported (returns 304 if the resource exists):\n\n   ```bash\n   curl -i -H 'If-None-Match: *' 'https://your.nextcloud/raw/.../file.ext'\n   ```\n\n#### Directory handling (`index.html`)\n\nIf the requested node is a directory, the app attempts to serve `index.html` from that directory.\n\n#### HEAD requests\n\n`files_sharing_raw` supports `HEAD` requests (headers only, no response body).\n\n#### Plain 404 for invalid public shares\n\nFor public endpoints, the app returns a minimal `text/plain` **404 Not found** response for disallowed/unknown tokens, missing shares, and missing paths. This avoids rendering large HTML error pages and keeps endpoints lightweight.\n\n---\n\n## Notes \u0026 best practices\n\n* **Do not enable raw access on password-protected shares** unless you explicitly intend the content to be publicly reachable without a password. Raw delivery is headless by design — there is no password prompt, so the share password is bypassed entirely.\n* **Review and update `allowed_raw_tokens` and `allowed_raw_token_wildcards` periodically** to align with your security requirements. Alternatively, manage access via the Files sidebar UI per share.\n* **Validate CSP rules and token configurations in a test environment** before applying them in production.\n* **Prefer `extension` or `path-based` matching for predictable results**. `path_contains` with `'/html/'` is usually the safest way to target a folder named `html`.\n* **Avoid `script-src 'unsafe-inline'` unless absolutely necessary**. When you need inline scripts, prefer nonces or restrictive policies.\n* **Keep the `token` selector (in `raw_csp`) only if you want per-share (per-token) policies from config**. If you do not need that granularity, it is safe to remove `token` and rely on path/extension/mimetype rules. Per-share CSP can also be set via the UI (stored in DB).\n* The manager normalizes directives and removes duplicates; unknown directives are ignored (no crash but check logs).\n\n### Keep `raw` settings in a dedicated config file\n\n* Nextcloud can load settings from multiple files in `config/`. For `files_sharing_raw`, it's recommended to keep all `raw`-related directives like `allowed_raw_tokens`, `allowed_raw_token_wildcards`, `raw_csp` etc. in a dedicated **`config/raw.config.php`** (any `*.config.php` in `config/` is loaded and merged alongside `config.php`).\n* This keeps raw-specific security settings isolated, avoids accidental clutter in `config.php`, and plays nicely with config management.\n* **Gotcha:** Nextcloud can consolidate config values into `config/config.php`. Don't rely on `occ` for `raw` settings if `config/raw.config.php` exists — `raw.config.php` has precedence and will override later.\n\n---\n\n## Installation\n\n### From the Nextcloud App Store\n\nThe easiest way to install this app is via the Nextcloud App Store:\n\n1. Log into Nextcloud as admin.\n2. Go to **Apps** → search for **Raw Fileserver** → Install.\n\n### Manual installation (release tarball)\n\n1. Download the latest release tarball (`files_sharing_raw.tar.gz`) from the\n   [GitHub Releases page](https://github.com/ernolf/files_sharing_raw/releases).\n2. Extract it into your Nextcloud `/apps` (or `/custom_apps`) folder:\n   ```bash\n   tar -xzf files_sharing_raw.tar.gz -C /path/to/nextcloud/apps/\n   ```\n3. Enable the app:\n   ```bash\n   occ app:enable files_sharing_raw\n   ```\n   or log into Nextcloud as admin and enable it in the Apps list.\n\n### Developer setup (from source)\n\n1. Clone the repository into your Nextcloud `/apps` (or `/custom_apps`) folder:\n   ```bash\n   git clone https://github.com/ernolf/files_sharing_raw\n   cd files_sharing_raw\n   ```\n2. Install frontend dependencies and build the JS bundle:\n   ```bash\n   npm ci\n   npm run build\n   ```\n3. Enable the app:\n   ```bash\n   occ app:enable files_sharing_raw\n   ```\n\n### Activating root alias URLs (`/raw/`)\n\nTo use the short `/raw/{token}` URLs instead of the longer `/apps/files_sharing_raw/{token}` fallback, `files_sharing_raw` must be registered in Nextcloud core's `rootUrlApps` list. The [pull request](https://github.com/nextcloud/server/pull/58648) for this has been merged and ships with **Nextcloud 32.0.7+ and 33.0.1+**. On those versions no manual action is needed.\n\nIf you are running an older patch release (below 32.0.7 or 33.0.1), the entry must be added once manually.\n\nThe change is a single line in `lib/private/AppFramework/Routing/RouteParser.php`:\n\n```php\nprivate const rootUrlApps = [\n    'cloud_federation_api',\n    'core',\n    'files_sharing_raw',   // ← add this line\n    'files_sharing',\n    // ...\n];\n```\n\nA patch script is included in the app directory — just make it executable and run it:\n\n```bash\nchmod +x patch-route-parser.sh \u0026\u0026 ./patch-route-parser.sh\n```\n\nThe script is idempotent — it finds `RouteParser.php` automatically and is safe to run multiple times.\n\n\u003e [!NOTE]\n\u003e **Nextcloud AIO / Docker users:** see the [AIO patch guide](Readme-aio.md) for step-by-step instructions on how to apply the patch inside the container.\n\n\u003e [!NOTE]\n\u003e On **Nextcloud 32.0.7+ and 33.0.1+** this manual step is not needed — the entry ships with the core update.\n\u003e On older patch releases, the patch is a one-time action and does not need to be repeated after subsequent Nextcloud updates (those updates will already carry the entry).\n\u003e\n\u003e Without this entry the app still works — it simply uses the longer fallback URLs.\n\n### Migrating from the `raw` app\n\nIf you previously used the original `raw` app (which stopped working with Nextcloud 32):\n\n1. Disable `raw`: `occ app:disable raw`\n2. Install and enable `files_sharing_raw` (see above).\n\nAll `raw_*` config keys (`allowed_raw_tokens`, `raw_csp`, etc.) are reused automatically — no data migration is needed.\n\n## Updating\n\n### Via the Nextcloud App Store\n\nUpdate directly from the Apps page in the Nextcloud admin UI — no manual steps needed.\n\n### Manual update\n\n1. Disable the app:\n   ```bash\n   occ app:disable files_sharing_raw\n   ```\n2. Update the app files — either via\n   - release tarball (see [Manual installation](#manual-installation-release-tarball) above)\n\n   or via  \n   - `git pull` + `npm ci \u0026\u0026 npm run build` (see [Developer setup (from source)](#developer-setup-from-source))\n\n   in the app directory.\n3. Enable the app again:\n   ```bash\n   occ app:enable files_sharing_raw\n   ```\n\n---\n\n[Content-Security-Policy]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy\n[`child-src`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/child-src\n[`connect-src`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/connect-src\n[`default-src`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/default-src\n[`font-src`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/font-src\n[`frame-src`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-src\n[`img-src`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/img-src\n[`manifest-src`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/manifest-src\n[`media-src`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/media-src\n[`object-src`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/object-src\n[`script-src`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src\n[`style-src`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/style-src\n[`worker-src`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/worker-src\n[`base-uri`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/base-uri\n[`sandbox`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/sandbox\n[`form-action`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/form-action\n[`frame-ancestors`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors\n[`upgrade-insecure-requests`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/upgrade-insecure-requests\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fernolf%2Ffiles_sharing_raw","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fernolf%2Ffiles_sharing_raw","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fernolf%2Ffiles_sharing_raw/lists"}