Archives - ZIP, 7Z, RAR and TAR/.tar.gz uploads are indexed in the background into archive_entries (7-Zip compiled to WASM in a worker thread; node-tar for tar). macOS/Windows clutter and paths that could escape the extraction folder are skipped. - Models inside archives are extracted once per archive to hash and geometry-fingerprint them, so duplicates are found inside packs too. Pictures, PDFs and readmes are listed but never matched. - New routes list an archive's entries and serve/convert a single entry, extracted on first use and cached under extracted/. Viewer - Model detail lists what's inside each archive with view/download links; opening an entry adds a viewer tab. - New 3D formats: FBX (with sibling textures), OBJ with its MTL and textures, STEP/STP via occt-import-js, and Rhino 3DM via rhino3dm — the WASM readers are served from node_modules, loaded on first use. - Pictures, PDFs and text files (jpg/png/gif/webp/bmp/svg, pdf, txt/md) can be uploaded and preview in the viewer; a model without an embedded 3MF preview uses its first picture as the card thumbnail. - Inline file responses send nosniff, and SVGs are sandboxed. - GLB conversions write to a temp file and rename, so a half-written preview is never served; a later tab can't be overdrawn by a slow load. - Better 3MF card image: the project thumbnail or plate 1 render, not a slicer pick map. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
152 lines
6.2 KiB
SQL
152 lines
6.2 KiB
SQL
-- STL Library Manager schema
|
|
-- A "model" is one logical download (a Thingiverse thing, a Printables model, a
|
|
-- single uploaded STL). A model owns one or more files (the actual .stl/.3mf/...).
|
|
|
|
-- Per-license commercial-use policy. Auto-registered as licenses are seen;
|
|
-- editable in Settings. commercial: true=sellable, false=non-commercial, null=unknown.
|
|
CREATE TABLE IF NOT EXISTS license_rules (
|
|
name TEXT PRIMARY KEY,
|
|
commercial BOOLEAN
|
|
);
|
|
|
|
-- App users (email/password auth).
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id SERIAL PRIMARY KEY,
|
|
email TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- One-time password-reset tokens (token stored hashed).
|
|
CREATE TABLE IF NOT EXISTS password_resets (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token_hash TEXT NOT NULL,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
used BOOLEAN NOT NULL DEFAULT false
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_password_resets_token ON password_resets(token_hash);
|
|
|
|
-- Simple key/value app settings (API tokens, site auth). Editable from the UI.
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS designers (
|
|
id SERIAL PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
source_site TEXT, -- thingiverse | printables | makerworld | ...
|
|
profile_url TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (name, source_site)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS projects (
|
|
id SERIAL PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
date_created TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
date_modified TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tags (
|
|
id SERIAL PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS models (
|
|
id SERIAL PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
source_site TEXT, -- where it came from, null for manual upload
|
|
source_url TEXT, -- original page URL (for attribution)
|
|
source_id TEXT, -- the site's id for the thing/model
|
|
license TEXT,
|
|
designer_id INTEGER REFERENCES designers(id) ON DELETE SET NULL,
|
|
thumbnail_path TEXT,
|
|
print_settings JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
date_added TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
date_modified TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS files (
|
|
id SERIAL PRIMARY KEY,
|
|
model_id INTEGER NOT NULL REFERENCES models(id) ON DELETE CASCADE,
|
|
file_name TEXT NOT NULL, -- on-disk name (unique suffix)
|
|
original_name TEXT NOT NULL, -- name as downloaded/uploaded
|
|
file_path TEXT NOT NULL, -- path on disk
|
|
format TEXT NOT NULL, -- stl | 3mf | obj | step | gcode | zip | ...
|
|
size BIGINT NOT NULL DEFAULT 0,
|
|
volume DOUBLE PRECISION, -- cm^3 (from node-stl), when computable
|
|
bbox JSONB, -- {x,y,z} bounding box
|
|
facets INTEGER,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS model_projects (
|
|
model_id INTEGER NOT NULL REFERENCES models(id) ON DELETE CASCADE,
|
|
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (model_id, project_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS model_tags (
|
|
model_id INTEGER NOT NULL REFERENCES models(id) ON DELETE CASCADE,
|
|
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (model_id, tag_id)
|
|
);
|
|
|
|
-- Cached GLB conversion of a 3MF, for in-browser 3D preview.
|
|
ALTER TABLE files ADD COLUMN IF NOT EXISTS glb_path TEXT;
|
|
|
|
-- Whether the model's license permits commercial use (selling). true/false/null.
|
|
ALTER TABLE models ADD COLUMN IF NOT EXISTS commercial_use BOOLEAN;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_files_model ON files(model_id);
|
|
CREATE INDEX IF NOT EXISTS idx_model_projects_p ON model_projects(project_id);
|
|
CREATE INDEX IF NOT EXISTS idx_model_tags_t ON model_tags(tag_id);
|
|
CREATE INDEX IF NOT EXISTS idx_models_source ON models(source_site, source_id);
|
|
|
|
-- Content + geometry fingerprints for the duplicate finder (filled in the
|
|
-- background by lib/fingerprint.js).
|
|
ALTER TABLE files ADD COLUMN IF NOT EXISTS sha256 TEXT;
|
|
ALTER TABLE files ADD COLUMN IF NOT EXISTS geometry_hash TEXT;
|
|
-- null = not checked yet; ok | error | missing | na (format has no geometry fingerprint)
|
|
ALTER TABLE files ADD COLUMN IF NOT EXISTS geometry_status TEXT;
|
|
ALTER TABLE files ADD COLUMN IF NOT EXISTS fingerprinted_at TIMESTAMPTZ;
|
|
CREATE INDEX IF NOT EXISTS idx_files_sha256 ON files(sha256);
|
|
CREATE INDEX IF NOT EXISTS idx_files_geometry ON files(geometry_hash);
|
|
|
|
-- Free-form notes per model.
|
|
ALTER TABLE models ADD COLUMN IF NOT EXISTS notes TEXT NOT NULL DEFAULT '';
|
|
-- Soft delete: set when a model is moved to the trash.
|
|
ALTER TABLE models ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
|
CREATE INDEX IF NOT EXISTS idx_models_deleted ON models(deleted_at);
|
|
|
|
-- Archive files (zip/7z/rar/tar) are indexed in the background:
|
|
-- null = not yet, ok | error (with archive_error).
|
|
ALTER TABLE files ADD COLUMN IF NOT EXISTS archive_status TEXT;
|
|
ALTER TABLE files ADD COLUMN IF NOT EXISTS archive_error TEXT;
|
|
|
|
-- Files inside archives, so they can be browsed, previewed and matched as duplicates.
|
|
CREATE TABLE IF NOT EXISTS archive_entries (
|
|
id SERIAL PRIMARY KEY,
|
|
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
path TEXT NOT NULL, -- path inside the archive, as the archive stores it
|
|
format TEXT NOT NULL,
|
|
size BIGINT NOT NULL DEFAULT 0,
|
|
sha256 TEXT,
|
|
geometry_hash TEXT,
|
|
geometry_status TEXT, -- same states as files.geometry_status
|
|
fingerprinted_at TIMESTAMPTZ,
|
|
UNIQUE (file_id, path)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_entries_sha256 ON archive_entries(sha256);
|
|
CREATE INDEX IF NOT EXISTS idx_entries_geometry ON archive_entries(geometry_hash);
|
|
|
|
-- Duplicate groups marked "not duplicates" (signature = sorted model ids).
|
|
CREATE TABLE IF NOT EXISTS duplicate_dismissals (
|
|
signature TEXT PRIMARY KEY,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|