Skip to content
Back to skills

Nocobase App Plugin File

ASecurity

Add file collections, uploads, downloads, DOCX/XLSX/PPTX previews, business attachments, and editable UI Library file components to a NocoBase 3 App using the public File Repository services.

  • 24,837 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added November 5, 2026
developmentrustgobashsqlreactnodeapidatabase

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned November 5, 2026

npx -y skills add nocobase/nocobase --skill nocobase-app-plugin-file --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Nocobase App Plugin File?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Nocobase App Plugin File
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nocobase-nocobase-app-plugin-file/badge)](https://www.skillsdirectory.com/skills/nocobase-nocobase-app-plugin-file)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
---
name: nocobase-app-plugin-file
description: Add file collections, uploads, downloads, DOCX/XLSX/PPTX previews, business attachments, and editable UI Library file components to a NocoBase 3 App using the public File Repository services.
---

# Add files to an App

Use `@nocobase/app-plugin-file/server` and `@nocobase/app-plugin-file/client`. The plugin provides Repository managers, service tokens, and route helpers. The UI Library provides `@nocobase/file-component-ui`. The App or business plugin owns collections, migrations, Drive configuration, resource routes, permissions, and pages.

Inspect the App's existing registrations, migrations, disks, and resources first. Register the Server default export and the Client default factory before consumers, using the App's plugin lifecycle commands. The core does not create collections or routes.

## Find and reuse the preview UI first

Inspect `client/extensions/nocobase-file-component-ui/` before writing upload or preview components. Current Default templates preinstall this application-owned UI Library source and its client dependencies; older applications and other templates may not. Import from its `index.ts`, not from plugin-internal paths. Registering the plugin supplies services and translations; it does not automatically install or upgrade the copied UI.

| Format                                               | UI Library preview                | Content access                                                                 |
| ---------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------ |
| DOCX / XLSX / PPTX                                   | Local `@silurus/ooxml` viewer     | Same-origin session credentials; no public URL or third-party service required |
| Raster image / PDF / text / Markdown / audio / video | Built-in preview components       | See the content access rules below                                             |
| Legacy DOC / XLS / PPT and OpenDocument              | Office Online fallback            | Internet-accessible absolute URL; cannot use the App session                   |
| Unsupported or unsafe active content                 | Explanation and optional download | Download still requires permission                                             |

To show a record's files as thumbnails and chips, or take uploads beside a form or a comment, install the UI Library's `attachment-list` component (`yes n | pnpm exec shadcn add @nocobase/attachment-list`; its example is `@nocobase/attachment-list-demo`), map file records to `AttachmentFile`, and open `FilePreviewDialog` from its `onPreview`. For custom attachment lists, uploads or business associations, reuse `FilePreviewDialog` with file records and `contentUrl`; it requires no repository prop. Extend the existing UI or add the necessary content adapter rather than reimplementing format detection and dropping supported formats. A DOCX download fallback indicates a request/rendering failure or an outdated/custom component, not that the plugin lacks DOCX support. Report the actual failure and preserve its evidence.

If the installed package, synchronized Skill and copied UI disagree, inspect the resolved plugin version, run `pnpm nocobase skills sync` to refresh guidance, and reconcile the UI source separately. Skills synchronization does not upgrade UI Library copies.

## Work incrementally

- Use standard File Repository routes, the Client File Repository and UI Library components for new attachment features. Save associations through the existing business API, whether standard CRUD or custom routes; do not migrate working business routes just to add files.
- Extend existing services, middleware, response adapters or installed UI components where needed. A necessary compatibility handler should delegate to File Repository; do not rebuild upload, storage and metadata handling with Drive or replace the whole CRUD stack.
- Inspect the App's registration, collection, disk and business save path together. Reuse what works; do not ask the user to choose an implementation or repeatedly explore source when the documented contract suffices.
- Verify one small upload → metadata query → content download before building UI. Then wire the returned IDs into the existing save/detail flow. Retain uploaded IDs if the business save fails, and run the affected checks after edits stabilize.

The following business example uses collection `invoice_files`, resource `invoiceAttachments`, connection `main`, and disk `local`. Adapt these to the App.

## Collection

Create an App-owned, self-contained migration under `database/main/migrations/` with a unique name matching its filename. Replace `main` with the target connection when needed:

```ts
import { defineMigration, type MigrationDefinition } from '@nocobase/db';

const migration: MigrationDefinition = defineMigration({
  name: '202609080001_create_invoice_files',
  async up({ builder }) {
    await builder.createCollection('invoice_files', (collection) => {
      collection.uuid('id').primary().notNull();
      collection.string('disk', { length: 255 }).notNull();
      collection.text('key').notNull();
      collection.text('filename').notNull();
      collection.string('ext', { length: 32 }).notNull();
      collection.string('mimeType', { length: 255 }).notNull();
      collection.bigInt('size').notNull();
      collection.datetime('createdAt').notNull();
      collection.datetime('updatedAt').notNull();
    });
  },
  async down({ builder }) {
    await builder.dropCollection('invoice_files');
  },
});
export default migration;
```

The fields are fixed; mapping is unsupported. The primary key must accept a 36-character UUID, string fields accept string/char/text, size accepts integer/bigInt, and timestamps accept datetime/datetimeTz. Apply with the App's migration command. Never import a live collection schema from a migration.

Runtime code that selects or checks the full column set imports `FILE_COLUMNS` (and its element type `FileColumn`) from `@nocobase/app-plugin-file/server` instead of copying the list, for example `select: (s) => s.fields(...FILE_COLUMNS)`. A migration still spells its columns out as above, because importing the list would let a later release change an already applied migration.

Upload generates the ID and storage key, normalizes the filename/extension, validates stored size, and supplies timestamps. Extra required columns need defaults because upload accepts no business values. `contentUrl` is derived, never persisted. Save returned file IDs through an App-owned relation or link table when submitting the business form; upload and form submission are separate commits.

## API routes

Declare in the App or business plugin's Server routes module and merge both returned contributions into its existing routes:

```ts
import { defineFileRepositoryApiRoutes } from '@nocobase/app-plugin-file/server';

const fileRoutes: ReturnType<typeof defineFileRepositoryApiRoutes> =
  defineFileRepositoryApiRoutes({
    repositories: [
      {
        name: 'invoiceAttachments',
        collection: 'invoice_files',
        connection: 'main',
        disk: 'local',
        accessPath: '/uploads/invoices',
        accessMode: 'stream',
        policy: {
          read: {
            scope: true,
            fields: [
              'id',
              'filename',
              'ext',
              'mimeType',
              'size',
              'createdAt',
              'updatedAt',
            ],
          },
          create: { scope: true },
          update: false,
          delete: { scope: true },
        },
        actions: {
          findMany: { maxLimit: 100 },
          findOne: {},
          deleteOne: {},
          uploadOne: { maxSize: 5 * 1024 * 1024 },
          uploadMany: { maxSize: 20 * 1024 * 1024 },
        },
      },
    ],
  });
export default fileRoutes;
```

`name` is the Client resource; `collection` defaults to name, `connection` to the database default, and `accessPath` to `/uploads/<name>`. Disk is required. Content paths contain static alphanumeric, underscore or hyphen segments, start with `/`, and have no trailing slash.

Only declared POST `/api/<name>/<action>` operations are exposed; `name` is a camelCase path segment such as `invoiceAttachments`. Ordinary actions are `findMany/findOne/count/exists/aggregate/groupBy/createOne/updateOne/deleteOne`. There are no createMany/updateMany/deleteMany HTTP actions.

Every exposed action appears in the application's API document at `/api/swagger/docs` (JSON at `/api/swagger`), under the exposure's name as its tag with operationIds such as `invoiceAttachmentsFindMany` and `invoiceAttachmentsUploadOne`. The upload operations are documented as `multipart/form-data` with their `maxSize`, `201` result and `400`/`403`/`413`/`415` errors; read it there rather than guessing a request shape. The content route under `accessPath` is outside `/api` and is not in the document.

`policy` is required and governs every action of the exposure, uploads included. It is a Repository Policy: `read`, `create`, `update` and `delete`, each `true`, `false`, or a rule node. A node that is `false` refuses that operation; a node with no `fields` accepts no caller-supplied field, which is what a metadata `createOne` needs before it will accept anything. Pass a function of the principal — `policy: (principal) => ({ ... })` — together with a `principal(context)` resolver to scope rows to the caller; without a principal the request is refused with 403.

An upload supplies no caller fields, so it binds a Policy derived from this one: the `create` scope and defaults are inherited and the field allowlist is replaced by the file columns. `create: false` therefore forbids uploading as well as creating metadata — decided, with the principal, before the body limit, the content type or the multipart body is read, so a refused upload answers `403 WRITE_FORBIDDEN` (domain `app`) and stores nothing — and a `create.defaults` of `{ ownerId }` is stamped onto uploaded rows — which is what keeps an uploaded file inside the same scope `findMany` reads. The content route under `accessPath` is the exception: it is public and this Policy does not reach it.

Content is GET `<accessPath>/<uuid>.<ext>` outside `/api`, omitting the dot when extensionless. Stream returns full bytes as an attachment; redirect returns a public storage URL or a five-minute signed URL. A disk without URL support requires stream mode; there is no automatic fallback.

These are public routes. For restricted files, register App-owned authentication and authorization on the paths each contribution owns before mounting it — `/<name>/<action>` per exposed API action, `<accessPath>/*` for content. Never `router.use('*', ...)` in a contribution router: contributions share the mounted router, so it also guards the SPA and every contribution mounted after yours. Check the operation and record/parent-record access; a login page, private disk, Client filter or Policy field allowlist is not authentication. Add scoped middleware and principal-based Policy to the generated routes first. If an existing contract needs an adapter, extend that module using the public Server manager and retain its authorization checks; missing built-in authentication alone is not a reason to replace the routes. Never trust browser-supplied ownership.

## Server and Client services

Resolve the original token from the current App container after registration; in a ServiceProvider use `this.app.container`. Do not create another database, Drive manager, or API Client.

```ts
import { serverFileRepositoryManagerToken } from '@nocobase/app-plugin-file/server';

const manager = container.resolve(serverFileRepositoryManagerToken);
const files = manager.repository('invoice_files', {
  connection: 'main',
  disk: 'local',
  accessPath: '/uploads/invoices',
  // Only the upload path binds this; the Repository itself is unrestricted,
  // as a db.repository() call is.
  policy: { read: true, create: true, update: false, delete: false },
});
const { record } = await files.uploadOne({ file });
const url = files.getUrl(record);
```

Server repository() takes the exact logical name from createCollection, not its physical table name (for example, crmAccountFiles is not crm_account_files), and requires disk/accessPath matching its routes. getUrl is synchronous and App-local; getStorageUrl asynchronously uses the record's disk/key to obtain a public or signed storage URL. Neither queries the database. Direct Server CRUD does not decorate URLs; uploads do.

A trusted server write can pass its outermost `DatabaseConnection` as `transaction` in these repository options. Uploaded rows then commit with the business write. Let errors propagate to roll back that transaction; rollback verifies ownership through the root connection before deleting uploaded objects. If ownership verification fails, objects are retained and a transaction callback warning is reported. Do not pass a savepoint connection or catch an upload error and commit the enclosing write. HTTP uploads remain independent commits and accept no transaction from the client.

Client code resolves `clientFileRepositoryManagerToken` and calls `manager.repository('invoiceAttachments')` with the resource name. It reuses apiClientToken, session and base URL; do not pass disk/connection/accessPath.

```ts
const { record } = await files.uploadOne({ file });
const batch = await files.uploadMany({ files: Array.from(input.files ?? []) });
const rows = await files.findMany({ limit: 20 });
await files.deleteOne({ filter: { id: record.id } });
```

Inputs are a native File or a nonempty File array. Results are `{ record, createdTargets, version? }` and `{ createdCount, records }`. Upload already creates metadata. Client uploads accept an optional second `{ signal }` argument; abort does not undo a server commit. Use returned contentUrl directly: HTTP already adds the host prefix, such as `/main`. Both the read Policy and custom selects must retain `id`, `ext`, `filename`, `mimeType`, `size`, `createdAt` and `updatedAt` when their results feed UI Library UI. `id` and `ext` derive `contentUrl`; the remaining fields support thumbnails, preview selection and component refresh. Verify queried records, not only the immediate upload response. Storage `disk` and `key` need not be exposed to read-only UI.

## UI Library components

The UI Library block carries its own `locales/`. Merge them into application locales before application overrides. Its components use the current namespace and label overrides take precedence. Plugin registration supplies repository services; it does not install UI.

Reuse the preinstalled directory when present. For an application without it, materialize `component-ui` from a NocoBase source workspace into the target application:

```bash
pnpm exec shadcn add @nocobase/file-component-ui
```

Run the command from a consuming application. In this source repository, copy the files declared by `ui-library/registry/file/registry.json` to their targets and retain workspace dependency ranges. For upgrades, merge changes with the existing application copy. The block adds no route or permissions; keep existing customized primitives.

Default already declares the viewer. For an application installing the source or upgrading an older copy, add the OOXML viewer from the target App directory if missing:

```bash
pnpm add -D --save-exact @silurus/ooxml@0.85.1
```

The viewer belongs in the App's `devDependencies`: Vite compiles this application-owned client source. UI Library item `dependencies` describes the installation recipe, while a plugin's published runtime Client imports belong in that plugin's `peerDependencies`. Do not move this viewer into server `dependencies` or add it as a plugin peer solely for copied UI Library source. Merge the block's locale resources into the application namespace. Merge UI Library upgrades with App customizations instead of overwriting installed source.

Use a version of `@nocobase/dev-config` whose `createAppViteConfig` excludes `@silurus/ooxml` from dependency prebundling. For an older shared preset or custom Vite configuration, merge this entry into the existing configuration and preserve other exclusions:

```ts
optimizeDeps: {
  exclude: ['@silurus/ooxml'],
},
```

Restart the development server after changing the configuration. This exclusion preserves the viewer's `import.meta.url`-relative WASM paths during development; verify the parser WASM requests in both development and the served production build.

Compose inside the started App's React context:

```tsx
import { useMemo, useState, type ReactElement } from 'react';
import { useService } from '@nocobase/app-client';
import {
  clientFileRepositoryManagerToken,
  type FileRecord,
} from '@nocobase/app-plugin-file/client';
import {
  FileUploadField,
  FileList,
} from '#extensions/nocobase-file-component-ui/index';

export function InvoiceAttachments(): ReactElement {
  const manager = useService(clientFileRepositoryManagerToken);
  const repository = useMemo(
    () => manager.repository('invoiceAttachments'),
    [manager],
  );
  const [value, setValue] = useState<readonly FileRecord[]>([]);
  const [error, setError] = useState('');
  return (
    <>
      <FileUploadField
        repository={repository}
        value={value}
        onChange={setValue}
        multiple
        onError={(cause) => setError(cause.message)}
      />
      <FileList files={value} onError={(cause) => setError(cause.message)} />
      {error && <p role='alert'>{error}</p>}
    </>
  );
}
```

FileUploadField takes a repository and controlled value/onChange; onStatusChange reports idle/uploading/error so forms can prevent incomplete submissions. Accept/maxSize/maxFiles are UI checks. removeOnDelete calls deleteOne, deleting metadata only; otherwise removal unlinks the selection. Read-only FileList, FileThumbnail, FilePreviewField and FilePreviewDialog use contentUrl without a repository prop. Import UI types from the installed recipe. Supply translated labels and adapt App-owned source as needed.

### Preview and content access

Preview supports safe raster images, PDF, text/Markdown and audio/video. HTML/SVG/XML previews and unsafe URL schemes are rejected. PDF is fetched and embedded as a same-origin `blob:` retyped to `application/pdf`, and an HTML or XML response is refused; the browser's own PDF viewer renders it, so a browser without one downloads it instead. DOCX, XLSX and PPTX use lazily loaded `@silurus/ooxml` viewers to render locally from `FileRecord.contentUrl`; their content is not sent to a third-party preview service. Legacy DOC/XLS/PPT and OpenDocument formats use Office Online, which requires an internet-accessible absolute URL and cannot use the App session.

OOXML, PDF and text fetches use same-origin credentials. An App-owned content route protected by same-origin session cookies can therefore serve restricted previews, provided it checks the caller's record access. Cross-origin fetches omit credentials and require CORS; an external cookie-protected URL is not supported by this default path. The Repository API client's Bearer token is not automatically attached to content fetches.

For Bearer-only content authorization, adapt the installed UI Library source in the App. Add an authenticated content loader to the OOXML fetch path and `PreviewBody`'s PDF/text fetches, and adapt download and media/image paths as needed. Resolve authentication through the App's supported client/session services and send credentials only to the trusted content endpoint. Do not put tokens in `contentUrl`. If the adapter creates blob URLs, keep an App-owned trusted URL set, pass it to the relevant `resolveSafeFileUrl` calls, and revoke URLs when replaced or unmounted. Merely assigning a `blob:` URL to `FileRecord.contentUrl` is insufficient: the default components reject untrusted blob URLs.

OOXML request or rendering failure displays an error and, when downloads are enabled, a download action. `download={false}` removes that action. This is not a second preview service or a guarantee that downloading the same inaccessible URL will succeed.

## Verify and handle failures

Verify upload → query → contentUrl → identical downloaded bytes, including batch upload, resource aliases and host prefixes. After materialization run the consuming App's typecheck/build and exercise upload, cancel/retry, remove, download and preview. Restricted files need anonymous, forbidden-user and permitted-user tests against both API and content routes. Inspectors check registration only.

For preview integration or changes, verify each of PDF/DOCX/XLSX/PPTX with real files in development and a served production build. Check relative same-origin URLs, an authorized and unauthorized session, cross-origin CORS success/failure, failed requests and invalid documents, and `download={false}`. Close or switch files while loading and after a failure: pending requests must abort, viewers must be destroyed, and the next file must render without stale state. Verify PDF in a headed browser: previewing and paging a PDF must keep the top-level URL and the session. In a headless browser without a PDF viewer, a download or an `about:blank` tab is a browser limitation: record the browser and its mode and retry headed before reporting a component defect. A login redirect points to the content route or the session. Unit mocks and a successful build do not establish document rendering fidelity.

- Upload defaults are 5 MiB single / 20 MiB batch for the whole multipart body, including overhead. Direct Server uploads have no HTTP limit; UI maxSize checks an individual file. The Client supplies the multipart boundary.
- Upload failures use the standard `/api` error body with domain `file`; branch on `error.reason` (`ApiClientError.reason` in the Client), never on `message`. A successful upload answers 201.
- BODY_TOO_LARGE (413) and UNSUPPORTED_MEDIA_TYPE (415): reduce request size or adjust route limits; send `multipart/form-data`. INVALID_MULTIPART/INVALID_FILE/INVALID_FILES (400): send native File values and a nonempty batch. PRINCIPAL_REQUIRED (403): a Policy function needs a resolved principal.
- INVALID_FILE_COLLECTION: first compare the migration name, route collection and Server repository argument; then await collections.get(logicalName) and check the active connection and generated metadata. SQLite physical text does not imply a broken datetime definition. Fix the specific lookup, metadata or schema problem and retry the same upload; do not bypass File Repository. Use a new migration for corrections to already-merged history.
- STORAGE_URL_UNAVAILABLE (503): configure a capable disk or stream mode.
- FILE_COMMIT_UNCERTAIN / FILE_CLEANUP_FAILED: reconcile database records and stored objects before retrying; uploads have no idempotency key.

There is no built-in route authentication, row ACL, Range/206, ETag, conditional download, resumable upload, physical cleanup, content sniffing or malware scan. Metadata deletion retains objects; cancelled forms may leave unlinked files. Implement the policies required by the business, including referenced-file deletion and orphan cleanup.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.