Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Platform

Reference

Attachments, notes, and storage

The shared core_attachments and core_notes tables, their services and routes, the upload policy, and the local, S3, and encrypted storage backends.

Any record in Cluster ERP can carry files and notes through two shared tables in the core. This page lists the tables, services, and routes in core/attachments.py, core/notes.py, server/routes/attachments.py, and server/routes/notes.py, the upload policy in core/upload_policy.py, and the storage backends in core/storage.py. The versioned document management of the documents module is a separate feature; see documents.

Scope and source

  • Defined in: src/cluster_erp/core/attachments.py (Attachment, AttachmentService), src/cluster_erp/core/notes.py (Note, NoteService), src/cluster_erp/core/upload_policy.py (check_upload), src/cluster_erp/core/storage.py (StorageBackend, create_storage_backend), src/cluster_erp/server/routes/attachments.py, src/cluster_erp/server/routes/notes.py
  • Count: 8 attachment routes and 6 note routes (counted from the @router. decorators); 3 storage backend classes; 40 default allowed extensions

Both tables link to their parent record by entity_type and entity_id, with no foreign key, so any entity can use them.

Attachments

core_attachments derives from BaseModel and adds these columns (src/cluster_erp/core/attachments.py:51-63):

Column Type Meaning Source
code String(20), unique Business code from the attachment sequence src/cluster_erp/core/attachments.py:54, src/cluster_erp/core/attachments.py:123
entity_type String(50) Type of the parent record src/cluster_erp/core/attachments.py:55
entity_id UUID Id of the parent record src/cluster_erp/core/attachments.py:56
filename String(255) Stored name: a random hex name plus the cleaned extension src/cluster_erp/core/attachments.py:57, src/cluster_erp/core/attachments.py:124-125
original_filename String(255) The name the user uploaded src/cluster_erp/core/attachments.py:58
content_type String(100) The content type declared by the upload src/cluster_erp/core/attachments.py:59
file_size BigInteger Size in bytes src/cluster_erp/core/attachments.py:60
storage_path String(500) Key in the storage backend src/cluster_erp/core/attachments.py:61
sha256 String(64) SHA-256 of the content src/cluster_erp/core/attachments.py:62
description Text Optional description src/cluster_erp/core/attachments.py:63

The storage key is attachments/<entity_type>/<entity_id>/<stored name>, prefixed with tenants/<tenant key>/ on a multi-tenant node (src/cluster_erp/core/attachments.py:38-48). Downloads carry a Content-Disposition header built by safe_content_disposition, which strips quote, backslash, line-break, and semicolon characters from the ASCII name and percent-encodes the UTF-8 name (src/cluster_erp/core/attachments.py:24-35).

AttachmentService methods (src/cluster_erp/core/attachments.py:88-206):

Method Behaviour Source
list_for_entity Live attachments of a record src/cluster_erp/core/attachments.py:93-104
get One live attachment src/cluster_erp/core/attachments.py:106-112
create Saves the bytes to storage, then inserts the row src/cluster_erp/core/attachments.py:114-143
update Changes description or original_filename src/cluster_erp/core/attachments.py:145-153
delete Sets deleted_at; the stored file stays src/cluster_erp/core/attachments.py:155-161
bulk_delete Deactivates many through bulk_soft_delete src/cluster_erp/core/attachments.py:163-166
get_file_data Loads the bytes from storage src/cluster_erp/core/attachments.py:168-169
replace_file Deletes the old stored file, saves the new one, and updates the row src/cluster_erp/core/attachments.py:171-197
count_for_entity Number of live attachments of a record src/cluster_erp/core/attachments.py:199-206

Attachment routes

The router prefix is /api/v1/attachments (src/cluster_erp/server/routes/attachments.py:27).

Method and path Permission Purpose Source
POST / (multipart: entity_type, entity_id, description, file) attachment.create Upload a file src/cluster_erp/server/routes/attachments.py:62-85
GET /count/{entity_type}/{entity_id} attachment.read Count a record's files src/cluster_erp/server/routes/attachments.py:88-99
GET /{attachment_id}/download attachment.download Download the bytes src/cluster_erp/server/routes/attachments.py:102-121
GET /{entity_type}/{entity_id} attachment.read List a record's files src/cluster_erp/server/routes/attachments.py:124-135
PUT /{attachment_id}/replace attachment.update Replace the file src/cluster_erp/server/routes/attachments.py:138-162
PUT /{attachment_id} attachment.update Change description or name src/cluster_erp/server/routes/attachments.py:165-181
POST /bulk-delete attachment.bulk_delete Deactivate many src/cluster_erp/server/routes/attachments.py:184-199
DELETE /{attachment_id} attachment.delete Deactivate one src/cluster_erp/server/routes/attachments.py:202-215

A download of a row whose file is missing answers FILE_NOT_FOUND_ON_DISK with HTTP 404 (src/cluster_erp/server/routes/attachments.py:114-115).

Upload policy

check_upload(filename, data, allowed) returns the accepted extension or raises AppError with code UPLOAD_TYPE_REFUSED and HTTP 415 (src/cluster_erp/core/upload_policy.py:65-84). It applies two checks:

  1. The lower-case extension must be in the allow list: the setting attachment_allowed_extensions, or the 40 defaults in DEFAULT_ALLOWED_EXTENSIONS (src/cluster_erp/core/upload_policy.py:21-27, src/cluster_erp/config/settings.py:368-374).
  2. For 24 extensions with a known file signature, the content must start with that signature (src/cluster_erp/core/upload_policy.py:30-55, src/cluster_erp/core/upload_policy.py:76-82).

The upload route calls check_upload with the configured list (src/cluster_erp/server/routes/attachments.py:73-75). The module docstring states that executables and scripts are not on the list (src/cluster_erp/core/upload_policy.py:1-10).

Notes

core_notes derives from BaseModel and adds code, entity_type, entity_id, and content (src/cluster_erp/core/notes.py:17-23). NoteCreate takes entity_type (at most 50 characters), entity_id, and a non-empty content; NoteUpdate takes content (src/cluster_erp/core/notes.py:26-33). NoteService lists, gets, creates (with a code from the note sequence), updates, deactivates, bulk-deactivates, and counts notes (src/cluster_erp/core/notes.py:48-114).

Note routes

The router prefix is /api/v1/notes (src/cluster_erp/server/routes/notes.py:14).

Method and path Permission Purpose Source
GET /{entity_type}/{entity_id} note.read List a record's notes src/cluster_erp/server/routes/notes.py:33-43
GET /count/{entity_type}/{entity_id} note.read Count a record's notes src/cluster_erp/server/routes/notes.py:46-56
POST / note.create Create a note src/cluster_erp/server/routes/notes.py:59-68
PUT /{note_id} note.update Change a note src/cluster_erp/server/routes/notes.py:71-86
POST /bulk-delete note.bulk_delete Deactivate many src/cluster_erp/server/routes/notes.py:89-101
DELETE /{note_id} note.delete Deactivate one src/cluster_erp/server/routes/notes.py:104-116

Storage backends

StorageBackend is a protocol with save, load, delete, and exists (src/cluster_erp/core/storage.py:11-18). The lifespan creates one backend per process and stores it on app.state.storage (src/cluster_erp/server/app.py:65-75, src/cluster_erp/server/app.py:92).

Class Stores in Notes Source
LocalStorageBackend A folder on disk (storage_path) Refuses keys containing .. or resolving outside the root src/cluster_erp/core/storage.py:21-48
S3StorageBackend An S3 or S3-compatible bucket Optional key prefix; blocking client calls run in a thread src/cluster_erp/core/storage.py:51-135
EncryptedStorageBackend Wraps either backend Seals every saved object with AES-256-GCM, using the storage key as associated data; reads legacy plaintext objects unchanged src/cluster_erp/core/storage.py:138-174

create_storage_backend picks S3 when storage_backend is s3, else local storage. It wraps the result in EncryptedStorageBackend when storage_encryption_key is set, and logs a warning when it is not (src/cluster_erp/core/storage.py:177-209). The settings validator requires the key in multi-tenant mode (src/cluster_erp/config/settings.py:805-809). Key handling is documented in second factor and secret storage.

Constraints and validation

  • An attachment row and its file are written in the same request; the file is saved before the row is flushed (src/cluster_erp/core/attachments.py:126-142).
  • Deactivating an attachment does not remove the stored file (src/cluster_erp/core/attachments.py:155-161). Removal of files at retention time belongs to the records module; see records.

Terms introduced

Term Code identifier Definition Turkish term Source
attachment Attachment A file linked to any record by entity type and id. ek src/cluster_erp/core/attachments.py:51
note Note A text note linked to any record by entity type and id. not src/cluster_erp/core/notes.py:17
storage backend StorageBackend The interface that saves and loads file bytes by key. depolama arka ucu src/cluster_erp/core/storage.py:11

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13