Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 780f85de59 |
@@ -0,0 +1,55 @@
|
||||
# Tanabata File Manager
|
||||
|
||||
Multi-user, tag-based web file manager for images and video.
|
||||
|
||||
## Architecture
|
||||
|
||||
Monorepo: `backend/` (Go) + `frontend/` (SvelteKit).
|
||||
|
||||
- Backend: Go + Gin + pgx v5 + goose migrations. Clean Architecture.
|
||||
- Frontend: SvelteKit SPA + Tailwind CSS + CSS custom properties.
|
||||
- DB: PostgreSQL 14+.
|
||||
- Auth: JWT Bearer tokens.
|
||||
|
||||
## Key documents (read before coding)
|
||||
|
||||
- `openapi.yaml` — full REST API specification (36 paths, 58 operations)
|
||||
- `docs/GO_PROJECT_STRUCTURE.md` — backend architecture, layer rules, DI pattern
|
||||
- `docs/FRONTEND_STRUCTURE.md` — frontend architecture, CSS approach, API client
|
||||
- `docs/Описание.md` — product requirements in Russian
|
||||
- `backend/migrations/001_init.sql` — database schema (4 schemas, 16 tables)
|
||||
|
||||
## Design reference
|
||||
|
||||
The `docs/reference/` directory contains the previous Python/Flask version.
|
||||
Use its visual design as the basis for the new frontend:
|
||||
- Color palette: #312F45 (bg), #9592B5 (accent), #444455 (tag default), #111118 (elevated)
|
||||
- Font: Epilogue (variable weight)
|
||||
- Dark theme is primary
|
||||
- Mobile-first layout with bottom navbar
|
||||
- 160×160 thumbnail grid for files
|
||||
- Colored tag pills
|
||||
- Floating selection bar for multi-select
|
||||
|
||||
## Backend commands
|
||||
```bash
|
||||
cd backend
|
||||
go run ./cmd/server # run dev server
|
||||
go test ./... # run all tests
|
||||
```
|
||||
|
||||
## Frontend commands
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev # vite dev server
|
||||
npm run build # production build
|
||||
npm run generate:types # regenerate API types from openapi.yaml
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- Go: gofmt, no global state, context.Context as first param in all service methods
|
||||
- TypeScript: strict mode, named exports
|
||||
- SQL: snake_case, all migrations via goose
|
||||
- API errors: { code, message, details? }
|
||||
- Git: conventional commits (feat:, fix:, docs:, refactor:)
|
||||
@@ -1,57 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(Tanabata
|
||||
VERSION 2.0.0
|
||||
HOMEPAGE_URL https://github.com/H1K0/tanabata
|
||||
LANGUAGES C
|
||||
)
|
||||
|
||||
set(CMAKE_C_STANDARD 99)
|
||||
|
||||
set(CORE_SRC
|
||||
include/core.h
|
||||
tanabata/core/core_func.h
|
||||
tanabata/core/sasahyou.c
|
||||
tanabata/core/sappyou.c
|
||||
tanabata/core/shoppyou.c
|
||||
)
|
||||
|
||||
set(TANABATA_SRC
|
||||
${CORE_SRC}
|
||||
include/tanabata.h
|
||||
tanabata/lib/database.c
|
||||
tanabata/lib/sasa.c
|
||||
tanabata/lib/tanzaku.c
|
||||
tanabata/lib/kazari.c
|
||||
)
|
||||
|
||||
set(TDBMS_SERVER_SRC
|
||||
${TANABATA_SRC}
|
||||
include/tdbms.h
|
||||
tdbms/server/tdbms-server.c
|
||||
)
|
||||
|
||||
set(TDBMS_CLIENT_SRC
|
||||
include/tdbms.h
|
||||
include/tdbms-client.h
|
||||
tdbms/client/tdbms-client.c
|
||||
)
|
||||
|
||||
set(CLI_SRC
|
||||
${TANABATA_SRC}
|
||||
tfm/cli/tfm-cli.c
|
||||
)
|
||||
|
||||
# Tanabata shared lib
|
||||
add_library(tanabata SHARED ${TANABATA_SRC})
|
||||
|
||||
# Tanabata DBMS server
|
||||
add_executable(tdbms ${TDBMS_SERVER_SRC})
|
||||
|
||||
# Tanabata DMBS CLI client app
|
||||
add_executable(tdb tdbms/cli/tdbms-cli.c ${TDBMS_CLIENT_SRC})
|
||||
|
||||
# Tanabata CLI app
|
||||
add_executable(tfm ${CLI_SRC})
|
||||
|
||||
add_executable(test test.c ${TDBMS_CLIENT_SRC})
|
||||
@@ -1,64 +0,0 @@
|
||||
<h1 align="center">🎋 Tanabata Project 🎋</h1>
|
||||
|
||||
---
|
||||
|
||||
[![Release version][release-shield]][release-link]
|
||||
|
||||
## Contents
|
||||
|
||||
- [About](#about)
|
||||
- [Glossary](#glossary)
|
||||
- [Tanabata library](#tanabata-library)
|
||||
- [Tanabata DBMS](#tanabata-dbms)
|
||||
- [Tanabata FM](#tanabata-fm)
|
||||
|
||||
## About
|
||||
|
||||
Tanabata (_jp._ 七夕) is Japanese festival. People generally celebrate this day by writing wishes, sometimes in the form of poetry, on _tanzaku_ (_jp._ 短冊), small pieces of paper, and hanging them on _sasa_ (_jp._ 笹), bamboo. See [this Wikipedia page](https://en.wikipedia.org/wiki/Tanabata) for more information.
|
||||
|
||||
Tanabata Project is a software project that will let you enjoy the Tanabata festival. It allows you to store and organize your data as _sasa_ bamboos, on which you can hang almost any number of _tanzaku_, just like adding tags on it.
|
||||
|
||||
## Glossary
|
||||
|
||||
**Tanabata (_jp._ 七夕)** is a software package project for storing information and organizing it with tags.
|
||||
|
||||
**Sasa (_jp._ 笹)** is a file record. It contains 64-bit ID number, the creation timestamp, and the path to the file.
|
||||
|
||||
**Tanzaku (_jp._ 短冊)** is a tag record. It contains 64-bit ID number, creation and last modification timestamps, name and description.
|
||||
|
||||
**Kazari (_jp._ 飾り)** is a sasa-tanzaku association record. It contains the creation timestamp and associated sasa and tanzaku IDs.
|
||||
|
||||
**Hyou (_jp._ 表)** is a table.
|
||||
|
||||
**Sasahyou (_jp._ 笹表)** is a table of sasa.
|
||||
|
||||
**Sappyou (_jp._ 冊表)** is a table of tanzaku.
|
||||
|
||||
**Shoppyou (_jp._ 飾表)** is a table of kazari.
|
||||
|
||||
**TDB (Tanabata DataBase)** is a relational database that consists of three tables: _sasahyou_, _sappyou_ and _shoppyou_.
|
||||
|
||||
**TDBMS (Tanabata DataBase Management System)** is a management system for TDBs.
|
||||
|
||||
**TFM (Tanabata File Manager)** is a TDBMS-powered file manager.
|
||||
|
||||
**Tweb (Tanabata web)** is the web user interface for TDBMS and TFM.
|
||||
|
||||
## Tanabata library
|
||||
|
||||
Tanabata library is a C library for TDB operations. Documentation coming soon...
|
||||
|
||||
## Tanabata DBMS
|
||||
|
||||
Tanabata Database Management System is the management system for Tanabata databases. Documentation coming soon...
|
||||
|
||||
## Tanabata FM
|
||||
|
||||
Tanabata File Manager is the TDBMS-powered file manager. Full documentation is [here](docs/fm.md).
|
||||
|
||||
---
|
||||
|
||||
<h6 align="center"><i>© Masahiko AMANO aka H1K0, 2022—present</i></h6>
|
||||
|
||||
[release-shield]: https://img.shields.io/github/release/H1K0/tanabata/all.svg?style=for-the-badge
|
||||
[release-link]: https://github.com/H1K0/tanabata/releases
|
||||
@@ -1,5 +0,0 @@
|
||||
title: Tanabata FM
|
||||
description: A file manager that will let you enjoy the Tanabata festival!
|
||||
remote_theme: pages-themes/merlot@v0.2.0
|
||||
plugins:
|
||||
- jekyll-remote-theme
|
||||
@@ -0,0 +1,425 @@
|
||||
-- =============================================================================
|
||||
-- Tanabata File Manager — Database Schema v2
|
||||
-- =============================================================================
|
||||
-- PostgreSQL 14+
|
||||
--
|
||||
-- Design decisions:
|
||||
-- • Business logic lives in Go (DDD), no stored procedures
|
||||
-- • UUID v7 for entity PKs (created_at extracted from UUID, no separate column)
|
||||
-- • ACL: is_public flag on objects + acl.permissions table for granular control
|
||||
-- • Schemas: core, data, acl, activity
|
||||
-- • Flat pools (no hierarchy)
|
||||
-- • Soft delete for files only (trash/recycle bin)
|
||||
-- • phash field for future duplicate detection
|
||||
-- • metadata jsonb on all entities
|
||||
-- • Unified audit log with reference tables instead of enums
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Extensions
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Schemas
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS core;
|
||||
CREATE SCHEMA IF NOT EXISTS data;
|
||||
CREATE SCHEMA IF NOT EXISTS acl;
|
||||
CREATE SCHEMA IF NOT EXISTS activity;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Utility functions
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- UUID v7 generator
|
||||
CREATE OR REPLACE FUNCTION public.uuid_v7(cts timestamptz DEFAULT clock_timestamp())
|
||||
RETURNS uuid LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
state text = current_setting('uuidv7.old_tp', true);
|
||||
old_tp text = split_part(state, ':', 1);
|
||||
base int = coalesce(nullif(split_part(state, ':', 4), '')::int, (random()*16777215/2-1)::int);
|
||||
tp text;
|
||||
entropy text;
|
||||
seq text = base;
|
||||
seqn int = split_part(state, ':', 2);
|
||||
ver text = coalesce(split_part(state, ':', 3), to_hex(8+(random()*3)::int));
|
||||
BEGIN
|
||||
base = (random()*16777215/2-1)::int;
|
||||
tp = lpad(to_hex(floor(extract(epoch from cts)*1000)::int8), 12, '0') || '7';
|
||||
IF tp IS DISTINCT FROM old_tp THEN
|
||||
old_tp = tp;
|
||||
ver = to_hex(8+(random()*3)::int);
|
||||
base = (random()*16777215/2-1)::int;
|
||||
seqn = base;
|
||||
ELSE
|
||||
seqn = seqn + (random()*1000)::int;
|
||||
END IF;
|
||||
PERFORM set_config('uuidv7.old_tp', old_tp||':'||seqn||':'||ver||':'||base, false);
|
||||
entropy = md5(gen_random_uuid()::text);
|
||||
seq = lpad(to_hex(seqn), 6, '0');
|
||||
RETURN (tp || substring(seq from 1 for 3) || ver || substring(seq from 4 for 3) ||
|
||||
substring(entropy from 1 for 12))::uuid;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Extract timestamp from UUID v7
|
||||
CREATE OR REPLACE FUNCTION public.uuid_extract_timestamp(uuid_val uuid)
|
||||
RETURNS timestamptz LANGUAGE sql IMMUTABLE PARALLEL SAFE AS $$
|
||||
SELECT to_timestamp(
|
||||
('x' || left(replace(uuid_val::text, '-', ''), 12))::bit(48)::bigint / 1000.0
|
||||
);
|
||||
$$;
|
||||
|
||||
-- =============================================================================
|
||||
-- SCHEMA: core
|
||||
-- =============================================================================
|
||||
|
||||
-- Users
|
||||
CREATE TABLE core.users (
|
||||
id smallint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(32) NOT NULL,
|
||||
password text NOT NULL, -- bcrypt hash via pgcrypto
|
||||
is_admin boolean NOT NULL DEFAULT false,
|
||||
can_create boolean NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT uni__users__name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- MIME types (whitelist of supported file types)
|
||||
CREATE TABLE core.mime_types (
|
||||
id smallint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(127) NOT NULL,
|
||||
extension varchar(16) NOT NULL,
|
||||
|
||||
CONSTRAINT uni__mime_types__name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- Object types (file, tag, category, pool — used in ACL and audit log)
|
||||
CREATE TABLE core.object_types (
|
||||
id smallint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(32) NOT NULL,
|
||||
|
||||
CONSTRAINT uni__object_types__name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- SCHEMA: data
|
||||
-- =============================================================================
|
||||
|
||||
-- Categories (logical grouping of tags)
|
||||
CREATE TABLE data.categories (
|
||||
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
|
||||
name varchar(256) NOT NULL,
|
||||
notes text,
|
||||
color char(6),
|
||||
metadata jsonb,
|
||||
creator_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
is_public boolean NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT uni__categories__name UNIQUE (name),
|
||||
CONSTRAINT chk__categories__color_hex
|
||||
CHECK (color IS NULL OR color ~* '^[A-Fa-f0-9]{6}$')
|
||||
);
|
||||
|
||||
-- Tags
|
||||
CREATE TABLE data.tags (
|
||||
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
|
||||
name varchar(256) NOT NULL,
|
||||
notes text,
|
||||
color char(6),
|
||||
category_id uuid REFERENCES data.categories(id)
|
||||
ON UPDATE CASCADE ON DELETE SET NULL,
|
||||
metadata jsonb,
|
||||
creator_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
is_public boolean NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT uni__tags__name UNIQUE (name),
|
||||
CONSTRAINT chk__tags__color_hex
|
||||
CHECK (color IS NULL OR color ~* '^[A-Fa-f0-9]{6}$')
|
||||
);
|
||||
|
||||
-- Tag rules (when when_tag is added to a file, then_tag is also added)
|
||||
CREATE TABLE data.tag_rules (
|
||||
when_tag_id uuid NOT NULL REFERENCES data.tags(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
then_tag_id uuid NOT NULL REFERENCES data.tags(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
|
||||
PRIMARY KEY (when_tag_id, then_tag_id)
|
||||
);
|
||||
|
||||
-- Files
|
||||
CREATE TABLE data.files (
|
||||
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
|
||||
original_name varchar(256), -- original filename at upload time
|
||||
mime_id smallint NOT NULL REFERENCES core.mime_types(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
content_datetime timestamptz NOT NULL DEFAULT clock_timestamp(), -- content datetime (e.g. photo taken)
|
||||
notes text,
|
||||
metadata jsonb, -- user-editable key-value data
|
||||
exif jsonb NOT NULL DEFAULT '{}'::jsonb, -- EXIF data extracted at upload (immutable)
|
||||
phash bigint, -- perceptual hash for duplicate detection (future)
|
||||
creator_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
is_public boolean NOT NULL DEFAULT false,
|
||||
is_deleted boolean NOT NULL DEFAULT false -- soft delete (trash)
|
||||
);
|
||||
|
||||
-- File ↔ Tag (many-to-many)
|
||||
CREATE TABLE data.file_tag (
|
||||
file_id uuid NOT NULL REFERENCES data.files(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
tag_id uuid NOT NULL REFERENCES data.tags(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
|
||||
PRIMARY KEY (file_id, tag_id)
|
||||
);
|
||||
|
||||
-- Pools (ordered collections of files)
|
||||
CREATE TABLE data.pools (
|
||||
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
|
||||
name varchar(256) NOT NULL,
|
||||
notes text,
|
||||
metadata jsonb,
|
||||
creator_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
is_public boolean NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT uni__pools__name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- File ↔ Pool (many-to-many, with ordering)
|
||||
-- `position` uses integer with gaps (e.g. 1000, 2000, 3000) to allow
|
||||
-- insertions without renumbering. Compact when gaps get too small.
|
||||
CREATE TABLE data.file_pool (
|
||||
file_id uuid NOT NULL REFERENCES data.files(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
pool_id uuid NOT NULL REFERENCES data.pools(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
position integer NOT NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY (file_id, pool_id)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- SCHEMA: acl
|
||||
-- =============================================================================
|
||||
|
||||
-- Granular permissions
|
||||
-- If is_public=true on the object, it is accessible to everyone (ACL ignored).
|
||||
-- If is_public=false, only creator and users with can_view=true see it.
|
||||
-- Admins bypass all ACL checks.
|
||||
CREATE TABLE acl.permissions (
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
object_type_id smallint NOT NULL REFERENCES core.object_types(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
object_id uuid NOT NULL,
|
||||
can_view boolean NOT NULL DEFAULT true,
|
||||
can_edit boolean NOT NULL DEFAULT false,
|
||||
|
||||
PRIMARY KEY (user_id, object_type_id, object_id)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- SCHEMA: activity
|
||||
-- =============================================================================
|
||||
|
||||
-- Action types (reference table for audit log)
|
||||
CREATE TABLE activity.action_types (
|
||||
id smallint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(64) NOT NULL,
|
||||
|
||||
CONSTRAINT uni__action_types__name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- Sessions
|
||||
CREATE TABLE activity.sessions (
|
||||
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
token_hash text NOT NULL, -- hashed session token
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
user_agent varchar(256) NOT NULL,
|
||||
started_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
expires_at timestamptz,
|
||||
last_activity timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
|
||||
CONSTRAINT uni__sessions__token_hash UNIQUE (token_hash)
|
||||
);
|
||||
|
||||
-- File views (analytics)
|
||||
CREATE TABLE activity.file_views (
|
||||
file_id uuid NOT NULL REFERENCES data.files(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
viewed_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
|
||||
PRIMARY KEY (file_id, viewed_at, user_id)
|
||||
);
|
||||
|
||||
-- Pool views (analytics)
|
||||
CREATE TABLE activity.pool_views (
|
||||
pool_id uuid NOT NULL REFERENCES data.pools(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
viewed_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
|
||||
PRIMARY KEY (pool_id, viewed_at, user_id)
|
||||
);
|
||||
|
||||
-- Tag usage tracking (when tag is used as filter)
|
||||
CREATE TABLE activity.tag_uses (
|
||||
tag_id uuid NOT NULL REFERENCES data.tags(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
used_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
is_included boolean NOT NULL, -- true=included in filter, false=excluded
|
||||
|
||||
PRIMARY KEY (tag_id, used_at, user_id)
|
||||
);
|
||||
|
||||
-- Audit log (unified journal for all user actions)
|
||||
CREATE TABLE activity.audit_log (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
action_type_id smallint NOT NULL REFERENCES activity.action_types(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
object_type_id smallint REFERENCES core.object_types(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
object_id uuid,
|
||||
details jsonb, -- action-specific payload
|
||||
performed_at timestamptz NOT NULL DEFAULT statement_timestamp()
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- SEED DATA
|
||||
-- =============================================================================
|
||||
|
||||
-- Object types
|
||||
INSERT INTO core.object_types (name) VALUES
|
||||
('file'), ('tag'), ('category'), ('pool');
|
||||
|
||||
-- Action types
|
||||
INSERT INTO activity.action_types (name) VALUES
|
||||
-- Auth
|
||||
('user_login'), ('user_logout'),
|
||||
-- Files
|
||||
('file_create'), ('file_edit'), ('file_delete'), ('file_restore'),
|
||||
('file_permanent_delete'), ('file_replace'),
|
||||
-- Tags
|
||||
('tag_create'), ('tag_edit'), ('tag_delete'),
|
||||
-- Categories
|
||||
('category_create'), ('category_edit'), ('category_delete'),
|
||||
-- Pools
|
||||
('pool_create'), ('pool_edit'), ('pool_delete'),
|
||||
-- Relations
|
||||
('file_tag_add'), ('file_tag_remove'),
|
||||
('file_pool_add'), ('file_pool_remove'),
|
||||
-- ACL
|
||||
('acl_change'),
|
||||
-- Admin
|
||||
('user_create'), ('user_delete'), ('user_block'), ('user_unblock'),
|
||||
('user_role_change'),
|
||||
-- Sessions
|
||||
('session_terminate');
|
||||
|
||||
-- =============================================================================
|
||||
-- INDEXES
|
||||
-- =============================================================================
|
||||
|
||||
-- core
|
||||
CREATE INDEX idx__users__name ON core.users USING hash (name);
|
||||
|
||||
-- data.categories
|
||||
CREATE INDEX idx__categories__creator_id ON data.categories USING hash (creator_id);
|
||||
|
||||
-- data.tags
|
||||
CREATE INDEX idx__tags__category_id ON data.tags USING hash (category_id);
|
||||
CREATE INDEX idx__tags__creator_id ON data.tags USING hash (creator_id);
|
||||
|
||||
-- data.tag_rules
|
||||
CREATE INDEX idx__tag_rules__when ON data.tag_rules USING hash (when_tag_id);
|
||||
CREATE INDEX idx__tag_rules__then ON data.tag_rules USING hash (then_tag_id);
|
||||
|
||||
-- data.files
|
||||
CREATE INDEX idx__files__mime_id ON data.files USING hash (mime_id);
|
||||
CREATE INDEX idx__files__creator_id ON data.files USING hash (creator_id);
|
||||
CREATE INDEX idx__files__content_datetime ON data.files USING btree (content_datetime DESC NULLS LAST);
|
||||
CREATE INDEX idx__files__is_deleted ON data.files USING btree (is_deleted) WHERE is_deleted = true;
|
||||
CREATE INDEX idx__files__phash ON data.files USING btree (phash) WHERE phash IS NOT NULL;
|
||||
|
||||
-- data.file_tag
|
||||
CREATE INDEX idx__file_tag__tag_id ON data.file_tag USING hash (tag_id);
|
||||
CREATE INDEX idx__file_tag__file_id ON data.file_tag USING hash (file_id);
|
||||
|
||||
-- data.pools
|
||||
CREATE INDEX idx__pools__creator_id ON data.pools USING hash (creator_id);
|
||||
|
||||
-- data.file_pool
|
||||
CREATE INDEX idx__file_pool__pool_id ON data.file_pool USING hash (pool_id);
|
||||
CREATE INDEX idx__file_pool__file_id ON data.file_pool USING hash (file_id);
|
||||
|
||||
-- acl.permissions
|
||||
CREATE INDEX idx__acl__object ON acl.permissions USING btree (object_type_id, object_id);
|
||||
CREATE INDEX idx__acl__user ON acl.permissions USING hash (user_id);
|
||||
|
||||
-- activity.sessions
|
||||
CREATE INDEX idx__sessions__user_id ON activity.sessions USING hash (user_id);
|
||||
CREATE INDEX idx__sessions__token_hash ON activity.sessions USING hash (token_hash);
|
||||
|
||||
-- activity.file_views
|
||||
CREATE INDEX idx__file_views__user_id ON activity.file_views USING hash (user_id);
|
||||
|
||||
-- activity.pool_views
|
||||
CREATE INDEX idx__pool_views__user_id ON activity.pool_views USING hash (user_id);
|
||||
|
||||
-- activity.tag_uses
|
||||
CREATE INDEX idx__tag_uses__user_id ON activity.tag_uses USING hash (user_id);
|
||||
|
||||
-- activity.audit_log
|
||||
CREATE INDEX idx__audit_log__user_id ON activity.audit_log USING hash (user_id);
|
||||
CREATE INDEX idx__audit_log__action_type_id ON activity.audit_log USING hash (action_type_id);
|
||||
CREATE INDEX idx__audit_log__object ON activity.audit_log USING btree (object_type_id, object_id)
|
||||
WHERE object_id IS NOT NULL;
|
||||
CREATE INDEX idx__audit_log__performed_at ON activity.audit_log USING btree (performed_at DESC);
|
||||
|
||||
-- =============================================================================
|
||||
-- COMMENTS
|
||||
-- =============================================================================
|
||||
|
||||
COMMENT ON TABLE core.users IS 'Application users';
|
||||
COMMENT ON TABLE core.mime_types IS 'Whitelist of supported MIME types';
|
||||
COMMENT ON TABLE core.object_types IS 'Reference: entity types for ACL and audit log';
|
||||
COMMENT ON TABLE data.categories IS 'Logical grouping of tags';
|
||||
COMMENT ON TABLE data.tags IS 'File labels/tags';
|
||||
COMMENT ON TABLE data.tag_rules IS 'Auto-tagging rules: when when_tag is assigned, then_tag follows';
|
||||
COMMENT ON TABLE data.files IS 'Managed files; actual content stored on disk as {id}.{ext}';
|
||||
COMMENT ON TABLE data.file_tag IS 'Many-to-many: files <-> tags';
|
||||
COMMENT ON TABLE data.pools IS 'Ordered collections of files';
|
||||
COMMENT ON TABLE data.file_pool IS 'Many-to-many: files <-> pools, with ordering';
|
||||
COMMENT ON TABLE acl.permissions IS 'Per-object permissions (used when is_public=false)';
|
||||
COMMENT ON TABLE activity.action_types IS 'Reference: types of auditable user actions';
|
||||
COMMENT ON TABLE activity.sessions IS 'Active user sessions';
|
||||
COMMENT ON TABLE activity.file_views IS 'File view history';
|
||||
COMMENT ON TABLE activity.pool_views IS 'Pool view history';
|
||||
COMMENT ON TABLE activity.tag_uses IS 'Tag usage in filters';
|
||||
COMMENT ON TABLE activity.audit_log IS 'Unified audit trail for all user actions';
|
||||
|
||||
COMMENT ON COLUMN data.files.original_name IS 'Original filename at upload time';
|
||||
COMMENT ON COLUMN data.files.content_datetime IS 'Content datetime (e.g. when photo was taken); falls back to EXIF DateTimeOriginal';
|
||||
COMMENT ON COLUMN data.files.metadata IS 'User-editable key-value metadata';
|
||||
COMMENT ON COLUMN data.files.exif IS 'EXIF data extracted at upload time (immutable, system-managed)';
|
||||
COMMENT ON COLUMN data.files.phash IS 'Perceptual hash for image/video duplicate detection';
|
||||
COMMENT ON COLUMN data.files.is_deleted IS 'Soft-deleted files (trash); true = in recycle bin';
|
||||
COMMENT ON COLUMN data.file_pool.position IS 'Manual ordering within pool; uses gapped integers';
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
BUILD_DIR=./build/
|
||||
TARGET=all
|
||||
|
||||
while getopts "b:t:" option; do
|
||||
case $option in
|
||||
b) BUILD_DIR=$OPTARG ;;
|
||||
t) TARGET=$OPTARG ;;
|
||||
?)
|
||||
echo "Error: invalid option"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ! -d "$BUILD_DIR" ]; then
|
||||
mkdir "$BUILD_DIR"
|
||||
if [ ! -d "$BUILD_DIR" ]; then
|
||||
echo "Error: could not create folder '$BUILD_DIR'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
cmake -S . -B "$BUILD_DIR" && cmake --build "$BUILD_DIR" --target "$TARGET"
|
||||
@@ -0,0 +1,383 @@
|
||||
# Tanabata File Manager — Frontend Structure
|
||||
|
||||
## Stack
|
||||
|
||||
- **Framework**: SvelteKit (SPA mode, `ssr: false`)
|
||||
- **Language**: TypeScript
|
||||
- **CSS**: Tailwind CSS + CSS custom properties (hybrid)
|
||||
- **API types**: Auto-generated via openapi-typescript
|
||||
- **PWA**: Service worker + web manifest
|
||||
- **Font**: Epilogue (variable weight)
|
||||
- **Package manager**: npm
|
||||
|
||||
## Monorepo Layout
|
||||
|
||||
```
|
||||
tanabata/
|
||||
├── backend/ ← Go project (go.mod in here)
|
||||
│ ├── cmd/
|
||||
│ ├── internal/
|
||||
│ ├── migrations/
|
||||
│ ├── go.mod
|
||||
│ └── go.sum
|
||||
│
|
||||
├── frontend/ ← SvelteKit project (package.json in here)
|
||||
│ └── (see below)
|
||||
│
|
||||
├── openapi.yaml ← Shared API contract (root level)
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile
|
||||
├── .env.example
|
||||
└── README.md
|
||||
```
|
||||
|
||||
`openapi.yaml` lives at repository root — both backend and frontend
|
||||
reference it. The frontend generates types from it; the backend
|
||||
validates its handlers against it.
|
||||
|
||||
## Frontend Directory Layout
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── package.json
|
||||
├── svelte.config.js
|
||||
├── vite.config.ts
|
||||
├── tsconfig.json
|
||||
├── tailwind.config.ts
|
||||
├── postcss.config.js
|
||||
│
|
||||
├── src/
|
||||
│ ├── app.html # Shell HTML (PWA meta, font preload)
|
||||
│ ├── app.css # Tailwind directives + CSS custom properties
|
||||
│ ├── hooks.server.ts # Server hooks (not used in SPA mode)
|
||||
│ ├── hooks.client.ts # Client hooks (global error handling)
|
||||
│ │
|
||||
│ ├── lib/ # Shared code ($lib/ alias)
|
||||
│ │ │
|
||||
│ │ ├── api/ # API client layer
|
||||
│ │ │ ├── client.ts # Base fetch wrapper: auth headers, token refresh,
|
||||
│ │ │ │ # error parsing, base URL
|
||||
│ │ │ ├── files.ts # listFiles, getFile, uploadFile, deleteFile, etc.
|
||||
│ │ │ ├── tags.ts # listTags, createTag, getTag, updateTag, etc.
|
||||
│ │ │ ├── categories.ts # Category API functions
|
||||
│ │ │ ├── pools.ts # Pool API functions
|
||||
│ │ │ ├── auth.ts # login, logout, refresh, listSessions
|
||||
│ │ │ ├── acl.ts # getPermissions, setPermissions
|
||||
│ │ │ ├── users.ts # getMe, updateMe, admin user CRUD
|
||||
│ │ │ ├── audit.ts # queryAuditLog
|
||||
│ │ │ ├── schema.ts # AUTO-GENERATED from openapi.yaml (do not edit)
|
||||
│ │ │ └── types.ts # Friendly type aliases:
|
||||
│ │ │ # export type File = components["schemas"]["File"]
|
||||
│ │ │ # export type Tag = components["schemas"]["Tag"]
|
||||
│ │ │
|
||||
│ │ ├── components/ # Reusable UI components
|
||||
│ │ │ │
|
||||
│ │ │ ├── layout/ # App shell
|
||||
│ │ │ │ ├── Navbar.svelte # Bottom navigation bar (mobile-first)
|
||||
│ │ │ │ ├── Header.svelte # Section header with sorting controls
|
||||
│ │ │ │ ├── SelectionBar.svelte # Floating bar for multi-select actions
|
||||
│ │ │ │ └── Loader.svelte # Full-screen loading overlay
|
||||
│ │ │ │
|
||||
│ │ │ ├── file/ # File-related components
|
||||
│ │ │ │ ├── FileGrid.svelte # Thumbnail grid with infinite scroll
|
||||
│ │ │ │ ├── FileCard.svelte # Single thumbnail (160×160, selectable)
|
||||
│ │ │ │ ├── FileViewer.svelte # Full-screen preview with prev/next navigation
|
||||
│ │ │ │ ├── FileUpload.svelte # Upload form + drag-and-drop zone
|
||||
│ │ │ │ ├── FileDetail.svelte # Metadata editor (notes, datetime, tags)
|
||||
│ │ │ │ └── FilterBar.svelte # DSL filter builder UI
|
||||
│ │ │ │
|
||||
│ │ │ ├── tag/ # Tag-related components
|
||||
│ │ │ │ ├── TagBadge.svelte # Colored pill with tag name
|
||||
│ │ │ │ ├── TagPicker.svelte # Searchable tag selector (add/remove)
|
||||
│ │ │ │ ├── TagList.svelte # Tag grid for section view
|
||||
│ │ │ │ └── TagRuleEditor.svelte # Auto-tag rule management
|
||||
│ │ │ │
|
||||
│ │ │ ├── pool/ # Pool-related components
|
||||
│ │ │ │ ├── PoolCard.svelte # Pool preview card
|
||||
│ │ │ │ ├── PoolFileList.svelte # Ordered file list with drag reorder
|
||||
│ │ │ │ └── PoolDetail.svelte # Pool metadata editor
|
||||
│ │ │ │
|
||||
│ │ │ ├── acl/ # Access control components
|
||||
│ │ │ │ └── PermissionEditor.svelte # User permission grid
|
||||
│ │ │ │
|
||||
│ │ │ └── common/ # Shared primitives
|
||||
│ │ │ ├── Button.svelte
|
||||
│ │ │ ├── Modal.svelte
|
||||
│ │ │ ├── ConfirmDialog.svelte
|
||||
│ │ │ ├── Toast.svelte
|
||||
│ │ │ ├── InfiniteScroll.svelte
|
||||
│ │ │ ├── Pagination.svelte
|
||||
│ │ │ ├── SortDropdown.svelte
|
||||
│ │ │ ├── SearchInput.svelte
|
||||
│ │ │ ├── ColorPicker.svelte
|
||||
│ │ │ ├── Checkbox.svelte # Three-state: checked, unchecked, partial
|
||||
│ │ │ └── EmptyState.svelte
|
||||
│ │ │
|
||||
│ │ ├── stores/ # Svelte stores (global state)
|
||||
│ │ │ ├── auth.ts # Current user, JWT tokens, isAuthenticated
|
||||
│ │ │ ├── selection.ts # Selected item IDs, selection mode toggle
|
||||
│ │ │ ├── sorting.ts # Per-section sort key + order (persisted to localStorage)
|
||||
│ │ │ ├── theme.ts # Dark/light mode (persisted, respects prefers-color-scheme)
|
||||
│ │ │ └── toast.ts # Notification queue (success, error, info)
|
||||
│ │ │
|
||||
│ │ └── utils/ # Pure helper functions
|
||||
│ │ ├── format.ts # formatDate, formatFileSize, formatDuration
|
||||
│ │ ├── dsl.ts # Filter DSL builder: UI state → query string
|
||||
│ │ ├── pwa.ts # PWA reset, cache clear, update prompt
|
||||
│ │ └── keyboard.ts # Keyboard shortcut helpers (Ctrl+A, Escape, etc.)
|
||||
│ │
|
||||
│ ├── routes/ # SvelteKit file-based routing
|
||||
│ │ │
|
||||
│ │ ├── +layout.svelte # Root layout: Navbar, theme wrapper, toast container
|
||||
│ │ ├── +layout.ts # Root load: auth guard → redirect to /login if no token
|
||||
│ │ │
|
||||
│ │ ├── +page.svelte # / → redirect to /files
|
||||
│ │ │
|
||||
│ │ ├── login/
|
||||
│ │ │ └── +page.svelte # Login form (decorative Tanabata images)
|
||||
│ │ │
|
||||
│ │ ├── files/
|
||||
│ │ │ ├── +page.svelte # File grid: filter bar, sort, multi-select, upload
|
||||
│ │ │ ├── +page.ts # Load: initial file list (cursor page)
|
||||
│ │ │ ├── [id]/
|
||||
│ │ │ │ ├── +page.svelte # File view: preview, metadata, tags, ACL
|
||||
│ │ │ │ └── +page.ts # Load: file detail + tags
|
||||
│ │ │ └── trash/
|
||||
│ │ │ ├── +page.svelte # Trash: restore / permanent delete
|
||||
│ │ │ └── +page.ts
|
||||
│ │ │
|
||||
│ │ ├── tags/
|
||||
│ │ │ ├── +page.svelte # Tag list: search, sort, multi-select
|
||||
│ │ │ ├── +page.ts
|
||||
│ │ │ ├── new/
|
||||
│ │ │ │ └── +page.svelte # Create tag form
|
||||
│ │ │ └── [id]/
|
||||
│ │ │ ├── +page.svelte # Tag detail: edit, category, rules, parent tags
|
||||
│ │ │ └── +page.ts
|
||||
│ │ │
|
||||
│ │ ├── categories/
|
||||
│ │ │ ├── +page.svelte # Category list
|
||||
│ │ │ ├── +page.ts
|
||||
│ │ │ ├── new/
|
||||
│ │ │ │ └── +page.svelte
|
||||
│ │ │ └── [id]/
|
||||
│ │ │ ├── +page.svelte # Category detail: edit, view tags
|
||||
│ │ │ └── +page.ts
|
||||
│ │ │
|
||||
│ │ ├── pools/
|
||||
│ │ │ ├── +page.svelte # Pool list
|
||||
│ │ │ ├── +page.ts
|
||||
│ │ │ ├── new/
|
||||
│ │ │ │ └── +page.svelte
|
||||
│ │ │ └── [id]/
|
||||
│ │ │ ├── +page.svelte # Pool detail: files (reorderable), filter, edit
|
||||
│ │ │ └── +page.ts
|
||||
│ │ │
|
||||
│ │ ├── settings/
|
||||
│ │ │ ├── +page.svelte # Profile: name, password, active sessions
|
||||
│ │ │ └── +page.ts
|
||||
│ │ │
|
||||
│ │ └── admin/
|
||||
│ │ ├── +layout.svelte # Admin layout: restrict to is_admin
|
||||
│ │ ├── users/
|
||||
│ │ │ ├── +page.svelte # User management list
|
||||
│ │ │ ├── +page.ts
|
||||
│ │ │ └── [id]/
|
||||
│ │ │ ├── +page.svelte # User detail: role, block/unblock
|
||||
│ │ │ └── +page.ts
|
||||
│ │ └── audit/
|
||||
│ │ ├── +page.svelte # Audit log with filters
|
||||
│ │ └── +page.ts
|
||||
│ │
|
||||
│ └── service-worker.ts # PWA: offline cache for pinned files, app shell caching
|
||||
│
|
||||
└── static/
|
||||
├── favicon.png
|
||||
├── favicon.ico
|
||||
├── manifest.webmanifest # PWA manifest (name, icons, theme_color)
|
||||
├── images/
|
||||
│ ├── tanabata-left.png # Login page decorations (from current design)
|
||||
│ ├── tanabata-right.png
|
||||
│ └── icons/ # PWA icons (192×192, 512×512, etc.)
|
||||
└── fonts/
|
||||
└── Epilogue-VariableFont_wght.ttf
|
||||
```
|
||||
|
||||
## Key Architecture Decisions
|
||||
|
||||
### CSS Hybrid: Tailwind + Custom Properties
|
||||
|
||||
Theme colors defined as CSS custom properties in `app.css`:
|
||||
|
||||
```css
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--color-bg-primary: #312F45;
|
||||
--color-bg-secondary: #181721;
|
||||
--color-bg-elevated: #111118;
|
||||
--color-accent: #9592B5;
|
||||
--color-accent-hover: #7D7AA4;
|
||||
--color-text-primary: #f0f0f0;
|
||||
--color-text-muted: #9999AD;
|
||||
--color-danger: #DB6060;
|
||||
--color-info: #4DC7ED;
|
||||
--color-warning: #F5E872;
|
||||
--color-tag-default: #444455;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
--color-bg-primary: #f5f5f5;
|
||||
--color-bg-secondary: #ffffff;
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
Tailwind references them in `tailwind.config.ts`:
|
||||
|
||||
```ts
|
||||
export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
bg: {
|
||||
primary: 'var(--color-bg-primary)',
|
||||
secondary: 'var(--color-bg-secondary)',
|
||||
elevated: 'var(--color-bg-elevated)',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'var(--color-accent)',
|
||||
hover: 'var(--color-accent-hover)',
|
||||
},
|
||||
// ...
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Epilogue', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
darkMode: 'class', // controlled via data-theme attribute
|
||||
};
|
||||
```
|
||||
|
||||
Usage in components: `<div class="bg-bg-primary text-text-primary rounded-xl p-4">`.
|
||||
Complex cases use scoped `<style>` inside `.svelte` files.
|
||||
|
||||
### API Client Pattern
|
||||
|
||||
`client.ts` — thin wrapper around fetch:
|
||||
|
||||
```ts
|
||||
// $lib/api/client.ts
|
||||
import { authStore } from '$lib/stores/auth';
|
||||
|
||||
const BASE = '/api/v1';
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const token = get(authStore).accessToken;
|
||||
const res = await fetch(BASE + path, {
|
||||
...init,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
...init?.headers,
|
||||
},
|
||||
});
|
||||
if (res.status === 401) {
|
||||
// attempt refresh, retry once
|
||||
}
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new ApiError(res.status, err.code, err.message, err.details);
|
||||
}
|
||||
if (res.status === 204) return undefined as T;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(path: string) => request<T>(path),
|
||||
post: <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, { method: 'POST', body: JSON.stringify(body) }),
|
||||
patch: <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
put: <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, { method: 'PUT', body: JSON.stringify(body) }),
|
||||
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
||||
upload: <T>(path: string, formData: FormData) =>
|
||||
request<T>(path, { method: 'POST', body: formData, headers: {} }),
|
||||
};
|
||||
```
|
||||
|
||||
Domain-specific modules use it:
|
||||
|
||||
```ts
|
||||
// $lib/api/files.ts
|
||||
import { api } from './client';
|
||||
import type { File, FileCursorPage } from './types';
|
||||
|
||||
export function listFiles(params: Record<string, string>) {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
return api.get<FileCursorPage>(`/files?${qs}`);
|
||||
}
|
||||
|
||||
export function uploadFile(formData: FormData) {
|
||||
return api.upload<File>('/files', formData);
|
||||
}
|
||||
```
|
||||
|
||||
### Type Generation
|
||||
|
||||
Script in `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"generate:types": "openapi-typescript ../openapi.yaml -o src/lib/api/schema.ts",
|
||||
"dev": "npm run generate:types && vite dev",
|
||||
"build": "npm run generate:types && vite build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Friendly aliases in `types.ts`:
|
||||
|
||||
```ts
|
||||
import type { components } from './schema';
|
||||
|
||||
export type File = components['schemas']['File'];
|
||||
export type Tag = components['schemas']['Tag'];
|
||||
export type Category = components['schemas']['Category'];
|
||||
export type Pool = components['schemas']['Pool'];
|
||||
export type FileCursorPage = components['schemas']['FileCursorPage'];
|
||||
export type TagOffsetPage = components['schemas']['TagOffsetPage'];
|
||||
export type Error = components['schemas']['Error'];
|
||||
// ...
|
||||
```
|
||||
|
||||
### SPA Mode
|
||||
|
||||
`svelte.config.js`:
|
||||
|
||||
```js
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
|
||||
export default {
|
||||
kit: {
|
||||
adapter: adapter({ fallback: 'index.html' }),
|
||||
// SPA: all routes handled client-side
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The Go backend serves `index.html` for all non-API routes (SPA fallback).
|
||||
In development, Vite dev server proxies `/api` to the Go backend.
|
||||
|
||||
### PWA
|
||||
|
||||
`service-worker.ts` handles:
|
||||
- App shell caching (HTML, CSS, JS, fonts)
|
||||
- User-pinned file caching (explicit, via UI button)
|
||||
- Cache versioning and cleanup on update
|
||||
- Reset function (clear all caches except pinned files)
|
||||
@@ -0,0 +1,320 @@
|
||||
# Tanabata File Manager — Go Project Structure
|
||||
|
||||
## Stack
|
||||
|
||||
- **Router**: Gin
|
||||
- **Database**: pgx v5 (pgxpool)
|
||||
- **Migrations**: goose v3 + go:embed (auto-migrate on startup)
|
||||
- **Auth**: JWT (golang-jwt/jwt/v5)
|
||||
- **Config**: environment variables via .env (joho/godotenv)
|
||||
- **Logging**: slog (stdlib, Go 1.21+)
|
||||
- **Validation**: go-playground/validator/v10
|
||||
- **EXIF**: rwcarlsen/goexif or dsoprea/go-exif
|
||||
- **Image processing**: disintegration/imaging (thumbnails, previews)
|
||||
- **Architecture**: Clean Architecture (domain → service → repository/handler)
|
||||
|
||||
## Monorepo Layout
|
||||
|
||||
```
|
||||
tanabata/
|
||||
├── backend/ ← Go project
|
||||
├── frontend/ ← SvelteKit project
|
||||
├── openapi.yaml ← Shared API contract
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile
|
||||
├── .env.example
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Backend Directory Layout
|
||||
|
||||
```
|
||||
backend/
|
||||
├── cmd/
|
||||
│ └── server/
|
||||
│ └── main.go # Entrypoint: config → DB → migrate → wire → run
|
||||
│
|
||||
├── internal/
|
||||
│ │
|
||||
│ ├── domain/ # Pure business entities & value objects
|
||||
│ │ ├── file.go # File, FileFilter, FilePage
|
||||
│ │ ├── tag.go # Tag, TagRule
|
||||
│ │ ├── category.go # Category
|
||||
│ │ ├── pool.go # Pool, PoolFile
|
||||
│ │ ├── user.go # User, Session
|
||||
│ │ ├── acl.go # Permission, ObjectType
|
||||
│ │ ├── audit.go # AuditEntry, ActionType
|
||||
│ │ └── errors.go # Domain error types (ErrNotFound, ErrForbidden, etc.)
|
||||
│ │
|
||||
│ ├── port/ # Interfaces (ports) — contracts between layers
|
||||
│ │ ├── repository.go # FileRepo, TagRepo, CategoryRepo, PoolRepo,
|
||||
│ │ │ # UserRepo, SessionRepo, ACLRepo, AuditRepo,
|
||||
│ │ │ # MimeRepo, TagRuleRepo
|
||||
│ │ └── storage.go # FileStorage interface (disk operations)
|
||||
│ │
|
||||
│ ├── service/ # Business logic (use cases)
|
||||
│ │ ├── file_service.go # Upload, update, delete, trash/restore, replace,
|
||||
│ │ │ # import, filter/list, duplicate detection
|
||||
│ │ ├── tag_service.go # CRUD + auto-tag application logic
|
||||
│ │ ├── category_service.go # CRUD (thin, delegates to repo + ACL + audit)
|
||||
│ │ ├── pool_service.go # CRUD + file ordering, add/remove files
|
||||
│ │ ├── auth_service.go # Login, logout, JWT issue/refresh, session management
|
||||
│ │ ├── acl_service.go # Permission checks, grant/revoke
|
||||
│ │ ├── audit_service.go # Log actions, query audit log
|
||||
│ │ └── user_service.go # Profile update, admin CRUD, block/unblock
|
||||
│ │
|
||||
│ ├── handler/ # HTTP layer (Gin handlers)
|
||||
│ │ ├── router.go # Route registration, middleware wiring
|
||||
│ │ ├── middleware.go # Auth middleware (JWT extraction → context)
|
||||
│ │ ├── request.go # Common request parsing helpers
|
||||
│ │ ├── response.go # Error/success response builders
|
||||
│ │ ├── file_handler.go # /files endpoints
|
||||
│ │ ├── tag_handler.go # /tags endpoints
|
||||
│ │ ├── category_handler.go # /categories endpoints
|
||||
│ │ ├── pool_handler.go # /pools endpoints
|
||||
│ │ ├── auth_handler.go # /auth endpoints
|
||||
│ │ ├── acl_handler.go # /acl endpoints
|
||||
│ │ ├── user_handler.go # /users endpoints
|
||||
│ │ └── audit_handler.go # /audit endpoints
|
||||
│ │
|
||||
│ ├── db/ # Database adapters
|
||||
│ │ ├── db.go # Common helpers: pagination, repo factory, transactor base
|
||||
│ │ └── postgres/ # PostgreSQL implementation
|
||||
│ │ ├── postgres.go # pgxpool init, tx-from-context helpers
|
||||
│ │ ├── file_repo.go # FileRepo implementation
|
||||
│ │ ├── tag_repo.go # TagRepo + TagRuleRepo implementation
|
||||
│ │ ├── category_repo.go # CategoryRepo implementation
|
||||
│ │ ├── pool_repo.go # PoolRepo implementation
|
||||
│ │ ├── user_repo.go # UserRepo implementation
|
||||
│ │ ├── session_repo.go # SessionRepo implementation
|
||||
│ │ ├── acl_repo.go # ACLRepo implementation
|
||||
│ │ ├── audit_repo.go # AuditRepo implementation
|
||||
│ │ ├── mime_repo.go # MimeRepo implementation
|
||||
│ │ └── filter_parser.go # DSL → SQL WHERE clause builder
|
||||
│ │
|
||||
│ ├── storage/ # File storage adapter
|
||||
│ │ └── disk.go # FileStorage implementation (read/write/delete on disk)
|
||||
│ │
|
||||
│ └── config/ # Configuration
|
||||
│ └── config.go # Struct + loader from env vars
|
||||
│
|
||||
├── migrations/ # SQL migration files (goose format)
|
||||
│ ├── 001_init_schemas.sql
|
||||
│ ├── 002_core_tables.sql
|
||||
│ ├── 003_data_tables.sql
|
||||
│ ├── 004_acl_tables.sql
|
||||
│ ├── 005_activity_tables.sql
|
||||
│ ├── 006_indexes.sql
|
||||
│ └── 007_seed_data.sql
|
||||
│
|
||||
├── go.mod
|
||||
└── go.sum
|
||||
```
|
||||
|
||||
## Layer Dependency Rules
|
||||
|
||||
```
|
||||
handler → service → port (interfaces) ← db/postgres / storage
|
||||
↓
|
||||
domain (entities, value objects, errors)
|
||||
```
|
||||
|
||||
- **domain/**: zero imports from other internal packages. Only stdlib.
|
||||
- **port/**: imports only domain/. Defines interfaces.
|
||||
- **service/**: imports domain/ and port/. Never imports db/ or handler/.
|
||||
- **handler/**: imports domain/ and service/. Never imports db/.
|
||||
- **db/postgres/**: imports domain/, port/, and db/ (common helpers). Implements port interfaces.
|
||||
- **db/**: imports domain/ and port/. Shared utilities for all DB adapters.
|
||||
- **storage/**: imports domain/ and port/. Implements FileStorage.
|
||||
|
||||
No layer may import a layer above it. No circular dependencies.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Dependency Injection (Wiring)
|
||||
|
||||
Manual wiring in `cmd/server/main.go`. No DI frameworks.
|
||||
|
||||
```go
|
||||
// Pseudocode
|
||||
pool := postgres.NewPool(cfg.DatabaseURL)
|
||||
goose.Up(pool, migrations)
|
||||
|
||||
// Repos (all from internal/db/postgres/)
|
||||
fileRepo := postgres.NewFileRepo(pool)
|
||||
tagRepo := postgres.NewTagRepo(pool)
|
||||
// ...
|
||||
|
||||
// Storage
|
||||
diskStore := storage.NewDiskStorage(cfg.FilesPath)
|
||||
|
||||
// Services
|
||||
aclSvc := service.NewACLService(aclRepo, objectTypeRepo)
|
||||
auditSvc := service.NewAuditService(auditRepo, actionTypeRepo)
|
||||
fileSvc := service.NewFileService(fileRepo, mimeRepo, tagRepo, diskStore, aclSvc, auditSvc)
|
||||
tagSvc := service.NewTagService(tagRepo, tagRuleRepo, aclSvc, auditSvc)
|
||||
// ...
|
||||
|
||||
// Handlers
|
||||
fileHandler := handler.NewFileHandler(fileSvc, tagSvc)
|
||||
// ...
|
||||
|
||||
router := handler.NewRouter(cfg, fileHandler, tagHandler, ...)
|
||||
router.Run(cfg.ListenAddr)
|
||||
```
|
||||
|
||||
### Context Propagation
|
||||
|
||||
Every service method receives `context.Context` as the first argument.
|
||||
The handler extracts user info from JWT (via middleware) and puts it
|
||||
into context. Services read the current user from context for ACL checks
|
||||
and audit logging.
|
||||
|
||||
```go
|
||||
// middleware.go
|
||||
func (m *AuthMiddleware) Handle(c *gin.Context) {
|
||||
claims := parseJWT(c.GetHeader("Authorization"))
|
||||
ctx := domain.WithUser(c.Request.Context(), claims.UserID, claims.IsAdmin)
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// domain/context.go
|
||||
type ctxKey int
|
||||
const userKey ctxKey = iota
|
||||
func WithUser(ctx context.Context, userID int16, isAdmin bool) context.Context { ... }
|
||||
func UserFromContext(ctx context.Context) (userID int16, isAdmin bool) { ... }
|
||||
```
|
||||
|
||||
### Transaction Management
|
||||
|
||||
Repository interfaces include a `Transactor`:
|
||||
|
||||
```go
|
||||
// port/repository.go
|
||||
type Transactor interface {
|
||||
WithTx(ctx context.Context, fn func(ctx context.Context) error) error
|
||||
}
|
||||
```
|
||||
|
||||
The postgres implementation wraps `pgxpool.Pool.BeginTx`. Inside `fn`,
|
||||
all repo calls use the transaction from context. This allows services
|
||||
to compose multiple repo calls in a single transaction:
|
||||
|
||||
```go
|
||||
// service/file_service.go
|
||||
func (s *FileService) Upload(ctx context.Context, input UploadInput) (*domain.File, error) {
|
||||
return s.tx.WithTx(ctx, func(ctx context.Context) error {
|
||||
file, err := s.fileRepo.Create(ctx, ...) // uses tx
|
||||
if err != nil { return err }
|
||||
for _, tagID := range input.TagIDs {
|
||||
s.tagRepo.AddFileTag(ctx, file.ID, tagID) // same tx
|
||||
}
|
||||
s.auditRepo.Log(ctx, ...) // same tx
|
||||
return nil
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### ACL Check Pattern
|
||||
|
||||
ACL logic is centralized in `ACLService`. Other services call it before
|
||||
any data mutation or retrieval:
|
||||
|
||||
```go
|
||||
// service/acl_service.go
|
||||
func (s *ACLService) CanView(ctx context.Context, objectType string, objectID uuid.UUID) error {
|
||||
userID, isAdmin := domain.UserFromContext(ctx)
|
||||
if isAdmin { return nil }
|
||||
// Check is_public on the object
|
||||
// If not public, check creator_id == userID
|
||||
// If not creator, check acl.permissions
|
||||
// Return domain.ErrForbidden if none match
|
||||
}
|
||||
```
|
||||
|
||||
### Error Mapping
|
||||
|
||||
Domain errors → HTTP status codes (handled in handler/response.go):
|
||||
|
||||
| Domain Error | HTTP Status | Error Code |
|
||||
|-----------------------|-------------|-------------------|
|
||||
| ErrNotFound | 404 | not_found |
|
||||
| ErrForbidden | 403 | forbidden |
|
||||
| ErrUnauthorized | 401 | unauthorized |
|
||||
| ErrConflict | 409 | conflict |
|
||||
| ErrValidation | 400 | validation_error |
|
||||
| ErrUnsupportedMIME | 415 | unsupported_mime |
|
||||
| (unexpected) | 500 | internal_error |
|
||||
|
||||
### Filter DSL
|
||||
|
||||
The DSL parser lives in `db/postgres/filter_parser.go` because it produces
|
||||
SQL WHERE clauses — it is a PostgreSQL-specific adapter concern.
|
||||
The service layer passes the raw DSL string to the repository; the
|
||||
repository parses it and builds the query.
|
||||
|
||||
For a different DBMS, a corresponding parser would live in
|
||||
`db/<dbms>/filter_parser.go`.
|
||||
|
||||
The interface:
|
||||
```go
|
||||
// port/repository.go
|
||||
type FileRepo interface {
|
||||
List(ctx context.Context, params FileListParams) (*domain.FilePage, error)
|
||||
// ...
|
||||
}
|
||||
|
||||
// domain/file.go
|
||||
type FileListParams struct {
|
||||
Filter string // raw DSL string
|
||||
Sort string
|
||||
Order string
|
||||
Cursor string
|
||||
Anchor *uuid.UUID
|
||||
Direction string // "forward" or "backward"
|
||||
Limit int
|
||||
Trash bool
|
||||
Search string
|
||||
}
|
||||
```
|
||||
|
||||
### JWT Structure
|
||||
|
||||
```go
|
||||
type Claims struct {
|
||||
jwt.RegisteredClaims
|
||||
UserID int16 `json:"uid"`
|
||||
IsAdmin bool `json:"adm"`
|
||||
SessionID int `json:"sid"`
|
||||
}
|
||||
```
|
||||
|
||||
Access token: short-lived (15 min). Refresh token: long-lived (30 days),
|
||||
stored as hash in `activity.sessions.token_hash`.
|
||||
|
||||
### Configuration (.env)
|
||||
|
||||
```env
|
||||
# Server
|
||||
LISTEN_ADDR=:8080
|
||||
JWT_SECRET=<random-32-bytes>
|
||||
JWT_ACCESS_TTL=15m
|
||||
JWT_REFRESH_TTL=720h
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgres://user:pass@host:5432/tanabata?sslmode=disable
|
||||
|
||||
# Storage
|
||||
FILES_PATH=/data/files
|
||||
THUMBS_CACHE_PATH=/data/thumbs
|
||||
|
||||
# Thumbnails
|
||||
THUMB_WIDTH=160
|
||||
THUMB_HEIGHT=160
|
||||
PREVIEW_WIDTH=1920
|
||||
PREVIEW_HEIGHT=1080
|
||||
|
||||
# Import
|
||||
IMPORT_PATH=/data/import
|
||||
```
|
||||
@@ -1,56 +0,0 @@
|
||||
# Tanabata File Manager
|
||||
|
||||
## Usage
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
Build the CLI app using `./build.sh -t tfm [-b <build_dir>]`. For better experience, you can move the executable to the `/usr/bin/` directory (totally safe unless you have another app named `tfm`) or add the directory with it to `PATH`.
|
||||
|
||||
Then just open the terminal and run `tfm -h`. If you are running it for the first time, run it with `sudo` or manually create the `/etc/tanabata/` directory and check its permissions. This is the directory where Tanabata programs store their configuration files. If everything is set up properly, you should get the following help message.
|
||||
|
||||
```
|
||||
(C) Masahiko AMANO aka H1K0, 2022—present
|
||||
(https://github.com/H1K0/tanabata)
|
||||
|
||||
Usage:
|
||||
tfm <options>
|
||||
|
||||
Options:
|
||||
-h Print this help and exit
|
||||
-I <dir> Initialize new Tanabata database in directory <dir>
|
||||
-O <dir> Open existing Tanabata database from directory <dir>
|
||||
-i View database info
|
||||
-s Set or add
|
||||
-u Unset or remove
|
||||
-e Edit or update
|
||||
-f <sasa_id or path> File-sasa menu
|
||||
-t <tanzaku_id or name> Tanzaku menu
|
||||
-c <sasa_id>-<tanzaku_id> Kazari menu (can only be used with the '-s' or '-u' option)
|
||||
-w Weed (defragment) database
|
||||
|
||||
No database connected
|
||||
```
|
||||
|
||||
So, let's take a look at each option.
|
||||
|
||||
Using the `-I <dir>` option, you can initialize an empty TFM database in the specified directory. The app creates empty sasahyou, sappyou and shoppyou files and saves the directory path to the configuration file. The new database will be used the next time you run the app until you change it.
|
||||
|
||||
Using the `-O <dir>` option, you can open an existing TFM database in the specified directory. The app checks if the directory contains sasahyou, sappyou and shoppyou files, and if they exist and are valid, saves the directory path to the configuration file. The new database will be used the next time you run the app until you change it.
|
||||
|
||||
Using the `-i` option, you can get info about your database. When your hyous were created and last modified, how many records and holes they have, and so on.
|
||||
|
||||
Using the `-s` option, you can add new sasa, tanzaku, or kazari.
|
||||
|
||||
Using the `-u` option, you can remove sasa, tanzaku, or kazari.
|
||||
|
||||
Using the `-e` option, you can update sasa file path or tanzaku name or description. If you want to keep the current value of a field (for example, if you want to change the description of tanzaku while keeping its name), just leave its line blank.
|
||||
|
||||
Using the `-f` option, you can manage your sasa. It takes sasa ID when used alone or with the `-u` or `-e` option or target file path when used with the `-s` option. If you want to view the list of all sasa, pass `.` as an argument. For example, `tfm -f 2d` prints the info about sasa with ID `2d` and `tfm -sf path/to/file` adds a new file to the database.
|
||||
|
||||
Using the `-t` option, you can manage your tanzaku. It takes tanzaku ID when used alone or with the `-u` or `-e` option or the name of new tanzaku when used with the `-s` option. If you want to view the list of all tanzaku, pass `.` as an argument. For example, `tfm -t c4` prints the info about sasa with ID `c4` and `tfm -st "New tag name"` adds a new tanzaku to the database.
|
||||
|
||||
The `-c` option can be used only with the `-s` or `-u` option. It takes the IDs of sasa and tanzaku to link/unlink separated with a hyphen. For example, `tfm -sc 10-4d` links sasa with ID `10` and tanzaku with ID `4d`.
|
||||
|
||||
Using the `-w` option, you can _weed_ the database. It's like defragmentation. For example, if you had 4 files with sasa IDs 0, 1, 2, 3 in your database and removed the 1st one, then your database would only have sasa IDs 0, 2, 3 and ID 1 would be a _hole_. Weeding fixes this hole by changing sasa ID 2 to 1, 3 to 2, and updating all related kazari, so for large databases this can take a while.
|
||||
|
||||
Using the `-V` option, you just get the current version of TFM.
|
||||
@@ -0,0 +1,148 @@
|
||||
## О проекте
|
||||
|
||||
Tanabata File Manager или сокращенно TFM — многопользовательский веб-файловый менеджер, организующий файлы по тегам. Работает на клиент-серверной архитектуре, управляется через веб-интерфейс. Главная цель проекта — обеспечить централизованное хранение файлов на сервере, доступ к ним и управление ими через веб как с компьютера, так и со смартфона. В первую очередь данное приложение ориентировано на изображения и видео.
|
||||
|
||||
## Общая архитектура
|
||||
|
||||
- File storage
|
||||
- Relational database (PostgreSQL)
|
||||
- REST API service (Go)
|
||||
- Frontend (SvelteKit)
|
||||
|
||||
Приложение предполагается разворачивать внутри контейнера Docker. Фронтенд и бэкенд - в одном контейнере, СУБД - отдельно (на моем сервере планируется подключать к СУБД на хосте). Все файлы, управляемые Танабатой, будут храниться кучей в одной папке. Имя файла на диске совпадает с его UUID в БД.
|
||||
|
||||
Приложение является PWA, которое можно установить на компьютер или смартфон.
|
||||
|
||||
В будущих версиях планируется введение поддержки других СУБД.
|
||||
|
||||
## Основные понятия
|
||||
|
||||
**Файл** — один файл на сервере. Может иметь сколько угодно тегов, может принадлежать скольким угодно пулам. Имеет автора, а также может иметь настройки доступа (пользователь (может быть null - таким образом можно делать файл публичным), флаг права на чтение, флаг права на изменение). Имеет оригинальное название и метаданные (ключ-значение, в том числе все данные EXIF).
|
||||
|
||||
**Тег** — метка файла. Может быть привязан к скольким угодно файлам, может быть привязан к одной категории. Имеет название, описание, метаданные (ключ-значение). Может иметь автотеги.
|
||||
|
||||
**Автотег** — правило, согласно которому при привязке к файлу условного тега А к этому же файлу автоматически привязывается условный тег Б.
|
||||
|
||||
**Категория** — сущность, логически объединяющая собой несколько тегов. Имеет название, описание, метаданные (ключ-значение).
|
||||
|
||||
**Пул** — логическое объединение файлов. Имеет название, описание, метаданные (ключ-значение). Файлы внутри могут быть как отсортированы автоматически, так и расположены в порядке, заданном пользователем вручную.
|
||||
|
||||
## Функциональные требования
|
||||
|
||||
1. Управление файлами
|
||||
1. Просмотр списка файлов (lazy load, pagination)
|
||||
2. Фильтрация файлов по тегам и метаданным
|
||||
3. Просмотр и редактирование настроек сортировки (сохраняется для каждого пользователя)
|
||||
4. Выделение нескольких файлов (Ctrl, Shift) и действия с ними
|
||||
1. Привязка/отвязка тегов
|
||||
2. Копирование/вставка тегов
|
||||
3. Добавление в пул
|
||||
4. Просмотр и редактирование настроек доступа
|
||||
5. Удаление (с запросом подтверждения)
|
||||
5. Просмотр одного файла
|
||||
6. Действия с одним файлом
|
||||
1. Привязка/отвязка тегов
|
||||
2. Копирование/вставка тегов
|
||||
3. Добавление в пул
|
||||
4. Просмотр и редактирование настроек доступа
|
||||
5. Замена файла (загрузка нового под таким же ID)
|
||||
6. Удаление (с запросом подтверждения)
|
||||
7. Листание файлов, как в галерее
|
||||
8. Загрузка новых файлов через веб-интерфейс (через форму или drag-n-drop прямо на список)
|
||||
9. Импорт новых файлов из папки на сервере
|
||||
10. Выявление дубликатов, в частности, изображений и видео
|
||||
1. Отображение групп дубликатов
|
||||
2. Возможность отвязывания фальшивых дубликатов (чтобы приложение запомнило, что изображение А не является дубликатом изображения Б)
|
||||
3. Возможность выбора дубликата для удаления/сохранения
|
||||
4. Возможность выбора, какие поля от какого дубликата подтягивать
|
||||
11. Корзина
|
||||
1. Просмотр файлов в корзине
|
||||
2. Восстановление из корзины
|
||||
3. Окончательное удаление
|
||||
2. Управление тегами
|
||||
1. Просмотр списка тегов (lazy load, pagination)
|
||||
2. Поиск по названию
|
||||
3. Просмотр и редактирование настроек сортировки (сохраняется для каждого пользователя)
|
||||
4. Выделение нескольких тегов (Ctrl, Shift) и действия с ними
|
||||
1. Назначение автотегов
|
||||
2. Изменение категории
|
||||
3. Удаление (с запросом подтверждения)
|
||||
5. Просмотр одного тега
|
||||
6. Действия с одним тегом
|
||||
1. Редактирование названия, описания и метаданных (ключ-значение)
|
||||
2. Изменение категории
|
||||
3. Назначение автотегов
|
||||
4. Удаление (с запросом подтверждения)
|
||||
7. Создание тега
|
||||
1. Внесение названия, описания и метаданных (ключ-значение)
|
||||
2. Назначение категории (опционально)
|
||||
3. Назначение автотегов
|
||||
3. Управление категориями
|
||||
1. Просмотр списка категорий (lazy load, pagination)
|
||||
2. Поиск по названию
|
||||
3. Просмотр и редактирование настроек сортировки (сохраняется для каждого пользователя)
|
||||
4. Выделение нескольких категорий (Ctrl, Shift) и действия с ними
|
||||
1. Просмотр привязанных общих тегов и тегов, привязанных к некоторым, но не ко всем
|
||||
2. Привязка/отвязка тегов
|
||||
3. Удаление (с запросом подтверждения)
|
||||
5. Просмотр одной категории
|
||||
6. Действия с одной категорией
|
||||
1. Редактирование названия, описания и метаданных (ключ-значение)
|
||||
2. Просмотр привязанных тегов
|
||||
3. Привязка/отвязка тегов
|
||||
4. Удаление (с запросом подтверждения)
|
||||
7. Создание категории
|
||||
1. Внесение названия, описания и метаданных (ключ-значение)
|
||||
2. Привязка тегов
|
||||
4. Управление пулами
|
||||
1. Просмотр списка пулов (lazy load, pagination)
|
||||
2. Поиск по названию
|
||||
3. Просмотр и редактирование настроек сортировки (сохраняется для каждого пользователя)
|
||||
4. Выделение нескольких пулов (Ctrl, Shift) и действия с ними
|
||||
1. Просмотр и редактирование настроек доступа
|
||||
2. Удаление (с запросом подтверждения)
|
||||
5. Просмотр одного пула
|
||||
6. Действия с одним пулом
|
||||
1. Редактирование названия, описания и метаданных (ключ-значение)
|
||||
2. Просмотр и редактирование настроек доступа
|
||||
3. Просмотр всех файлов, входящих в пул
|
||||
4. Фильтрация файлов по тегам
|
||||
5. Изменение настройки сортировки файлов (в том числе можно отключить автоматическую сортировку)
|
||||
6. Ручное изменение порядка файлов (при отключенной сортировке)
|
||||
7. Удаление (с запросом подтверждения)
|
||||
7. Создание категории
|
||||
1. Внесение названия, описания и метаданных (ключ-значение)
|
||||
2. Привязка тегов
|
||||
5. Управление пользовательскими настройками
|
||||
1. Имя пользователя
|
||||
2. Пароль
|
||||
3. Сессии
|
||||
1. Завершение сессии
|
||||
4. Путь к папке на сервере, которая будет сканироваться при импорта файлов
|
||||
6. Управление настройками сервера (админка)
|
||||
1. Пользователи
|
||||
1. Просмотр списка
|
||||
2. Просмотр одного
|
||||
3. Создание
|
||||
4. Удаление
|
||||
5. Блокировка/разблокировка
|
||||
6. Установка роли (читатель/редактор)
|
||||
7. Журналирование пользовательских действий в БД
|
||||
1. Просмотры файлов
|
||||
2. Смены настроек доступа к файлам
|
||||
3. Создание/редактирование/удаление файла, тега, категории, пула, связи файл-тег
|
||||
4. Создание/блокировка/разблокировка/удаление пользователя
|
||||
5. Смена роли пользователя
|
||||
6. Авторизация/логаут пользователя
|
||||
7. Завершение сессии
|
||||
|
||||
## Нефункциональные требования
|
||||
|
||||
1. Интерфейс должен быть максимально простым и удобным, все необходимое должно быть под рукой, доступным за минимальное количество действий
|
||||
2. Интерфейс должен быть адаптирован под десктоп и под мобильные устройства
|
||||
3. Интерфейс должен иметь темную и светлую темы
|
||||
4. Использование технологии PWA (также должна быть кнопка, при нажатии которой PWA будет полностью сбрасываться (кроме кэша) и заново загружаться с сервера)
|
||||
5. Возможность сохранять некоторые файлы в кэш и просматривать их оффлайн при использовании установленного PWA
|
||||
6. При первичном запуске приложение должно требовать минимума действий: автоматическая миграция БД, заранее готовый файл docker compose, файл .env с настраиваемыми параметрами установки
|
||||
7. Использование подхода DDD для сервера API
|
||||
8. Не принимать файлы, чей MIME отсутствует в БД (нет в БД — нет поддержки)
|
||||
@@ -1,103 +0,0 @@
|
||||
// Tanabata file manager core names
|
||||
// By Masahiko AMANO aka H1K0
|
||||
|
||||
#pragma once
|
||||
#ifndef TANABATA_CORE_H
|
||||
#define TANABATA_CORE_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
extern "C" {
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#endif
|
||||
|
||||
// ==================== STRUCTS ==================== //
|
||||
|
||||
// Sasa (笹) - a file record
|
||||
typedef struct sasa {
|
||||
uint64_t id; // Sasa ID
|
||||
uint64_t created_ts; // Sasa creation timestamp
|
||||
char *path; // File path
|
||||
} Sasa;
|
||||
|
||||
// Tanzaku (短冊) - a tag record
|
||||
typedef struct tanzaku {
|
||||
uint64_t id; // Tanzaku ID
|
||||
uint64_t created_ts; // Tanzaku creation timestamp
|
||||
uint64_t modified_ts; // Tanzaku last modification timestamp
|
||||
char *name; // Tanzaku name
|
||||
char *description; // Tanzaku description
|
||||
} Tanzaku;
|
||||
|
||||
// Kazari (飾り) - a sasa-tanzaku association record
|
||||
typedef struct kazari {
|
||||
uint64_t sasa_id; // Sasa ID
|
||||
uint64_t tanzaku_id; // Tanzaku ID
|
||||
uint64_t created_ts; // Kazari creation timestamp
|
||||
} Kazari;
|
||||
|
||||
// Sasahyou (笹表) - database of sasa
|
||||
typedef struct sasahyou {
|
||||
uint64_t created_ts; // Sasahyou creation timestamp
|
||||
uint64_t modified_ts; // Sasahyou last modification timestamp
|
||||
uint64_t size; // Sasahyou size (including holes)
|
||||
Sasa *database; // Array of sasa
|
||||
uint64_t hole_cnt; // Number of holes
|
||||
Sasa **holes; // Array of pointers to holes
|
||||
FILE *file; // Storage file for sasahyou
|
||||
} Sasahyou;
|
||||
|
||||
// Sappyou (冊表) - database of tanzaku
|
||||
typedef struct sappyou {
|
||||
uint64_t created_ts; // Sappyou creation timestamp
|
||||
uint64_t modified_ts; // Sappyou last modification timestamp
|
||||
uint64_t size; // Sappyou size (including holes)
|
||||
Tanzaku *database; // Array of tanzaku
|
||||
uint64_t hole_cnt; // Number of holes
|
||||
Tanzaku **holes; // Array of pointers to holes
|
||||
FILE *file; // Storage file for sappyou
|
||||
} Sappyou;
|
||||
|
||||
// Shoppyou (飾表) - database of kazari
|
||||
typedef struct shoppyou {
|
||||
uint64_t created_ts; // Shoppyou creation timestamp
|
||||
uint64_t modified_ts; // Shoppyou last modification timestamp
|
||||
uint64_t size; // Shoppyou size (including holes)
|
||||
Kazari *database; // Array of kazari
|
||||
uint64_t hole_cnt; // Number of holes
|
||||
Kazari **holes; // Array of pointers to holes
|
||||
FILE *file; // Storage file for shoppyou
|
||||
} Shoppyou;
|
||||
|
||||
// Tanabata (七夕) - the struct with all databases
|
||||
typedef struct tanabata {
|
||||
Sasahyou sasahyou; // Sasahyou struct
|
||||
Sappyou sappyou; // Sappyou struct
|
||||
Shoppyou shoppyou; // Shoppyou struct
|
||||
uint64_t sasahyou_mod; // Sasahyou file last modificaton timestamp
|
||||
uint64_t sappyou_mod; // Sappyou file last modificaton timestamp
|
||||
uint64_t shoppyou_mod; // Shoppyou file last modificaton timestamp
|
||||
} Tanabata;
|
||||
|
||||
// ==================== CONSTANTS ==================== //
|
||||
|
||||
// ID of hole - an invalid record
|
||||
#define HOLE_ID (-1)
|
||||
|
||||
// Hole sasa constant with hole ID
|
||||
extern const Sasa HOLE_SASA;
|
||||
|
||||
// Hole tanzaku constant with hole ID
|
||||
extern const Tanzaku HOLE_TANZAKU;
|
||||
|
||||
// Hole kazari constant with hole ID
|
||||
extern const Kazari HOLE_KAZARI;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //TANABATA_CORE_H
|
||||
@@ -1,86 +0,0 @@
|
||||
// Tanabata lib
|
||||
// By Masahiko AMANO aka H1K0
|
||||
|
||||
#pragma once
|
||||
#ifndef TANABATA_H
|
||||
#define TANABATA_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
extern "C" {
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
#include "core.h"
|
||||
|
||||
// ==================== DATABASE SECTION ==================== //
|
||||
|
||||
// Initialize empty tanabata
|
||||
int tanabata_init(Tanabata *tanabata);
|
||||
|
||||
// Free tanabata
|
||||
int tanabata_free(Tanabata *tanabata);
|
||||
|
||||
// Weed tanabata
|
||||
int tanabata_weed(Tanabata *tanabata);
|
||||
|
||||
// Load tanabata
|
||||
int tanabata_load(Tanabata *tanabata);
|
||||
|
||||
// Save tanabata
|
||||
int tanabata_save(Tanabata *tanabata);
|
||||
|
||||
// Open tanabata
|
||||
int tanabata_open(Tanabata *tanabata, const char *path);
|
||||
|
||||
// Dump tanabata
|
||||
int tanabata_dump(Tanabata *tanabata, const char *path);
|
||||
|
||||
// ==================== SASA SECTION ==================== //
|
||||
|
||||
// Add sasa
|
||||
Sasa tanabata_sasa_add(Tanabata *tanabata, const char *path);
|
||||
|
||||
// Remove sasa by ID
|
||||
int tanabata_sasa_rem(Tanabata *tanabata, uint64_t sasa_id);
|
||||
|
||||
// Update sasa file path
|
||||
int tanabata_sasa_upd(Tanabata *tanabata, uint64_t sasa_id, const char *path);
|
||||
|
||||
// Get sasa by ID
|
||||
Sasa tanabata_sasa_get(Tanabata *tanabata, uint64_t sasa_id);
|
||||
|
||||
// ==================== TANZAKU SECTION ==================== //
|
||||
|
||||
// Add tanzaku
|
||||
Tanzaku tanabata_tanzaku_add(Tanabata *tanabata, const char *name, const char *description);
|
||||
|
||||
// Remove tanzaku by ID
|
||||
int tanabata_tanzaku_rem(Tanabata *tanabata, uint64_t tanzaku_id);
|
||||
|
||||
// Update tanzaku name and description
|
||||
int tanabata_tanzaku_upd(Tanabata *tanabata, uint64_t tanzaku_id, const char *name, const char *description);
|
||||
|
||||
// Get tanzaku by ID
|
||||
Tanzaku tanabata_tanzaku_get(Tanabata *tanabata, uint64_t tanzaku_id);
|
||||
|
||||
// ==================== KAZARI SECTION ==================== //
|
||||
|
||||
// Add kazari
|
||||
int tanabata_kazari_add(Tanabata *tanabata, uint64_t sasa_id, uint64_t tanzaku_id);
|
||||
|
||||
// Remove kazari
|
||||
int tanabata_kazari_rem(Tanabata *tanabata, uint64_t sasa_id, uint64_t tanzaku_id);
|
||||
|
||||
// Get tanzaku list of sasa
|
||||
Tanzaku *tanabata_tanzaku_get_by_sasa(Tanabata *tanabata, uint64_t sasa_id);
|
||||
|
||||
// Get sasa list of tanzaku
|
||||
Sasa *tanabata_sasa_get_by_tanzaku(Tanabata *tanabata, uint64_t tanzaku_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //TANABATA_H
|
||||
@@ -1,27 +0,0 @@
|
||||
// Tanabata DBMS client lib
|
||||
// By Masahiko AMANO aka H1K0
|
||||
|
||||
#pragma once
|
||||
#ifndef TANABATA_DBMS_CLIENT_H
|
||||
#define TANABATA_DBMS_CLIENT_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "tdbms.h"
|
||||
|
||||
// Connect to TDBMS server
|
||||
int tdbms_connect(const char *domain, const char *addr);
|
||||
|
||||
// Close connection to TDBMS server
|
||||
int tdbms_close(int socket_fd);
|
||||
|
||||
// Execute a TDB request
|
||||
char *tdb_query(int socket_fd, const char *db_name, char request_code, const char *request_body);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //TANABATA_DBMS_CLIENT_H
|
||||
@@ -1,58 +0,0 @@
|
||||
// Tanabata DBMS core names
|
||||
// By Masahiko AMANO aka H1K0
|
||||
|
||||
#pragma once
|
||||
#ifndef TANABATA_DBMS_H
|
||||
#define TANABATA_DBMS_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// ASCII End Of Transmission code
|
||||
#define EOT 4
|
||||
|
||||
// TDBMS request code bits
|
||||
enum TRC_BITS {
|
||||
trc_bit_remove = 0b1,
|
||||
trc_bit_add = 0b10,
|
||||
trc_bit_update = 0b100,
|
||||
trc_bit_kazari = 0b1000,
|
||||
trc_bit_sasa = 0b10000,
|
||||
trc_bit_tanzaku = 0b100000,
|
||||
};
|
||||
|
||||
// TDBMS request codes
|
||||
enum TRC {
|
||||
trc_db_stats = 0b0,
|
||||
trc_db_init = 0b11,
|
||||
trc_db_load = 0b10,
|
||||
trc_db_save = 0b100,
|
||||
trc_db_edit = 0b110,
|
||||
trc_db_remove_soft = 0b1,
|
||||
trc_db_remove_hard = 0b101,
|
||||
trc_db_weed = 0b111,
|
||||
trc_sasa_get = 0b10000,
|
||||
trc_sasa_get_by_tanzaku = 0b101000,
|
||||
trc_sasa_add = 0b10010,
|
||||
trc_sasa_update = 0b10100,
|
||||
trc_sasa_remove = 0b10001,
|
||||
trc_tanzaku_get = 0b100000,
|
||||
trc_tanzaku_get_by_sasa = 0b11000,
|
||||
trc_tanzaku_add = 0b100010,
|
||||
trc_tanzaku_update = 0b100100,
|
||||
trc_tanzaku_remove = 0b100001,
|
||||
trc_kazari_get = 0b1000,
|
||||
trc_kazari_add = 0b1010,
|
||||
trc_kazari_add_single_sasa_to_multiple_tanzaku = 0b11010,
|
||||
trc_kazari_add_single_tanzaku_to_multiple_sasa = 0b101010,
|
||||
trc_kazari_remove = 0b1001,
|
||||
trc_kazari_remove_single_sasa_to_multiple_tanzaku = 0b11001,
|
||||
trc_kazari_remove_single_tanzaku_to_multiple_sasa = 0b101001,
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //TANABATA_DBMS_H
|
||||
@@ -1,111 +0,0 @@
|
||||
// Tanabata file manager core functions
|
||||
// By Masahiko AMANO aka H1K0
|
||||
|
||||
#pragma once
|
||||
#ifndef TANABATA_CORE_FUNC_H
|
||||
#define TANABATA_CORE_FUNC_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
extern "C" {
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
#include "../../include/core.h"
|
||||
|
||||
// ==================== SASAHYOU SECTION ==================== //
|
||||
|
||||
// Initialize empty sasahyou
|
||||
int sasahyou_init(Sasahyou *sasahyou);
|
||||
|
||||
// Free sasahyou
|
||||
int sasahyou_free(Sasahyou *sasahyou);
|
||||
|
||||
// Load sasahyou from file
|
||||
int sasahyou_load(Sasahyou *sasahyou);
|
||||
|
||||
// Save sasahyou to file
|
||||
int sasahyou_save(Sasahyou *sasahyou);
|
||||
|
||||
// Open sasahyou file and load data from it
|
||||
int sasahyou_open(Sasahyou *sasahyou, const char *path);
|
||||
|
||||
// Dump sasahyou to file
|
||||
int sasahyou_dump(Sasahyou *sasahyou, const char *path);
|
||||
|
||||
// Add sasa to sasahyou
|
||||
Sasa sasa_add(Sasahyou *sasahyou, const char *path);
|
||||
|
||||
// Remove sasa from sasahyou
|
||||
int sasa_rem(Sasahyou *sasahyou, uint64_t sasa_id);
|
||||
|
||||
// Update sasa file path
|
||||
int sasa_upd(Sasahyou *sasahyou, uint64_t sasa_id, const char *path);
|
||||
|
||||
// ==================== SAPPYOU SECTION ==================== //
|
||||
|
||||
// Initialize empty sappyou
|
||||
int sappyou_init(Sappyou *sappyou);
|
||||
|
||||
// Free sappyou
|
||||
int sappyou_free(Sappyou *sappyou);
|
||||
|
||||
// Load sappyou from file
|
||||
int sappyou_load(Sappyou *sappyou);
|
||||
|
||||
// Save sappyou to file
|
||||
int sappyou_save(Sappyou *sappyou);
|
||||
|
||||
// Open sappyou file and load data from it
|
||||
int sappyou_open(Sappyou *sappyou, const char *path);
|
||||
|
||||
// Dump sappyou to file
|
||||
int sappyou_dump(Sappyou *sappyou, const char *path);
|
||||
|
||||
// Add new tanzaku to sappyou
|
||||
Tanzaku tanzaku_add(Sappyou *sappyou, const char *name, const char *description);
|
||||
|
||||
// Remove tanzaku from sappyou
|
||||
int tanzaku_rem(Sappyou *sappyou, uint64_t tanzaku_id);
|
||||
|
||||
// Update tanzaku name and description
|
||||
int tanzaku_upd(Sappyou *sappyou, uint64_t tanzaku_id, const char *name, const char *description);
|
||||
|
||||
// ==================== SHOPPYOU SECTION ==================== //
|
||||
|
||||
// Initialize empty shoppyou
|
||||
int shoppyou_init(Shoppyou *shoppyou);
|
||||
|
||||
// Free shoppyou
|
||||
int shoppyou_free(Shoppyou *shoppyou);
|
||||
|
||||
// Load shoppyou from file
|
||||
int shoppyou_load(Shoppyou *shoppyou);
|
||||
|
||||
// Save shoppyou to file
|
||||
int shoppyou_save(Shoppyou *shoppyou);
|
||||
|
||||
// Open shoppyou file and load data from it
|
||||
int shoppyou_open(Shoppyou *shoppyou, const char *path);
|
||||
|
||||
// Dump shoppyou to file
|
||||
int shoppyou_dump(Shoppyou *shoppyou, const char *path);
|
||||
|
||||
// Add kazari to shoppyou
|
||||
int kazari_add(Shoppyou *shoppyou, uint64_t sasa_id, uint64_t tanzaku_id);
|
||||
|
||||
// Remove kazari from shoppyou
|
||||
int kazari_rem(Shoppyou *shoppyou, uint64_t sasa_id, uint64_t tanzaku_id);
|
||||
|
||||
// Remove all kazari with a specific sasa ID from shoppyou
|
||||
int kazari_rem_by_sasa(Shoppyou *shoppyou, uint64_t sasa_id);
|
||||
|
||||
// Remove all kazari with a specific tanzaku ID from shoppyou
|
||||
int kazari_rem_by_tanzaku(Shoppyou *shoppyou, uint64_t tanzaku_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //TANABATA_CORE_FUNC_H
|
||||
@@ -1,222 +0,0 @@
|
||||
#include <stdint.h>
|
||||
#include <malloc.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "core_func.h"
|
||||
|
||||
const Tanzaku HOLE_TANZAKU = {HOLE_ID, 0, 0, NULL, NULL};
|
||||
|
||||
// Sappyou file signature: 七夕冊表
|
||||
const uint16_t SAPPYOU_SIG[4] = {L'七', L'夕', L'冊', L'表'};
|
||||
|
||||
int sappyou_init(Sappyou *sappyou) {
|
||||
sappyou->created_ts = time(NULL);
|
||||
sappyou->modified_ts = sappyou->created_ts;
|
||||
sappyou->size = 0;
|
||||
sappyou->database = NULL;
|
||||
sappyou->hole_cnt = 0;
|
||||
sappyou->holes = NULL;
|
||||
sappyou->file = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sappyou_free(Sappyou *sappyou) {
|
||||
sappyou->created_ts = 0;
|
||||
sappyou->modified_ts = 0;
|
||||
sappyou->size = 0;
|
||||
sappyou->hole_cnt = 0;
|
||||
if (sappyou->database != NULL) {
|
||||
for (Tanzaku *current_tanzaku = sappyou->database + sappyou->size - 1;
|
||||
current_tanzaku >= sappyou->database; current_tanzaku--) {
|
||||
free(current_tanzaku->name);
|
||||
free(current_tanzaku->description);
|
||||
}
|
||||
free(sappyou->database);
|
||||
sappyou->database = NULL;
|
||||
}
|
||||
free(sappyou->holes);
|
||||
sappyou->holes = NULL;
|
||||
if (sappyou->file != NULL) {
|
||||
fclose(sappyou->file);
|
||||
sappyou->file = NULL;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sappyou_load(Sappyou *sappyou) {
|
||||
if (sappyou->file == NULL ||
|
||||
(sappyou->file = freopen(NULL, "rb", sappyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Sappyou temp;
|
||||
sappyou_init(&temp);
|
||||
temp.file = sappyou->file;
|
||||
uint16_t signature[4];
|
||||
if (fread(signature, 2, 4, temp.file) != 4 ||
|
||||
memcmp(signature, SAPPYOU_SIG, 8) != 0 ||
|
||||
fread(&temp.created_ts, 8, 1, temp.file) != 1 ||
|
||||
fread(&temp.modified_ts, 8, 1, temp.file) != 1 ||
|
||||
fread(&temp.size, 8, 1, temp.file) != 1 ||
|
||||
fread(&temp.hole_cnt, 8, 1, temp.file) != 1) {
|
||||
return 1;
|
||||
}
|
||||
temp.database = calloc(temp.size, sizeof(Tanzaku));
|
||||
temp.holes = calloc(temp.hole_cnt, sizeof(Tanzaku *));
|
||||
size_t max_string_len = SIZE_MAX;
|
||||
Tanzaku *current_tanzaku = temp.database;
|
||||
for (uint64_t i = 0, r = temp.hole_cnt; i < temp.size; i++, current_tanzaku++) {
|
||||
if (fgetc(temp.file) != 0) {
|
||||
current_tanzaku->id = i;
|
||||
if (fread(¤t_tanzaku->created_ts, 8, 1, temp.file) != 1 ||
|
||||
fread(¤t_tanzaku->modified_ts, 8, 1, temp.file) != 1 ||
|
||||
getdelim(¤t_tanzaku->name, &max_string_len, 0, temp.file) == -1 ||
|
||||
getdelim(¤t_tanzaku->description, &max_string_len, 0, temp.file) == -1) {
|
||||
temp.file = NULL;
|
||||
sappyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
current_tanzaku->id = HOLE_ID;
|
||||
if (r == 0) {
|
||||
temp.file = NULL;
|
||||
sappyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
r--;
|
||||
temp.holes[r] = current_tanzaku;
|
||||
}
|
||||
}
|
||||
if (fflush(temp.file) == 0) {
|
||||
sappyou->file = NULL;
|
||||
sappyou_free(sappyou);
|
||||
*sappyou = temp;
|
||||
return 0;
|
||||
}
|
||||
temp.file = NULL;
|
||||
sappyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sappyou_save(Sappyou *sappyou) {
|
||||
if (sappyou->file == NULL ||
|
||||
(sappyou->file = freopen(NULL, "wb", sappyou->file)) == NULL ||
|
||||
fwrite(SAPPYOU_SIG, 2, 4, sappyou->file) != 4 ||
|
||||
fwrite(&sappyou->created_ts, 8, 1, sappyou->file) != 1 ||
|
||||
fwrite(&sappyou->modified_ts, 8, 1, sappyou->file) != 1 ||
|
||||
fwrite(&sappyou->size, 8, 1, sappyou->file) != 1 ||
|
||||
fwrite(&sappyou->hole_cnt, 8, 1, sappyou->file) != 1 ||
|
||||
fflush(sappyou->file) != 0) {
|
||||
return 1;
|
||||
}
|
||||
Tanzaku *current_tanzaku = sappyou->database;
|
||||
for (uint64_t i = 0; i < sappyou->size; i++, current_tanzaku++) {
|
||||
if (current_tanzaku->id != HOLE_ID) {
|
||||
if (fputc(0xff, sappyou->file) == EOF ||
|
||||
fwrite(¤t_tanzaku->created_ts, 8, 1, sappyou->file) != 1 ||
|
||||
fwrite(¤t_tanzaku->modified_ts, 8, 1, sappyou->file) != 1 ||
|
||||
fputs(current_tanzaku->name, sappyou->file) == EOF ||
|
||||
fputc(0, sappyou->file) == EOF ||
|
||||
fputs(current_tanzaku->description, sappyou->file) == EOF ||
|
||||
fputc(0, sappyou->file) == EOF) {
|
||||
return 1;
|
||||
}
|
||||
} else if (fputc(0, sappyou->file) == EOF) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return fflush(sappyou->file);
|
||||
}
|
||||
|
||||
int sappyou_open(Sappyou *sappyou, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (sappyou->file == NULL && (sappyou->file = fopen(path, "rb")) == NULL ||
|
||||
sappyou->file != NULL && (sappyou->file = freopen(path, "rb", sappyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
return sappyou_load(sappyou);
|
||||
}
|
||||
|
||||
int sappyou_dump(Sappyou *sappyou, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (sappyou->file == NULL && (sappyou->file = fopen(path, "wb")) == NULL ||
|
||||
sappyou->file != NULL && (sappyou->file = freopen(path, "wb", sappyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
return sappyou_save(sappyou);
|
||||
}
|
||||
|
||||
Tanzaku tanzaku_add(Sappyou *sappyou, const char *name, const char *description) {
|
||||
if (name == NULL || description == NULL || sappyou->size == -1 && sappyou->hole_cnt == 0) {
|
||||
return HOLE_TANZAKU;
|
||||
}
|
||||
Tanzaku newbie;
|
||||
newbie.created_ts = time(NULL);
|
||||
newbie.modified_ts = newbie.created_ts;
|
||||
newbie.name = malloc(strlen(name) + 1);
|
||||
strcpy(newbie.name, name);
|
||||
newbie.description = malloc(strlen(description) + 1);
|
||||
strcpy(newbie.description, description);
|
||||
if (sappyou->hole_cnt > 0) {
|
||||
sappyou->hole_cnt--;
|
||||
Tanzaku **hole_ptr = sappyou->holes + sappyou->hole_cnt;
|
||||
newbie.id = *hole_ptr - sappyou->database;
|
||||
**hole_ptr = newbie;
|
||||
sappyou->holes = reallocarray(sappyou->holes, sappyou->hole_cnt, sizeof(Tanzaku *));
|
||||
} else {
|
||||
newbie.id = sappyou->size;
|
||||
sappyou->size++;
|
||||
sappyou->database = reallocarray(sappyou->database, sappyou->size, sizeof(Tanzaku));
|
||||
sappyou->database[newbie.id] = newbie;
|
||||
}
|
||||
sappyou->modified_ts = newbie.created_ts;
|
||||
return newbie;
|
||||
}
|
||||
|
||||
int tanzaku_rem(Sappyou *sappyou, uint64_t tanzaku_id) {
|
||||
if (tanzaku_id == HOLE_ID || tanzaku_id >= sappyou->size) {
|
||||
return 1;
|
||||
}
|
||||
Tanzaku *current_tanzaku = sappyou->database + tanzaku_id;
|
||||
if (current_tanzaku->id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
current_tanzaku->id = HOLE_ID;
|
||||
free(current_tanzaku->name);
|
||||
free(current_tanzaku->description);
|
||||
if (tanzaku_id == sappyou->size - 1) {
|
||||
sappyou->size--;
|
||||
sappyou->database = reallocarray(sappyou->database, sappyou->size, sizeof(Tanzaku));
|
||||
} else {
|
||||
sappyou->hole_cnt++;
|
||||
sappyou->holes = reallocarray(sappyou->holes, sappyou->hole_cnt, sizeof(Tanzaku *));
|
||||
sappyou->holes[sappyou->hole_cnt - 1] = current_tanzaku;
|
||||
}
|
||||
sappyou->modified_ts = time(NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tanzaku_upd(Sappyou *sappyou, uint64_t tanzaku_id, const char *name, const char *description) {
|
||||
if (tanzaku_id == HOLE_ID || tanzaku_id >= sappyou->size || name == NULL && description == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Tanzaku *current_tanzaku = sappyou->database + tanzaku_id;
|
||||
if (current_tanzaku->id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
if (name != NULL) {
|
||||
current_tanzaku->name = realloc(current_tanzaku->name, strlen(name) + 1);
|
||||
strcpy(current_tanzaku->name, name);
|
||||
}
|
||||
if (description != NULL) {
|
||||
current_tanzaku->description = realloc(current_tanzaku->description, strlen(description) + 1);
|
||||
strcpy(current_tanzaku->description, description);
|
||||
}
|
||||
sappyou->modified_ts = time(NULL);
|
||||
current_tanzaku->modified_ts = sappyou->modified_ts;
|
||||
return 0;
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
#include <stdint.h>
|
||||
#include <malloc.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "core_func.h"
|
||||
|
||||
const Sasa HOLE_SASA = {HOLE_ID, 0, NULL};
|
||||
|
||||
// Sasahyou file signature: 七夕笹表
|
||||
const uint16_t SASAHYOU_SIG[4] = {L'七', L'夕', L'笹', L'表'};
|
||||
|
||||
int sasahyou_init(Sasahyou *sasahyou) {
|
||||
sasahyou->created_ts = time(NULL);
|
||||
sasahyou->modified_ts = sasahyou->created_ts;
|
||||
sasahyou->size = 0;
|
||||
sasahyou->database = NULL;
|
||||
sasahyou->hole_cnt = 0;
|
||||
sasahyou->holes = NULL;
|
||||
sasahyou->file = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sasahyou_free(Sasahyou *sasahyou) {
|
||||
sasahyou->created_ts = 0;
|
||||
sasahyou->modified_ts = 0;
|
||||
sasahyou->size = 0;
|
||||
sasahyou->hole_cnt = 0;
|
||||
if (sasahyou->database != NULL) {
|
||||
for (Sasa *current_sasa = sasahyou->database + sasahyou->size - 1;
|
||||
current_sasa >= sasahyou->database; current_sasa--) {
|
||||
free(current_sasa->path);
|
||||
}
|
||||
free(sasahyou->database);
|
||||
sasahyou->database = NULL;
|
||||
}
|
||||
free(sasahyou->holes);
|
||||
sasahyou->holes = NULL;
|
||||
if (sasahyou->file != NULL) {
|
||||
fclose(sasahyou->file);
|
||||
sasahyou->file = NULL;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sasahyou_load(Sasahyou *sasahyou) {
|
||||
if (sasahyou->file == NULL ||
|
||||
(sasahyou->file = freopen(NULL, "rb", sasahyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Sasahyou temp;
|
||||
sasahyou_init(&temp);
|
||||
temp.file = sasahyou->file;
|
||||
uint16_t signature[4];
|
||||
if (fread(signature, 2, 4, temp.file) != 4 ||
|
||||
memcmp(signature, SASAHYOU_SIG, 8) != 0 ||
|
||||
fread(&temp.created_ts, 8, 1, temp.file) != 1 ||
|
||||
fread(&temp.modified_ts, 8, 1, temp.file) != 1 ||
|
||||
fread(&temp.size, 8, 1, temp.file) != 1 ||
|
||||
fread(&temp.hole_cnt, 8, 1, temp.file) != 1) {
|
||||
return 1;
|
||||
}
|
||||
temp.database = calloc(temp.size, sizeof(Sasa));
|
||||
temp.holes = calloc(temp.hole_cnt, sizeof(Sasa *));
|
||||
size_t max_path_len = SIZE_MAX;
|
||||
Sasa *current_sasa = temp.database;
|
||||
for (uint64_t i = 0, r = temp.hole_cnt; i < temp.size; i++, current_sasa++) {
|
||||
if (fgetc(temp.file) != 0) {
|
||||
current_sasa->id = i;
|
||||
if (fread(¤t_sasa->created_ts, 8, 1, temp.file) != 1 ||
|
||||
getdelim(¤t_sasa->path, &max_path_len, 0, temp.file) == -1) {
|
||||
temp.file = NULL;
|
||||
sasahyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
current_sasa->id = HOLE_ID;
|
||||
if (r == 0) {
|
||||
temp.file = NULL;
|
||||
sasahyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
r--;
|
||||
temp.holes[r] = current_sasa;
|
||||
}
|
||||
}
|
||||
if (fflush(temp.file) == 0) {
|
||||
sasahyou->file = NULL;
|
||||
sasahyou_free(sasahyou);
|
||||
*sasahyou = temp;
|
||||
return 0;
|
||||
}
|
||||
temp.file = NULL;
|
||||
sasahyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sasahyou_save(Sasahyou *sasahyou) {
|
||||
if (sasahyou->file == NULL ||
|
||||
(sasahyou->file = freopen(NULL, "wb", sasahyou->file)) == NULL ||
|
||||
fwrite(SASAHYOU_SIG, 2, 4, sasahyou->file) != 4 ||
|
||||
fwrite(&sasahyou->created_ts, 8, 1, sasahyou->file) != 1 ||
|
||||
fwrite(&sasahyou->modified_ts, 8, 1, sasahyou->file) != 1 ||
|
||||
fwrite(&sasahyou->size, 8, 1, sasahyou->file) != 1 ||
|
||||
fwrite(&sasahyou->hole_cnt, 8, 1, sasahyou->file) != 1 ||
|
||||
fflush(sasahyou->file) != 0) {
|
||||
return 1;
|
||||
}
|
||||
Sasa *current_sasa = sasahyou->database;
|
||||
for (uint64_t i = 0; i < sasahyou->size; i++, current_sasa++) {
|
||||
if (current_sasa->id != HOLE_ID) {
|
||||
if (fputc(0xff, sasahyou->file) == EOF ||
|
||||
fwrite(¤t_sasa->created_ts, 8, 1, sasahyou->file) != 1 ||
|
||||
fputs(current_sasa->path, sasahyou->file) == EOF ||
|
||||
fputc(0, sasahyou->file) == EOF) {
|
||||
return 1;
|
||||
}
|
||||
} else if (fputc(0, sasahyou->file) == EOF) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return fflush(sasahyou->file);
|
||||
}
|
||||
|
||||
int sasahyou_open(Sasahyou *sasahyou, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (sasahyou->file == NULL && (sasahyou->file = fopen(path, "rb")) == NULL ||
|
||||
sasahyou->file != NULL && (sasahyou->file = freopen(path, "rb", sasahyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
return sasahyou_load(sasahyou);
|
||||
}
|
||||
|
||||
int sasahyou_dump(Sasahyou *sasahyou, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (sasahyou->file == NULL && (sasahyou->file = fopen(path, "wb")) == NULL ||
|
||||
sasahyou->file != NULL && (sasahyou->file = freopen(path, "wb", sasahyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
return sasahyou_save(sasahyou);
|
||||
}
|
||||
|
||||
Sasa sasa_add(Sasahyou *sasahyou, const char *path) {
|
||||
if (path == NULL || sasahyou->size == -1 && sasahyou->hole_cnt == 0) {
|
||||
return HOLE_SASA;
|
||||
}
|
||||
Sasa newbie;
|
||||
newbie.created_ts = time(NULL);
|
||||
newbie.path = malloc(strlen(path) + 1);
|
||||
strcpy(newbie.path, path);
|
||||
if (sasahyou->hole_cnt > 0) {
|
||||
sasahyou->hole_cnt--;
|
||||
Sasa **hole_ptr = sasahyou->holes + sasahyou->hole_cnt;
|
||||
newbie.id = *hole_ptr - sasahyou->database;
|
||||
**hole_ptr = newbie;
|
||||
sasahyou->holes = reallocarray(sasahyou->holes, sasahyou->hole_cnt, sizeof(Sasa *));
|
||||
} else {
|
||||
newbie.id = sasahyou->size;
|
||||
sasahyou->size++;
|
||||
sasahyou->database = reallocarray(sasahyou->database, sasahyou->size, sizeof(Sasa));
|
||||
sasahyou->database[newbie.id] = newbie;
|
||||
}
|
||||
sasahyou->modified_ts = newbie.created_ts;
|
||||
return newbie;
|
||||
}
|
||||
|
||||
int sasa_rem(Sasahyou *sasahyou, uint64_t sasa_id) {
|
||||
if (sasa_id == HOLE_ID || sasa_id >= sasahyou->size) {
|
||||
return 1;
|
||||
}
|
||||
Sasa *current_sasa = sasahyou->database + sasa_id;
|
||||
if (current_sasa->id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
current_sasa->id = HOLE_ID;
|
||||
free(current_sasa->path);
|
||||
current_sasa->path = NULL;
|
||||
if (sasa_id == sasahyou->size - 1) {
|
||||
sasahyou->size--;
|
||||
sasahyou->database = reallocarray(sasahyou->database, sasahyou->size, sizeof(Sasa));
|
||||
} else {
|
||||
sasahyou->hole_cnt++;
|
||||
sasahyou->holes = reallocarray(sasahyou->holes, sasahyou->hole_cnt, sizeof(Sasa *));
|
||||
sasahyou->holes[sasahyou->hole_cnt - 1] = current_sasa;
|
||||
}
|
||||
sasahyou->modified_ts = time(NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sasa_upd(Sasahyou *sasahyou, uint64_t sasa_id, const char *path) {
|
||||
if (sasa_id == HOLE_ID || sasa_id >= sasahyou->size || path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Sasa *current_sasa = sasahyou->database + sasa_id;
|
||||
if (current_sasa->id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
current_sasa->path = realloc(current_sasa->path, strlen(path) + 1);
|
||||
strcpy(current_sasa->path, path);
|
||||
sasahyou->modified_ts = time(NULL);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
#include <stdint.h>
|
||||
#include <malloc.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "core_func.h"
|
||||
|
||||
const Kazari HOLE_KAZARI = {HOLE_ID, HOLE_ID, 0};
|
||||
|
||||
// Shoppyou file signature: 七夕飾表
|
||||
static const uint16_t SHOPPYOU_SIG[4] = {L'七', L'夕', L'飾', L'表'};
|
||||
|
||||
int shoppyou_init(Shoppyou *shoppyou) {
|
||||
shoppyou->created_ts = time(NULL);
|
||||
shoppyou->modified_ts = shoppyou->created_ts;
|
||||
shoppyou->size = 0;
|
||||
shoppyou->database = NULL;
|
||||
shoppyou->hole_cnt = 0;
|
||||
shoppyou->holes = NULL;
|
||||
shoppyou->file = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int shoppyou_free(Shoppyou *shoppyou) {
|
||||
shoppyou->created_ts = 0;
|
||||
shoppyou->modified_ts = 0;
|
||||
shoppyou->size = 0;
|
||||
shoppyou->hole_cnt = 0;
|
||||
free(shoppyou->database);
|
||||
shoppyou->database = NULL;
|
||||
free(shoppyou->holes);
|
||||
shoppyou->holes = NULL;
|
||||
if (shoppyou->file != NULL) {
|
||||
fclose(shoppyou->file);
|
||||
shoppyou->file = NULL;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int shoppyou_load(Shoppyou *shoppyou) {
|
||||
if (shoppyou->file == NULL ||
|
||||
(shoppyou->file = freopen(NULL, "rb", shoppyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Shoppyou temp;
|
||||
shoppyou_init(&temp);
|
||||
temp.file = shoppyou->file;
|
||||
uint16_t signature[4];
|
||||
if (fread(signature, 2, 4, temp.file) != 4 ||
|
||||
memcmp(signature, SHOPPYOU_SIG, 8) != 0 ||
|
||||
fread(&temp.created_ts, 8, 1, temp.file) != 1 ||
|
||||
fread(&temp.modified_ts, 8, 1, temp.file) != 1 ||
|
||||
fread(&temp.size, 8, 1, temp.file) != 1) {
|
||||
return 1;
|
||||
}
|
||||
temp.database = calloc(temp.size, sizeof(Kazari));
|
||||
Kazari *current_kazari = temp.database;
|
||||
for (uint64_t i = 0; i < temp.size; i++, current_kazari++) {
|
||||
if (fread(¤t_kazari->created_ts, 8, 1, temp.file) != 1 ||
|
||||
fread(¤t_kazari->sasa_id, 8, 1, temp.file) != 1 ||
|
||||
fread(¤t_kazari->tanzaku_id, 8, 1, temp.file) != 1) {
|
||||
temp.file = NULL;
|
||||
shoppyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (fflush(temp.file) == 0) {
|
||||
shoppyou->file = NULL;
|
||||
shoppyou_free(shoppyou);
|
||||
*shoppyou = temp;
|
||||
return 0;
|
||||
}
|
||||
temp.file = NULL;
|
||||
shoppyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int shoppyou_save(Shoppyou *shoppyou) {
|
||||
if (shoppyou->file == NULL ||
|
||||
(shoppyou->file = freopen(NULL, "wb", shoppyou->file)) == NULL ||
|
||||
fwrite(SHOPPYOU_SIG, 2, 4, shoppyou->file) != 4 ||
|
||||
fwrite(&shoppyou->created_ts, 8, 1, shoppyou->file) != 1 ||
|
||||
fwrite(&shoppyou->modified_ts, 8, 1, shoppyou->file) != 1) {
|
||||
return 1;
|
||||
}
|
||||
uint64_t size = shoppyou->size - shoppyou->hole_cnt;
|
||||
if (fwrite(&size, 8, 1, shoppyou->file) != 1 ||
|
||||
fflush(shoppyou->file) != 0) {
|
||||
return 1;
|
||||
}
|
||||
Kazari *current_kazari = shoppyou->database;
|
||||
for (uint64_t i = 0; i < shoppyou->size; i++, current_kazari++) {
|
||||
if (shoppyou->database[i].sasa_id != HOLE_ID && shoppyou->database[i].tanzaku_id != HOLE_ID) {
|
||||
if (fwrite(¤t_kazari->created_ts, 8, 1, shoppyou->file) != 1 ||
|
||||
fwrite(¤t_kazari->sasa_id, 8, 1, shoppyou->file) != 1 ||
|
||||
fwrite(¤t_kazari->tanzaku_id, 8, 1, shoppyou->file) != 1) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fflush(shoppyou->file);
|
||||
}
|
||||
|
||||
int shoppyou_open(Shoppyou *shoppyou, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (shoppyou->file == NULL && (shoppyou->file = fopen(path, "rb")) == NULL ||
|
||||
shoppyou->file != NULL && (shoppyou->file = freopen(path, "rb", shoppyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
return shoppyou_load(shoppyou);
|
||||
}
|
||||
|
||||
int shoppyou_dump(Shoppyou *shoppyou, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (shoppyou->file == NULL && (shoppyou->file = fopen(path, "wb")) == NULL ||
|
||||
shoppyou->file != NULL && (shoppyou->file = freopen(path, "wb", shoppyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
return shoppyou_save(shoppyou);
|
||||
}
|
||||
|
||||
int kazari_add(Shoppyou *shoppyou, uint64_t sasa_id, uint64_t tanzaku_id) {
|
||||
if (sasa_id == HOLE_ID || tanzaku_id == HOLE_ID || shoppyou->size == -1 && shoppyou->hole_cnt == 0) {
|
||||
return 1;
|
||||
}
|
||||
Kazari newbie;
|
||||
newbie.created_ts = time(NULL);
|
||||
newbie.sasa_id = sasa_id;
|
||||
newbie.tanzaku_id = tanzaku_id;
|
||||
if (shoppyou->hole_cnt > 0) {
|
||||
shoppyou->hole_cnt--;
|
||||
**(shoppyou->holes + shoppyou->hole_cnt) = newbie;
|
||||
shoppyou->holes = reallocarray(shoppyou->holes, shoppyou->hole_cnt, sizeof(Kazari *));
|
||||
} else {
|
||||
shoppyou->size++;
|
||||
shoppyou->database = reallocarray(shoppyou->database, shoppyou->size, sizeof(Kazari));
|
||||
shoppyou->database[shoppyou->size - 1] = newbie;
|
||||
}
|
||||
shoppyou->modified_ts = newbie.created_ts;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int kazari_rem(Shoppyou *shoppyou, uint64_t sasa_id, uint64_t tanzaku_id) {
|
||||
if (sasa_id == HOLE_ID || tanzaku_id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
Kazari *current_kazari = shoppyou->database;
|
||||
for (uint64_t i = 0; i < shoppyou->size; i++, current_kazari++) {
|
||||
if (current_kazari->sasa_id == sasa_id && current_kazari->tanzaku_id == tanzaku_id) {
|
||||
current_kazari->sasa_id = HOLE_ID;
|
||||
current_kazari->tanzaku_id = HOLE_ID;
|
||||
shoppyou->hole_cnt++;
|
||||
shoppyou->holes = reallocarray(shoppyou->holes, shoppyou->hole_cnt, sizeof(Kazari *));
|
||||
shoppyou->holes[shoppyou->hole_cnt - 1] = current_kazari;
|
||||
shoppyou->modified_ts = time(NULL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int kazari_rem_by_sasa(Shoppyou *shoppyou, uint64_t sasa_id) {
|
||||
if (sasa_id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
Kazari *current_kazari = shoppyou->database;
|
||||
_Bool changed = 0;
|
||||
for (uint64_t i = 0; i < shoppyou->size; i++, current_kazari++) {
|
||||
if (current_kazari->sasa_id == sasa_id) {
|
||||
current_kazari->sasa_id = HOLE_ID;
|
||||
current_kazari->tanzaku_id = HOLE_ID;
|
||||
shoppyou->hole_cnt++;
|
||||
shoppyou->holes = reallocarray(shoppyou->holes, shoppyou->hole_cnt, sizeof(Kazari *));
|
||||
shoppyou->holes[shoppyou->hole_cnt - 1] = current_kazari;
|
||||
changed = 1;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
shoppyou->modified_ts = time(NULL);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int kazari_rem_by_tanzaku(Shoppyou *shoppyou, uint64_t tanzaku_id) {
|
||||
if (tanzaku_id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
Kazari *current_kazari = shoppyou->database;
|
||||
_Bool changed = 0;
|
||||
for (uint64_t i = 0; i < shoppyou->size; i++, current_kazari++) {
|
||||
if (current_kazari->tanzaku_id == tanzaku_id) {
|
||||
current_kazari->sasa_id = HOLE_ID;
|
||||
current_kazari->tanzaku_id = HOLE_ID;
|
||||
shoppyou->hole_cnt++;
|
||||
shoppyou->holes = reallocarray(shoppyou->holes, shoppyou->hole_cnt, sizeof(Kazari *));
|
||||
shoppyou->holes[shoppyou->hole_cnt - 1] = current_kazari;
|
||||
changed = 1;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
shoppyou->modified_ts = time(NULL);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
#include <malloc.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "../core/core_func.h"
|
||||
#include "../../include/tanabata.h"
|
||||
|
||||
int tanabata_init(Tanabata *tanabata) {
|
||||
if (sasahyou_init(&tanabata->sasahyou) != 0 ||
|
||||
sappyou_init(&tanabata->sappyou) != 0 ||
|
||||
shoppyou_init(&tanabata->shoppyou) != 0) {
|
||||
return 1;
|
||||
}
|
||||
tanabata->sappyou.size = 1;
|
||||
tanabata->sappyou.database = malloc(sizeof(Tanzaku));
|
||||
tanabata->sappyou.database->id = 0;
|
||||
tanabata->sappyou.database->created_ts = tanabata->sappyou.created_ts;
|
||||
tanabata->sappyou.database->modified_ts = tanabata->sappyou.created_ts;
|
||||
tanabata->sappyou.database->name = malloc(9);
|
||||
tanabata->sappyou.database->description = malloc(30);
|
||||
strcpy(tanabata->sappyou.database->name, "FAVORITE");
|
||||
strcpy(tanabata->sappyou.database->description, "Special tanzaku for favorites");
|
||||
tanabata->sasahyou_mod = 0;
|
||||
tanabata->sappyou_mod = 0;
|
||||
tanabata->shoppyou_mod = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tanabata_free(Tanabata *tanabata) {
|
||||
if (sasahyou_free(&tanabata->sasahyou) != 0 ||
|
||||
sappyou_free(&tanabata->sappyou) != 0 ||
|
||||
shoppyou_free(&tanabata->shoppyou) != 0) {
|
||||
return 1;
|
||||
}
|
||||
tanabata->sasahyou_mod = 0;
|
||||
tanabata->sappyou_mod = 0;
|
||||
tanabata->shoppyou_mod = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tanabata_weed(Tanabata *tanabata) {
|
||||
uint64_t hole_cnt = 0, new_id;
|
||||
Kazari *current_kazari;
|
||||
Sasa *current_sasa = tanabata->sasahyou.database;
|
||||
for (uint64_t i = 0; i < tanabata->sasahyou.size; i++, current_sasa++) {
|
||||
if (current_sasa->id != HOLE_ID) {
|
||||
if (hole_cnt > 0) {
|
||||
new_id = current_sasa->id - hole_cnt;
|
||||
for (current_kazari = tanabata->shoppyou.database + tanabata->shoppyou.size - 1;
|
||||
current_kazari >= tanabata->shoppyou.database; current_kazari--) {
|
||||
if (current_kazari->sasa_id == current_sasa->id) {
|
||||
current_kazari->sasa_id = new_id;
|
||||
}
|
||||
}
|
||||
current_sasa->id = new_id;
|
||||
*(current_sasa - hole_cnt) = *current_sasa;
|
||||
}
|
||||
} else {
|
||||
kazari_rem_by_sasa(&tanabata->shoppyou, current_sasa->id);
|
||||
hole_cnt++;
|
||||
}
|
||||
}
|
||||
if (hole_cnt > 0) {
|
||||
tanabata->sasahyou.size -= hole_cnt;
|
||||
tanabata->sasahyou.hole_cnt = 0;
|
||||
free(tanabata->sasahyou.holes);
|
||||
tanabata->sasahyou.holes = NULL;
|
||||
tanabata->sasahyou.database = reallocarray(tanabata->sasahyou.database, tanabata->sasahyou.size,
|
||||
sizeof(Sasa));
|
||||
tanabata->sasahyou.modified_ts = time(NULL);
|
||||
}
|
||||
hole_cnt = 0;
|
||||
Tanzaku *current_tanzaku = tanabata->sappyou.database;
|
||||
for (uint64_t i = 0; i < tanabata->sappyou.size; i++, current_tanzaku++) {
|
||||
if (current_tanzaku->id != HOLE_ID) {
|
||||
if (hole_cnt > 0) {
|
||||
new_id = current_tanzaku->id - hole_cnt;
|
||||
for (current_kazari = tanabata->shoppyou.database + tanabata->shoppyou.size - 1;
|
||||
current_kazari >= tanabata->shoppyou.database; current_kazari--) {
|
||||
if (current_kazari->tanzaku_id == current_tanzaku->id) {
|
||||
current_kazari->tanzaku_id = new_id;
|
||||
}
|
||||
}
|
||||
current_tanzaku->id = new_id;
|
||||
*(current_tanzaku - hole_cnt) = *current_tanzaku;
|
||||
} else {
|
||||
hole_cnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hole_cnt > 0) {
|
||||
tanabata->sappyou.size -= tanabata->sappyou.hole_cnt;
|
||||
tanabata->sappyou.hole_cnt = 0;
|
||||
free(tanabata->sappyou.holes);
|
||||
tanabata->sappyou.holes = NULL;
|
||||
tanabata->sappyou.database = reallocarray(tanabata->sappyou.database, tanabata->sappyou.size,
|
||||
sizeof(Tanzaku));
|
||||
tanabata->sappyou.modified_ts = time(NULL);
|
||||
}
|
||||
hole_cnt = 0;
|
||||
current_kazari = tanabata->shoppyou.database;
|
||||
for (uint64_t i = 0; i < tanabata->shoppyou.size; i++, current_kazari++) {
|
||||
if (current_kazari->sasa_id != HOLE_ID && current_kazari->tanzaku_id != HOLE_ID &&
|
||||
current_kazari->sasa_id < tanabata->sasahyou.size &&
|
||||
current_kazari->tanzaku_id < tanabata->sappyou.size) {
|
||||
if (hole_cnt > 0) {
|
||||
*(current_kazari - hole_cnt) = *current_kazari;
|
||||
}
|
||||
} else {
|
||||
hole_cnt++;
|
||||
}
|
||||
}
|
||||
if (hole_cnt > 0) {
|
||||
tanabata->shoppyou.size -= tanabata->shoppyou.hole_cnt;
|
||||
tanabata->shoppyou.hole_cnt = 0;
|
||||
free(tanabata->shoppyou.holes);
|
||||
tanabata->shoppyou.holes = NULL;
|
||||
tanabata->shoppyou.database = reallocarray(tanabata->shoppyou.database, tanabata->shoppyou.size,
|
||||
sizeof(Kazari));
|
||||
tanabata->shoppyou.modified_ts = time(NULL);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tanabata_load(Tanabata *tanabata) {
|
||||
if (sasahyou_load(&tanabata->sasahyou) != 0 ||
|
||||
sappyou_load(&tanabata->sappyou) != 0 ||
|
||||
shoppyou_load(&tanabata->shoppyou) != 0) {
|
||||
return 1;
|
||||
}
|
||||
tanabata->sasahyou_mod = tanabata->sasahyou.modified_ts;
|
||||
tanabata->sappyou_mod = tanabata->sappyou.modified_ts;
|
||||
tanabata->shoppyou_mod = tanabata->shoppyou.modified_ts;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tanabata_save(Tanabata *tanabata) {
|
||||
if (tanabata->sasahyou_mod != tanabata->sasahyou.modified_ts && sasahyou_save(&tanabata->sasahyou) != 0 ||
|
||||
tanabata->sappyou_mod != tanabata->sappyou.modified_ts && sappyou_save(&tanabata->sappyou) != 0 ||
|
||||
tanabata->shoppyou_mod != tanabata->shoppyou.modified_ts && shoppyou_save(&tanabata->shoppyou) != 0) {
|
||||
return 1;
|
||||
}
|
||||
tanabata->sasahyou_mod = tanabata->sasahyou.modified_ts;
|
||||
tanabata->sappyou_mod = tanabata->sappyou.modified_ts;
|
||||
tanabata->shoppyou_mod = tanabata->shoppyou.modified_ts;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tanabata_open(Tanabata *tanabata, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
||||
return 1;
|
||||
}
|
||||
size_t pathlen = strlen(path);
|
||||
char *file_path = malloc(pathlen + 10);
|
||||
strcpy(file_path, path);
|
||||
if (sasahyou_open(&tanabata->sasahyou, strcpy(file_path + pathlen, "/sasahyou") - pathlen) != 0 ||
|
||||
sappyou_open(&tanabata->sappyou, strcpy(file_path + pathlen, "/sappyou") - pathlen) != 0 ||
|
||||
shoppyou_open(&tanabata->shoppyou, strcpy(file_path + pathlen, "/shoppyou") - pathlen) != 0) {
|
||||
free(file_path);
|
||||
return 1;
|
||||
}
|
||||
free(file_path);
|
||||
tanabata->sasahyou_mod = tanabata->sasahyou.modified_ts;
|
||||
tanabata->sappyou_mod = tanabata->sappyou.modified_ts;
|
||||
tanabata->shoppyou_mod = tanabata->shoppyou.modified_ts;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tanabata_dump(Tanabata *tanabata, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
||||
return 1;
|
||||
}
|
||||
size_t pathlen = strlen(path);
|
||||
char *file_path = malloc(pathlen + 10);
|
||||
strcpy(file_path, path);
|
||||
if (tanabata->sasahyou_mod != tanabata->sasahyou.modified_ts &&
|
||||
sasahyou_dump(&tanabata->sasahyou, strcpy(file_path + pathlen, "/sasahyou") - pathlen) != 0 ||
|
||||
tanabata->sappyou_mod != tanabata->sappyou.modified_ts &&
|
||||
sappyou_dump(&tanabata->sappyou, strcpy(file_path + pathlen, "/sappyou") - pathlen) != 0 ||
|
||||
tanabata->shoppyou_mod != tanabata->shoppyou.modified_ts &&
|
||||
shoppyou_dump(&tanabata->shoppyou, strcpy(file_path + pathlen, "/shoppyou") - pathlen) != 0) {
|
||||
free(file_path);
|
||||
return 1;
|
||||
}
|
||||
free(file_path);
|
||||
tanabata->sasahyou_mod = tanabata->sasahyou.modified_ts;
|
||||
tanabata->sappyou_mod = tanabata->sappyou.modified_ts;
|
||||
tanabata->shoppyou_mod = tanabata->shoppyou.modified_ts;
|
||||
return 0;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
#include <malloc.h>
|
||||
|
||||
#include "../core/core_func.h"
|
||||
#include "../../include/tanabata.h"
|
||||
|
||||
int tanabata_kazari_add(Tanabata *tanabata, uint64_t sasa_id, uint64_t tanzaku_id) {
|
||||
if (sasa_id >= tanabata->sasahyou.size || tanzaku_id >= tanabata->sappyou.size ||
|
||||
tanabata->shoppyou.size == -1 && tanabata->shoppyou.hole_cnt == 0) {
|
||||
return 1;
|
||||
}
|
||||
if (tanabata->shoppyou.size - tanabata->shoppyou.hole_cnt > 0) {
|
||||
Kazari *current_kazari = tanabata->shoppyou.database + tanabata->shoppyou.size - 1;
|
||||
for (; current_kazari >= tanabata->shoppyou.database; current_kazari--) {
|
||||
if (current_kazari->sasa_id == sasa_id && current_kazari->tanzaku_id == tanzaku_id) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return kazari_add(&tanabata->shoppyou, sasa_id, tanzaku_id);
|
||||
}
|
||||
|
||||
int tanabata_kazari_rem(Tanabata *tanabata, uint64_t sasa_id, uint64_t tanzaku_id) {
|
||||
return kazari_rem(&tanabata->shoppyou, sasa_id, tanzaku_id);
|
||||
}
|
||||
|
||||
Tanzaku *tanabata_tanzaku_get_by_sasa(Tanabata *tanabata, uint64_t sasa_id) {
|
||||
if (sasa_id == HOLE_ID || sasa_id >= tanabata->sasahyou.size ||
|
||||
tanabata->shoppyou.size - tanabata->shoppyou.hole_cnt == 0) {
|
||||
return NULL;
|
||||
}
|
||||
Tanzaku *tanzaku_list = NULL;
|
||||
uint64_t tanzaku_count = 0;
|
||||
Tanzaku temp;
|
||||
Kazari *current_kazari = tanabata->shoppyou.database;
|
||||
for (uint64_t i = 0; i < tanabata->shoppyou.size; i++, current_kazari++) {
|
||||
if (current_kazari->sasa_id == sasa_id &&
|
||||
(temp = tanabata_tanzaku_get(tanabata, current_kazari->tanzaku_id)).id != HOLE_ID) {
|
||||
tanzaku_count++;
|
||||
tanzaku_list = reallocarray(tanzaku_list, tanzaku_count, sizeof(Tanzaku));
|
||||
tanzaku_list[tanzaku_count - 1] = temp;
|
||||
}
|
||||
}
|
||||
tanzaku_list = reallocarray(tanzaku_list, tanzaku_count + 1, sizeof(Tanzaku));
|
||||
tanzaku_list[tanzaku_count] = HOLE_TANZAKU;
|
||||
return tanzaku_list;
|
||||
}
|
||||
|
||||
Sasa *tanabata_sasa_get_by_tanzaku(Tanabata *tanabata, uint64_t tanzaku_id) {
|
||||
if (tanzaku_id == HOLE_ID || tanzaku_id >= tanabata->sappyou.size ||
|
||||
tanabata->shoppyou.size - tanabata->shoppyou.hole_cnt == 0) {
|
||||
return NULL;
|
||||
}
|
||||
Sasa *sasa_list = NULL;
|
||||
uint64_t sasa_count = 0;
|
||||
Sasa temp;
|
||||
Kazari *current_kazari = tanabata->shoppyou.database;
|
||||
for (uint64_t i = 0; i < tanabata->shoppyou.size; i++, current_kazari++) {
|
||||
if (current_kazari->tanzaku_id == tanzaku_id &&
|
||||
(temp = tanabata_sasa_get(tanabata, current_kazari->sasa_id)).id != HOLE_ID) {
|
||||
sasa_count++;
|
||||
sasa_list = reallocarray(sasa_list, sasa_count, sizeof(Sasa));
|
||||
sasa_list[sasa_count - 1] = temp;
|
||||
}
|
||||
}
|
||||
sasa_list = reallocarray(sasa_list, sasa_count + 1, sizeof(Sasa));
|
||||
sasa_list[sasa_count] = HOLE_SASA;
|
||||
return sasa_list;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#include "../core/core_func.h"
|
||||
#include "../../include/tanabata.h"
|
||||
|
||||
Sasa tanabata_sasa_add(Tanabata *tanabata, const char *path) {
|
||||
return sasa_add(&tanabata->sasahyou, path);
|
||||
}
|
||||
|
||||
int tanabata_sasa_rem(Tanabata *tanabata, uint64_t sasa_id) {
|
||||
if (sasa_rem(&tanabata->sasahyou, sasa_id) == 0 &&
|
||||
kazari_rem_by_sasa(&tanabata->shoppyou, sasa_id) == 0) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int tanabata_sasa_upd(Tanabata *tanabata, uint64_t sasa_id, const char *path) {
|
||||
return sasa_upd(&tanabata->sasahyou, sasa_id, path);
|
||||
}
|
||||
|
||||
Sasa tanabata_sasa_get(Tanabata *tanabata, uint64_t sasa_id) {
|
||||
if (sasa_id == HOLE_ID || sasa_id >= tanabata->sasahyou.size) {
|
||||
return HOLE_SASA;
|
||||
}
|
||||
return tanabata->sasahyou.database[sasa_id];
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#include "../core/core_func.h"
|
||||
#include "../../include/tanabata.h"
|
||||
|
||||
Tanzaku tanabata_tanzaku_add(Tanabata *tanabata, const char *name, const char *description) {
|
||||
return tanzaku_add(&tanabata->sappyou, name, description);
|
||||
}
|
||||
|
||||
int tanabata_tanzaku_rem(Tanabata *tanabata, uint64_t tanzaku_id) {
|
||||
if (tanzaku_rem(&tanabata->sappyou, tanzaku_id) == 0 &&
|
||||
kazari_rem_by_tanzaku(&tanabata->shoppyou, tanzaku_id) == 0) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int tanabata_tanzaku_upd(Tanabata *tanabata, uint64_t tanzaku_id, const char *name, const char *description) {
|
||||
return tanzaku_upd(&tanabata->sappyou, tanzaku_id, name, description);
|
||||
}
|
||||
|
||||
Tanzaku tanabata_tanzaku_get(Tanabata *tanabata, uint64_t tanzaku_id) {
|
||||
if (tanzaku_id == HOLE_ID || tanzaku_id >= tanabata->sappyou.size) {
|
||||
return HOLE_TANZAKU;
|
||||
}
|
||||
return tanabata->sappyou.database[tanzaku_id];
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../../include/tdbms-client.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc == 1 || strcmp(argv[1], "-h") == 0) {
|
||||
printf("Tanabata Database Management client\n\n"
|
||||
"Usage\n"
|
||||
" tdb [DB_NAME [REQUEST_CODE [REQUEST_BODY]]]\n\n"
|
||||
"Request codes:\n"
|
||||
" 0\tDB stats\n"
|
||||
" 3\tDB init\n"
|
||||
" 2\tDB load\n"
|
||||
" 4\tDB save\n"
|
||||
" 6\tDB edit\n"
|
||||
" 1\tDB remove soft\n"
|
||||
" 5\tDB remove hard\n"
|
||||
" 7\tDB weed\n"
|
||||
" 16\tSasa get\n"
|
||||
" 40\tSasa get by tanzaku\n"
|
||||
" 18\tSasa add\n"
|
||||
" 20\tSasa update\n"
|
||||
" 17\tSasa remove\n"
|
||||
" 32\tTanzaku get\n"
|
||||
" 24\tTanzaku get by sasa\n"
|
||||
" 34\tTanzaku add\n"
|
||||
" 36\tTanzaku update\n"
|
||||
" 33\tTanzaku remove\n"
|
||||
" 8\tKazari get\n"
|
||||
" 10\tKazari add\n"
|
||||
" 26\tKazari add single sasa to multiple tanzaku\n"
|
||||
" 42\tKazari add single tanzaku to multiple sasa\n"
|
||||
" 9\tKazari remove\n"
|
||||
" 25\tKazari remove single sasa to multiple tanzaku\n"
|
||||
" 41\tKazari remove single tanzaku to multiple sasa\n");
|
||||
return 0;
|
||||
}
|
||||
char *db_name, request_code, *request_body;
|
||||
if (argc < 4) {
|
||||
request_body = "";
|
||||
} else {
|
||||
request_body = argv[3];
|
||||
}
|
||||
if (argc < 3) {
|
||||
request_code = 0;
|
||||
} else {
|
||||
char *endptr;
|
||||
request_code = (char) strtol(argv[2], &endptr, 0);
|
||||
if (*endptr != 0) {
|
||||
fprintf(stderr, "FATAL: invalid request code '%s'\n", argv[2]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (argc < 2) {
|
||||
db_name = "";
|
||||
} else {
|
||||
db_name = argv[1];
|
||||
}
|
||||
int socket_fd = tdbms_connect("UNIX", "/tmp/tdbms.sock");
|
||||
if (socket_fd < 0) {
|
||||
fprintf(stderr, "FATAL: failed to connect to TDBMS server\n");
|
||||
return 1;
|
||||
}
|
||||
char *response = tdb_query(socket_fd, db_name, request_code, request_body);
|
||||
if (response == NULL) {
|
||||
fprintf(stderr, "FATAL: failed to execute request\n");
|
||||
return 1;
|
||||
}
|
||||
printf("%s\n", response);
|
||||
tdbms_close(socket_fd);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#include "../../include/tdbms-client.h"
|
||||
|
||||
int tdbms_connect(const char *domain, const char *addr) {
|
||||
int socket_fd;
|
||||
struct sockaddr_un sockaddr;
|
||||
int domain_code;
|
||||
if (strcmp(domain, "UNIX") == 0) {
|
||||
domain_code = AF_UNIX;
|
||||
} else {
|
||||
fprintf(stderr, "ERROR: unexpected socket domain '%s'\n", domain);
|
||||
return -1;
|
||||
}
|
||||
if (strlen(addr) > sizeof(sockaddr.sun_path) - 1) {
|
||||
fprintf(stderr, "ERROR: too long socket address\n");
|
||||
return -1;
|
||||
}
|
||||
socket_fd = socket(domain_code, SOCK_STREAM, 0);
|
||||
if (socket_fd < 0) {
|
||||
fprintf(stderr, "ERROR: failed to initialize socket\n");
|
||||
return -1;
|
||||
}
|
||||
bzero(&sockaddr, sizeof(sockaddr));
|
||||
sockaddr.sun_family = domain_code;
|
||||
strcpy(sockaddr.sun_path, addr);
|
||||
if (connect(socket_fd, (const struct sockaddr *) &sockaddr, sizeof(sockaddr)) < 0) {
|
||||
fprintf(stderr, "ERROR: failed to connect the socket\n");
|
||||
return -1;
|
||||
}
|
||||
return socket_fd;
|
||||
}
|
||||
|
||||
int tdbms_close(int socket_fd) {
|
||||
return close(socket_fd);
|
||||
}
|
||||
|
||||
char *tdb_query(int socket_fd, const char *db_name, char request_code, const char *request_body) {
|
||||
if (socket_fd < 0 || db_name == NULL || request_body == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
size_t req_size = 1 + strlen(db_name) + 1 + strlen(request_body) + 1, resp_size;
|
||||
ssize_t nread, nwrite;
|
||||
char *request = malloc(req_size);
|
||||
char *buffer = request;
|
||||
*buffer = request_code;
|
||||
buffer++;
|
||||
strcpy(buffer, db_name);
|
||||
buffer += strlen(db_name) + 1;
|
||||
strcpy(buffer, request_body);
|
||||
for (buffer = request; (nwrite = write(socket_fd, buffer, req_size)) > 0;) {
|
||||
buffer += nwrite;
|
||||
req_size -= nwrite;
|
||||
if (req_size == 0) {
|
||||
nwrite = write(socket_fd, "\4", 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
free(request);
|
||||
if (nwrite <= 0) {
|
||||
fprintf(stderr, "ERROR: failed to send request to server\n");
|
||||
return NULL;
|
||||
}
|
||||
char *response = malloc(BUFSIZ);
|
||||
resp_size = BUFSIZ;
|
||||
buffer = malloc(BUFSIZ);
|
||||
for (off_t offset = 0; (nread = read(socket_fd, buffer, BUFSIZ)) > 0;) {
|
||||
if (offset + nread > resp_size) {
|
||||
resp_size += BUFSIZ;
|
||||
response = realloc(response, resp_size);
|
||||
}
|
||||
memcpy(response + offset, buffer, nread);
|
||||
offset += nread;
|
||||
if (response[offset - 1] == EOT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
free(buffer);
|
||||
if (nread < 0) {
|
||||
fprintf(stderr, "ERROR: failed to get server response\n");
|
||||
free(response);
|
||||
return NULL;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script performs the installation of the Tanabata DBMS server
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
|
||||
|
||||
getent group tanabata &>/dev/null || groupadd -g 42776 tanabata
|
||||
id tanabata &>/dev/null || useradd -u 42776 -g 42776 tanabata
|
||||
if [ ! "$(id -nG 42776 | grep -w tanabata)" ]; then
|
||||
echo "FATAL: failed to create user and group 'tanabata'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d /etc/tanabata ]; then
|
||||
mkdir /etc/tanabata
|
||||
if [ ! -d /etc/tanabata ]; then
|
||||
echo "FATAL: failed to create directory '/etc/tanabata'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
chown 42776:42776 /etc/tanabata
|
||||
chmod 2755 /etc/tanabata
|
||||
|
||||
if [ ! -d /var/lib/tanabata ]; then
|
||||
mkdir /var/lib/tanabata
|
||||
if [ ! -d /var/lib/tanabata ]; then
|
||||
echo "FATAL: failed to create directory '/var/lib/tanabata'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
chown 42776:42776 /var/lib/tanabata
|
||||
chmod 2755 /var/lib/tanabata
|
||||
|
||||
if [ ! -d /var/lib/tanabata/tdbms ]; then
|
||||
mkdir /var/lib/tanabata/tdbms
|
||||
if [ ! -d /var/lib/tanabata/tdbms ]; then
|
||||
echo "FATAL: failed to create directory '/var/lib/tanabata/tdbms'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
chown 42776:42776 /var/lib/tanabata/tdbms
|
||||
chmod 2755 /var/lib/tanabata/tdbms
|
||||
|
||||
if [ ! -d /var/log/tanabata ]; then
|
||||
mkdir /var/log/tanabata
|
||||
if [ ! -d /var/log/tanabata ]; then
|
||||
echo "FATAL: failed to create directory '/var/log/tanabata'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
chown 42776:42776 /var/log/tanabata
|
||||
chmod 2775 /var/log/tanabata
|
||||
|
||||
if [ -d ../build ]; then
|
||||
rm -r ../build/*
|
||||
else
|
||||
mkdir ../build
|
||||
if [ -d ../build ]; then
|
||||
echo "FATAL: failed to create build directory"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if ! (cmake -S .. -B ../build && cmake --build ../build --target tdbms); then
|
||||
echo "FATAL: failed to build TDBMS server"
|
||||
exit 1
|
||||
fi
|
||||
mv -f ../build/tdbms /usr/bin/
|
||||
chown 0:0 /usr/bin/tdbms
|
||||
chmod 0755 /usr/bin/tdbms
|
||||
|
||||
if ! cp ./tdbms.service /etc/systemd/system/; then
|
||||
echo "FATAL: failed to copy 'tdbms.service' to '/etc/systemd/system'"
|
||||
exit 1
|
||||
fi
|
||||
chown 0:0 /etc/systemd/system/tdbms.service
|
||||
chmod 0644 /etc/systemd/system/tdbms.service
|
||||
|
||||
if ! (cmake -S .. -B ../build && cmake --build ../build --target tdb); then
|
||||
echo "FATAL: failed to build TDB CLI client"
|
||||
exit 1
|
||||
fi
|
||||
mv -f ../build/tdb /usr/bin/
|
||||
chown 42776 /usr/bin/tdb
|
||||
chmod 4755 /usr/bin/tdb
|
||||
|
||||
echo "TDBMS server successfully installed."
|
||||
echo "Start it with 'systemctl start tdbms'"
|
||||
@@ -1,14 +0,0 @@
|
||||
[Unit]
|
||||
Description=Tanabata Database Management System service
|
||||
After=network.target
|
||||
AssertPathIsDirectory=/var/lib/tanabata/tdbms
|
||||
AssertPathIsDirectory=/var/log/tanabata
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=no
|
||||
User=tanabata
|
||||
ExecStart=/usr/bin/tdbms
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,613 +0,0 @@
|
||||
#include <stdlib.h>
|
||||
#include <getopt.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "../../include/tanabata.h"
|
||||
|
||||
// TFM configuration directory
|
||||
#define TFM_CONFIG_DIR "/etc/tanabata/"
|
||||
|
||||
// Stylization macros
|
||||
#define TABLE_HEADER(s) "[7;36m"s"[0m"
|
||||
#define HIGHLIGHT(s) "[0;36m"s"[0m"
|
||||
#define SUCCESS(s) "[0;32m"s"[0m"
|
||||
#define ERROR(s) "[0;31m"s"[0m"
|
||||
|
||||
#define DT_FORMAT "%F %T"
|
||||
|
||||
static Tanabata tanabata;
|
||||
|
||||
// Print the list of all sasa
|
||||
void print_sasa_all() {
|
||||
printf(TABLE_HEADER(" Sasa ID\tFile path")"\n");
|
||||
for (uint64_t i = 0; i < tanabata.sasahyou.size; i++) {
|
||||
if (tanabata.sasahyou.database[i].id != HOLE_ID) {
|
||||
printf("%16lx\t%s\n", tanabata.sasahyou.database[i].id, tanabata.sasahyou.database[i].path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print the list of all tanzaku
|
||||
void print_tanzaku_all() {
|
||||
printf(TABLE_HEADER(" Tanzaku ID\tName")"\n");
|
||||
for (uint64_t i = 0; i < tanabata.sappyou.size; i++) {
|
||||
if (tanabata.sappyou.database[i].id != HOLE_ID) {
|
||||
printf("%16lx\t%s\n", tanabata.sappyou.database[i].id, tanabata.sappyou.database[i].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sasa view menu handler
|
||||
int menu_view_sasa(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(arg, ".") == 0) {
|
||||
print_sasa_all();
|
||||
return 0;
|
||||
}
|
||||
char *endptr;
|
||||
uint64_t sasa_id = strtoull(arg, &endptr, 16);
|
||||
if (*endptr == 0) {
|
||||
Sasa current_sasa = tanabata_sasa_get(&tanabata, sasa_id);
|
||||
if (current_sasa.id != HOLE_ID) {
|
||||
char datetime[20];
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) ¤t_sasa.created_ts));
|
||||
printf(HIGHLIGHT("Sasa ID")" %lx\n"
|
||||
HIGHLIGHT("File path")" %s\n"
|
||||
HIGHLIGHT("Added datetime")" %s\n\n",
|
||||
sasa_id, current_sasa.path, datetime);
|
||||
Tanzaku *related_tanzaku = tanabata_tanzaku_get_by_sasa(&tanabata, current_sasa.id);
|
||||
if (related_tanzaku != NULL) {
|
||||
printf(HIGHLIGHT("↓ Related tanzaku ↓")"\n"
|
||||
HIGHLIGHT(" Tanzaku ID\tName")"\n");
|
||||
for (Tanzaku *current_tanzaku = related_tanzaku;
|
||||
current_tanzaku->id != HOLE_ID; current_tanzaku++) {
|
||||
printf("%16lx\t%s\n", current_tanzaku->id, current_tanzaku->name);
|
||||
}
|
||||
printf(HIGHLIGHT("↑ Related tanzaku ↑")"\n");
|
||||
} else {
|
||||
printf(HIGHLIGHT("No related tanzaku")"\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("No sasa with this ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, ERROR("Invalid ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Tanzaku view menu handler
|
||||
int menu_view_tanzaku(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(arg, ".") == 0) {
|
||||
print_tanzaku_all();
|
||||
return 0;
|
||||
}
|
||||
char *endptr;
|
||||
uint64_t tanzaku_id = strtoull(arg, &endptr, 16);
|
||||
if (*endptr == 0) {
|
||||
Tanzaku current_tanzaku = tanabata_tanzaku_get(&tanabata, tanzaku_id);
|
||||
if (current_tanzaku.id != HOLE_ID) {
|
||||
char datetime[20];
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) ¤t_tanzaku.created_ts));
|
||||
printf(HIGHLIGHT("Tanzaku ID")" %lx\n"
|
||||
HIGHLIGHT("Name")" %s\n"
|
||||
HIGHLIGHT("Created datetime")" %s\n",
|
||||
tanzaku_id, current_tanzaku.name, datetime);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) ¤t_tanzaku.modified_ts));
|
||||
printf(HIGHLIGHT("Modified datetime")" %s\n\n", datetime);
|
||||
if (*current_tanzaku.description != 0) {
|
||||
printf(HIGHLIGHT("↓ Description ↓")"\n"
|
||||
"%s\n"
|
||||
HIGHLIGHT("↑ Description ↑")"\n\n", current_tanzaku.description);
|
||||
} else {
|
||||
printf(HIGHLIGHT("No description")"\n\n");
|
||||
}
|
||||
Sasa *related_sasa = tanabata_sasa_get_by_tanzaku(&tanabata, tanzaku_id);
|
||||
if (related_sasa != NULL) {
|
||||
printf(HIGHLIGHT("↓ Related sasa ↓")"\n"
|
||||
HIGHLIGHT(" Sasa ID\tFile path")"\n");
|
||||
for (Sasa *current_sasa = related_sasa;
|
||||
current_sasa->id != HOLE_ID; current_sasa++) {
|
||||
printf("%16lx\t%s\n", current_sasa->id, current_sasa->path);
|
||||
}
|
||||
printf(HIGHLIGHT("↑ Related sasa ↑")"\n");
|
||||
} else {
|
||||
printf(HIGHLIGHT("No related sasa")"\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("No tanzaku with this ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, ERROR("Invalid ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Sasa add menu handler
|
||||
int menu_add_sasa(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (tanabata.sasahyou.size == -1 && tanabata.sasahyou.hole_cnt == 0) {
|
||||
fprintf(stderr, ERROR("Failed to add file to database: sasahyou is full")"\n");
|
||||
return 1;
|
||||
}
|
||||
char *path = realpath(arg, NULL);
|
||||
if (path == NULL) {
|
||||
fprintf(stderr, ERROR("Invalid file path")"\n");
|
||||
free(path);
|
||||
return 1;
|
||||
}
|
||||
if (tanabata_sasa_add(&tanabata, path).id != HOLE_ID &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully added file to database")"\n");
|
||||
free(path);
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to add file to database")"\n");
|
||||
free(path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Tanzaku add menu handler
|
||||
int menu_add_tanzaku(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (tanabata.sappyou.size == -1 && tanabata.sappyou.hole_cnt == 0) {
|
||||
fprintf(stderr, ERROR("Failed to add tanzaku: sappyou is full")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (*arg != 0) {
|
||||
char description[4096];
|
||||
printf(HIGHLIGHT("Enter tanzaku description:")"\n");
|
||||
fgets(description, 4096, stdin);
|
||||
description[strlen(description) - 1] = 0;
|
||||
if (tanabata_tanzaku_add(&tanabata, arg, description).id != HOLE_ID &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully added tanzaku to database")"\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to add tanzaku to database")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Kazari add menu handler
|
||||
int menu_add_kazari(char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (tanabata.shoppyou.size == -1 && tanabata.shoppyou.hole_cnt == 0) {
|
||||
fprintf(stderr, ERROR("Failed to add kazari: shoppyou is full")"\n");
|
||||
return 1;
|
||||
}
|
||||
char *left = arg, *right = "\0", *endptr;
|
||||
for (size_t i = 0; i < strlen(arg); i++) {
|
||||
if (arg[i] == '-') {
|
||||
arg[i] = 0;
|
||||
right = arg + i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (*left == 0 || *right == 0) {
|
||||
fprintf(stderr, ERROR("Failed to add kazari: invalid argument")"\n");
|
||||
return 1;
|
||||
}
|
||||
uint64_t sasa_id = strtoull(left, &endptr, 16);
|
||||
if (*endptr != 0) {
|
||||
fprintf(stderr, ERROR("Failed to add kazari: invalid sasa ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
uint64_t tanzaku_id = strtoull(right, &endptr, 16);
|
||||
if (*endptr != 0) {
|
||||
fprintf(stderr, ERROR("Failed to add kazari: invalid tanzaku ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (tanabata_kazari_add(&tanabata, sasa_id, tanzaku_id) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully added kazari")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to add kazari")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Sasa remove menu handler
|
||||
int menu_rem_sasa(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
char *endptr;
|
||||
uint64_t sasa_id = strtoull(arg, &endptr, 16);
|
||||
if (*endptr == 0) {
|
||||
if (tanabata_sasa_rem(&tanabata, sasa_id) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully removed sasa")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to remove sasa")"\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, ERROR("Invalid ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Tanzaku remove menu handler
|
||||
int menu_rem_tanzaku(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
char *endptr;
|
||||
uint64_t tanzaku_id = strtoull(arg, &endptr, 16);
|
||||
if (*endptr == 0) {
|
||||
if (tanabata_tanzaku_rem(&tanabata, tanzaku_id) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully removed tanzaku")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to remove tanzaku")"\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, ERROR("Invalid ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Kazari remove menu handler
|
||||
int menu_rem_kazari(char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
char *left = arg, *right = "\0", *endptr;
|
||||
for (size_t i = 0; i < strlen(arg); i++) {
|
||||
if (arg[i] == '-') {
|
||||
arg[i] = 0;
|
||||
right = arg + i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (*left == 0 || *right == 0) {
|
||||
fprintf(stderr, ERROR("Failed to remove kazari: invalid argument")"\n");
|
||||
return 1;
|
||||
}
|
||||
uint64_t sasa_id = strtoull(left, &endptr, 16);
|
||||
if (*endptr != 0) {
|
||||
fprintf(stderr, ERROR("Failed to remove kazari: invalid sasa ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
uint64_t tanzaku_id = strtoull(right, &endptr, 16);
|
||||
if (*endptr != 0) {
|
||||
fprintf(stderr, ERROR("Failed to remove kazari: invalid tanzaku ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (tanabata_kazari_rem(&tanabata, sasa_id, tanzaku_id) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully removed kazari")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to remove kazari")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Sasa update menu handler
|
||||
int menu_upd_sasa(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
char *endptr;
|
||||
uint64_t sasa_id = strtoull(arg, &endptr, 16);
|
||||
if (*endptr == 0) {
|
||||
char *path = malloc(4096);
|
||||
printf(HIGHLIGHT("Enter the new file path (leave blank to keep current):")"\n");
|
||||
fgets(path, 4096, stdin);
|
||||
if (*path == '\n') {
|
||||
free(path);
|
||||
path = NULL;
|
||||
} else {
|
||||
path[strlen(path) - 1] = 0;
|
||||
}
|
||||
if (tanabata_sasa_upd(&tanabata, sasa_id, path) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully updated sasa")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to update sasa")"\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, ERROR("Invalid ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Tanzaku update menu handler
|
||||
int menu_upd_tanzaku(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
char *endptr;
|
||||
uint64_t tanzaku_id = strtoull(arg, &endptr, 16);
|
||||
if (*endptr == 0) {
|
||||
char *name = malloc(4096), *description = malloc(4096);
|
||||
printf(HIGHLIGHT("Enter the new name of tanzaku (leave blank to keep current):")"\n");
|
||||
fgets(name, 4096, stdin);
|
||||
if (*name == '\n') {
|
||||
free(name);
|
||||
name = NULL;
|
||||
} else {
|
||||
name[strlen(name) - 1] = 0;
|
||||
}
|
||||
printf(HIGHLIGHT("Enter the new description of tanzaku (leave blank to keep current):")"\n");
|
||||
fgets(description, 4096, stdin);
|
||||
if (*description == '\n') {
|
||||
free(description);
|
||||
description = NULL;
|
||||
} else {
|
||||
description[strlen(description) - 1] = 0;
|
||||
}
|
||||
if (tanabata_tanzaku_upd(&tanabata, tanzaku_id, name, description) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully updated tanzaku")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to update tanzaku")"\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, ERROR("Invalid ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc == 1) {
|
||||
fprintf(stderr, ERROR("No options provided")"\n");
|
||||
return 1;
|
||||
}
|
||||
char *tanabata_path;
|
||||
FILE *config = fopen(TFM_CONFIG_DIR"tfm-cli.conf", "r");
|
||||
if (config == NULL) {
|
||||
tanabata_path = NULL;
|
||||
struct stat st;
|
||||
if (stat(TFM_CONFIG_DIR, &st) == -1) {
|
||||
if (mkdir(TFM_CONFIG_DIR, 0755) != 0) {
|
||||
fprintf(stderr, ERROR("Failed to create %s directory. "
|
||||
"Try again with 'sudo' or check your permissions")"\n", TFM_CONFIG_DIR);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
config = fopen(TFM_CONFIG_DIR"tfm-cli.conf", "w");
|
||||
if (config == NULL) {
|
||||
fprintf(stderr, ERROR("Failed to create config file. "
|
||||
"Try again with 'sudo' or check your permissions")"\n");
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
fseek(config, 0L, SEEK_END);
|
||||
long fsize = ftell(config);
|
||||
rewind(config);
|
||||
if (fsize == 0) {
|
||||
tanabata_path = NULL;
|
||||
} else {
|
||||
tanabata_path = malloc(fsize + 1);
|
||||
if (fgets(tanabata_path, INT32_MAX, config) == NULL) {
|
||||
fprintf(stderr, ERROR("Failed to read config file")"\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const char *shortopts = "hI:O:isuef:t:c:w";
|
||||
char *abspath = NULL;
|
||||
int opt;
|
||||
_Bool opt_i = 0;
|
||||
_Bool opt_s = 0;
|
||||
_Bool opt_u = 0;
|
||||
_Bool opt_e = 0;
|
||||
_Bool opt_f = 0;
|
||||
_Bool opt_t = 0;
|
||||
_Bool opt_c = 0;
|
||||
_Bool opt_w = 0;
|
||||
char *opt_f_arg;
|
||||
char *opt_t_arg;
|
||||
char *opt_c_arg;
|
||||
while ((opt = getopt(argc, argv, shortopts)) != -1) {
|
||||
switch (opt) {
|
||||
case 'h':
|
||||
printf(
|
||||
HIGHLIGHT("(C) Masahiko AMANO aka H1K0, 2022—present")"\n"
|
||||
HIGHLIGHT("(https://github.com/H1K0/tanabata)")"\n\n"
|
||||
HIGHLIGHT("Usage:")"\n"
|
||||
"tfm <options>\n\n"
|
||||
HIGHLIGHT("Options:")"\n"
|
||||
HIGHLIGHT("-h")" Print this help and exit\n"
|
||||
HIGHLIGHT("-I <dir>")" Initialize new Tanabata database in directory <dir>\n"
|
||||
HIGHLIGHT("-O <dir>")" Open existing Tanabata database from directory <dir>\n"
|
||||
HIGHLIGHT("-i")" View database info\n"
|
||||
HIGHLIGHT("-s")" Set or add\n"
|
||||
HIGHLIGHT("-u")" Unset or remove\n"
|
||||
HIGHLIGHT("-e")" Edit or update\n"
|
||||
HIGHLIGHT("-f <sasa_id or path>")" File-sasa menu\n"
|
||||
HIGHLIGHT("-t <tanzaku_id or name>")" Tanzaku menu\n"
|
||||
HIGHLIGHT("-c <sasa_id>-<tanzaku_id>")" Kazari menu "
|
||||
"(can only be used with the '-s' or '-u' option)\n"
|
||||
HIGHLIGHT("-w")" Weed (defragment) database\n"
|
||||
);
|
||||
if (tanabata_path != NULL) {
|
||||
printf(HIGHLIGHT("Current database location: %s")"\n", tanabata_path);
|
||||
} else {
|
||||
printf(HIGHLIGHT("No database connected")"\n");
|
||||
}
|
||||
return 0;
|
||||
case 'I':
|
||||
abspath = realpath(optarg, abspath);
|
||||
if (abspath == NULL) {
|
||||
fprintf(stderr, ERROR("Invalid path")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (tanabata_init(&tanabata) == 0 &&
|
||||
tanabata_dump(&tanabata, abspath) == 0) {
|
||||
config = freopen(NULL, "w", config);
|
||||
if (config == NULL) {
|
||||
fprintf(stderr, ERROR("Failed to update config file. "
|
||||
"Try again with 'sudo' or check your permissions")"\n");
|
||||
return 1;
|
||||
}
|
||||
fputs(abspath, config);
|
||||
fclose(config);
|
||||
printf(SUCCESS("Successfully initialized Tanabata database")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to initialize Tanabata database")"\n");
|
||||
return 1;
|
||||
case 'O':
|
||||
abspath = realpath(optarg, abspath);
|
||||
if (abspath == NULL) {
|
||||
fprintf(stderr, ERROR("Invalid path")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (tanabata_open(&tanabata, abspath) == 0) {
|
||||
config = freopen(NULL, "w", config);
|
||||
if (config == NULL) {
|
||||
fprintf(stderr, ERROR("Failed to update config file. "
|
||||
"Try again with 'sudo' or check your permissions")"\n");
|
||||
return 1;
|
||||
}
|
||||
fputs(abspath, config);
|
||||
fclose(config);
|
||||
printf(SUCCESS("Successfully opened Tanabata database")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to open Tanabata database")"\n");
|
||||
return 1;
|
||||
case 'i':
|
||||
opt_i = 1;
|
||||
break;
|
||||
case 's':
|
||||
opt_s = 1;
|
||||
break;
|
||||
case 'u':
|
||||
opt_u = 1;
|
||||
break;
|
||||
case 'e':
|
||||
opt_e = 1;
|
||||
break;
|
||||
case 'f':
|
||||
opt_f = 1;
|
||||
opt_f_arg = optarg;
|
||||
break;
|
||||
case 't':
|
||||
opt_t = 1;
|
||||
opt_t_arg = optarg;
|
||||
break;
|
||||
case 'c':
|
||||
opt_c = 1;
|
||||
opt_c_arg = optarg;
|
||||
break;
|
||||
case 'w':
|
||||
opt_w = 1;
|
||||
break;
|
||||
case '?':
|
||||
return 1;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tanabata_path == NULL) {
|
||||
fprintf(stderr, ERROR("No connected database")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (tanabata_open(&tanabata, tanabata_path) != 0) {
|
||||
fprintf(stderr, ERROR("Failed to load database")"\n");
|
||||
return 1;
|
||||
}
|
||||
fclose(config);
|
||||
if (opt_i) {
|
||||
char datetime[20];
|
||||
printf(HIGHLIGHT("Current database location: %s")"\n\n"
|
||||
HIGHLIGHT("SASAHYOU")"\n", tanabata_path);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) &tanabata.sasahyou.created_ts));
|
||||
printf(" "HIGHLIGHT("Created")" %s\n", datetime);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) &tanabata.sasahyou.modified_ts));
|
||||
printf(" "HIGHLIGHT("Last modified")" %s\n"
|
||||
" "HIGHLIGHT("Number of sasa")" %lu\n"
|
||||
" "HIGHLIGHT("Number of holes")" %lu\n\n"
|
||||
HIGHLIGHT("SAPPYOU")"\n", datetime, tanabata.sasahyou.size, tanabata.sasahyou.hole_cnt);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) &tanabata.sappyou.created_ts));
|
||||
printf(" "HIGHLIGHT("Created")" %s\n", datetime);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) &tanabata.sappyou.modified_ts));
|
||||
printf(" "HIGHLIGHT("Last modified")" %s\n"
|
||||
" "HIGHLIGHT("Number of tanzaku")" %lu\n"
|
||||
" "HIGHLIGHT("Number of holes")" %lu\n\n"
|
||||
HIGHLIGHT("SHOPPYOU")"\n", datetime, tanabata.sappyou.size, tanabata.sappyou.hole_cnt);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) &tanabata.shoppyou.created_ts));
|
||||
printf(" "HIGHLIGHT("Created")" %s\n", datetime);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) &tanabata.shoppyou.modified_ts));
|
||||
printf(" "HIGHLIGHT("Last modified")" %s\n"
|
||||
" "HIGHLIGHT("Number of kazari")" %lu\n"
|
||||
" "HIGHLIGHT("Number of holes")" %lu\n",
|
||||
datetime, tanabata.shoppyou.size, tanabata.shoppyou.hole_cnt);
|
||||
return 0;
|
||||
}
|
||||
free(tanabata_path);
|
||||
if (opt_w) {
|
||||
if (tanabata_weed(&tanabata) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully weeded database")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to weed database")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (opt_s && opt_u) {
|
||||
opt_s = 0;
|
||||
opt_u = 0;
|
||||
}
|
||||
if (opt_s) {
|
||||
if (opt_f) {
|
||||
return menu_add_sasa(opt_f_arg);
|
||||
}
|
||||
if (opt_t) {
|
||||
return menu_add_tanzaku(opt_t_arg);
|
||||
}
|
||||
if (opt_c) {
|
||||
return menu_add_kazari(opt_c_arg);
|
||||
}
|
||||
} else if (opt_u) {
|
||||
if (opt_f) {
|
||||
return menu_rem_sasa(opt_f_arg);
|
||||
}
|
||||
if (opt_t) {
|
||||
return menu_rem_tanzaku(opt_t_arg);
|
||||
}
|
||||
if (opt_c) {
|
||||
return menu_rem_kazari(opt_c_arg);
|
||||
}
|
||||
} else if (opt_e) {
|
||||
if (opt_f) {
|
||||
return menu_upd_sasa(opt_f_arg);
|
||||
}
|
||||
if (opt_t) {
|
||||
return menu_upd_tanzaku(opt_t_arg);
|
||||
}
|
||||
} else {
|
||||
if (opt_f) {
|
||||
return menu_view_sasa(opt_f_arg);
|
||||
}
|
||||
if (opt_t) {
|
||||
return menu_view_tanzaku(opt_t_arg);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script performs the installation of Tanabata web server
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
|
||||
|
||||
../tdbms/install.sh || exit 1
|
||||
|
||||
usermod -a -G tanabata www-data
|
||||
|
||||
if [ ! -d /var/lib/tanabata/tweb ]; then
|
||||
mkdir /var/lib/tanabata/tweb
|
||||
if [ ! -d /var/lib/tanabata/tweb ]; then
|
||||
echo "FATAL: failed to create directory '/var/lib/tanabata/tweb'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
chown 42776:42776 /var/lib/tanabata/tweb
|
||||
chmod 2755 /var/lib/tanabata/tweb
|
||||
|
||||
if [ -d ../build ]; then
|
||||
rm -r ../build/*
|
||||
else
|
||||
mkdir ../build
|
||||
if [ -d ../build ]; then
|
||||
echo "FATAL: failed to create build directory"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
cd ./server
|
||||
echo "Building Tweb server..."
|
||||
if ! go build -o ../build; then
|
||||
echo "FATAL: failed to build Tweb server"
|
||||
exit 1
|
||||
fi
|
||||
cd ..
|
||||
mv -f ../build/tweb /usr/bin/
|
||||
chown 0:0 /usr/bin/tweb
|
||||
chmod 0755 /usr/bin/tweb
|
||||
|
||||
if ! cp ./tweb.service /etc/systemd/system/; then
|
||||
echo "FATAL: failed to copy 'tweb.service' to '/etc/systemd/system'"
|
||||
exit 1
|
||||
fi
|
||||
chown 0:0 /etc/systemd/system/tweb.service
|
||||
chmod 0644 /etc/systemd/system/tweb.service
|
||||
|
||||
if ! cp -r ./public/* /srv/www/tanabata/; then
|
||||
echo "FATAL: failed to copy public files to '/srv/www/tanabata'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Tweb server successfully installed."
|
||||
echo "Start it with 'systemctl start tweb'"
|
||||
@@ -1,51 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Authentication | Tanabata</title>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/tanabata.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#615880">
|
||||
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#615880">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/general.css">
|
||||
<link rel="stylesheet" href="/css/auth.css">
|
||||
<script src="/js/jquery-3.6.0.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Welcome to Tanabata!</h1>
|
||||
</header>
|
||||
<main>
|
||||
<div class="contents-wrapper">
|
||||
<form id="auth">
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" class="form-control" maxlength="32" placeholder="Password">
|
||||
<div class="invalid-feedback">Invalid password!</div>
|
||||
<div class="valid-feedback">Authorization success!</div>
|
||||
</div>
|
||||
<div class="form-group button-flex">
|
||||
<button type="submit" class="btn btn-primary" id="submit">Submit</button>
|
||||
<a href="/" class="btn btn-secondary">Back to home</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
<script src="js/auth.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig><msapplication><tile><square70x70logo src="/images/ms-icon-70x70.png"/><square150x150logo src="/images/ms-icon-150x150.png"/><square310x310logo src="/images/ms-icon-310x310.png"/><TileColor>#5c913b</TileColor></tile></msapplication></browserconfig>
|
||||
@@ -1,3 +0,0 @@
|
||||
.btn-secondary {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Epilogue&family=Secular+One&display=swap');
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #eee;
|
||||
background-color: #2d2b40;
|
||||
background-image: url(/images/bg-1920x1440-dark.webp);
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
header {
|
||||
margin: 0;
|
||||
margin-top: 2vw;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 12px 0;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font-family: Epilogue, sans-serif;
|
||||
font-size: 10vmin;
|
||||
text-shadow: 0 0 8px #555;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
h1 a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
h1 a:hover {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
main {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #3348;
|
||||
box-shadow: 0 0 0.5vw black;
|
||||
border-radius: 16px;
|
||||
width: 80vw;
|
||||
max-width: 700px;
|
||||
transition: 0.3s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
main:hover {
|
||||
background-color: #334;
|
||||
box-shadow: 0 0 1vw black;
|
||||
}
|
||||
|
||||
.contents-wrapper {
|
||||
margin: 0;
|
||||
padding: 2vw 2vw;
|
||||
flex: 1 1 auto;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
padding: 14px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: hsla(270, 30%, 60%, 0.6);
|
||||
border-bottom: 0;
|
||||
color: #111;
|
||||
font-family: Secular One, sans-serif;
|
||||
font-size: 5.5vmin;
|
||||
text-shadow: 2.5px 2px 0.5px #ddd;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
margin-top: 0.5vw;
|
||||
font-family: Secular One, sans-serif;
|
||||
font-size: 3vmax;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: .375rem 2.25rem .375rem .75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: .25rem;
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-control:focus {
|
||||
color: #ddd !important;
|
||||
background-color: #445;
|
||||
}
|
||||
|
||||
.form-control::placeholder,
|
||||
.form-control::-webkit-input-placeholder {
|
||||
color: #bbb !important;
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.button-flex {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 10px;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
html,
|
||||
body {
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100vw;
|
||||
background-color: #2c3034;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
main:hover {
|
||||
background-color: #2c3034;
|
||||
}
|
||||
|
||||
.contents-wrapper {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: inset -5px 5px 5px #1111, inset -5px -5px 5px #1111;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.contents-wrapper:after {
|
||||
content: "";
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.button-flex {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding-top: .5rem;
|
||||
padding-bottom: .8rem;
|
||||
background-color: #334;
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
html,
|
||||
body {
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100vw;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.contents-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
box-shadow: inset -5px 5px 5px #1111, inset -5px -5px 5px #1111;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.contents-wrapper:after {
|
||||
content: "";
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.menu-wrapper {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
background-color: #0008;
|
||||
}
|
||||
|
||||
.menu {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-width: 120vmin;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
background-color: #334;
|
||||
box-shadow: 0 0 0.5vw black;
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
transition: 0.3s;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#preview {
|
||||
width: 100%;
|
||||
max-height: 60vh;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.file-nav-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#file-prev {
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
#file-next {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
form {
|
||||
flex: 1 1 auto;
|
||||
padding: 2vw 2vw;
|
||||
}
|
||||
|
||||
.form-group.row {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.col-form-label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.col-form-input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1 1 auto;
|
||||
height: 50vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 0;
|
||||
box-shadow: inset -5px 5px 5px #1111, inset -5px -5px 5px #1111;
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.list:after {
|
||||
content: "";
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.sasa {
|
||||
position: relative;
|
||||
margin: 8px;
|
||||
padding: 0;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sasa .thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.sasa .overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: #0002;
|
||||
}
|
||||
|
||||
.sasa:hover .overlay {
|
||||
background-color: #0004;
|
||||
}
|
||||
|
||||
.sasa.selected .overlay {
|
||||
background-color: #0006;
|
||||
}
|
||||
|
||||
.tanzaku {
|
||||
margin: 5px;
|
||||
padding: 7px;
|
||||
border: 1px solid #555;
|
||||
border-radius: 7px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tanzaku:hover,
|
||||
.tanzaku.selected:hover {
|
||||
background-color: #0006;
|
||||
}
|
||||
|
||||
.tanzaku.selected {
|
||||
background-color: #0004;
|
||||
}
|
||||
|
||||
.button-flex {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding-top: .5rem;
|
||||
padding-bottom: .8rem;
|
||||
background-color: #334;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 421 KiB |
|
Before Width: | Height: | Size: 712 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -1,39 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Home | Tanabata</title>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/tanabata.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#615880">
|
||||
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#615880">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/general.css">
|
||||
<script src="/js/jquery-3.6.0.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome to Tanabata!</h1>
|
||||
<main>
|
||||
<h2>Select Tanabata service</h2>
|
||||
<div class="contents-wrapper button-flex">
|
||||
<a href="/auth" class="btn btn-primary">Authorize</a>
|
||||
<a href="/tfm" class="btn btn-primary">File Manager</a>
|
||||
<a href="/tdbms" class="btn btn-primary">Database Management</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,37 +0,0 @@
|
||||
$("#auth").on("submit", function submit(e) {
|
||||
e.preventDefault();
|
||||
var input_password = $("#password");
|
||||
let password = input_password.val();
|
||||
input_password.val("");
|
||||
$.ajax({
|
||||
url: "/AUTH",
|
||||
type: "POST",
|
||||
contentType: "text/plain",
|
||||
data: password,
|
||||
dataType: "json",
|
||||
success: function (resp) {
|
||||
if (resp.status) {
|
||||
input_password.removeClass("is-invalid");
|
||||
input_password.addClass("is-valid");
|
||||
$(".btn-secondary").css("display", "block");
|
||||
} else {
|
||||
input_password.removeClass("is-valid");
|
||||
input_password.addClass("is-invalid");
|
||||
}
|
||||
},
|
||||
failure: function (err) {
|
||||
alert(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).keyup(function (e) {
|
||||
switch (e.key) {
|
||||
case "Esc":
|
||||
case "Escape":
|
||||
location.href = "/";
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
/*!
|
||||
* jQuery Cookie Plugin v1.4.1
|
||||
* https://github.com/carhartl/jquery-cookie
|
||||
*
|
||||
* Copyright 2013 Klaus Hartl
|
||||
* Released under the MIT license
|
||||
*/
|
||||
!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):e("object"==typeof exports?require("jquery"):jQuery)}(function(e){var i=/\+/g;function o(e){return t.raw?e:encodeURIComponent(e)}function r(e){return t.raw?e:decodeURIComponent(e)}function n(o,r){var n=t.raw?o:function e(o){0===o.indexOf('"')&&(o=o.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return o=decodeURIComponent(o.replace(i," ")),t.json?JSON.parse(o):o}catch(r){}}(o);return e.isFunction(r)?r(n):n}var t=e.cookie=function(i,c,u){if(void 0!==c&&!e.isFunction(c)){if("number"==typeof(u=e.extend({},t.defaults,u)).expires){var a,s=u.expires,f=u.expires=new Date;f.setTime(+f+864e5*s)}return document.cookie=[o(i),"=",(a=c,o(t.json?JSON.stringify(a):String(a))),u.expires?"; expires="+u.expires.toUTCString():"",u.path?"; path="+u.path:"",u.domain?"; domain="+u.domain:"",u.secure?"; secure":""].join("")}for(var p=i?void 0:{},d=document.cookie?document.cookie.split("; "):[],v=0,x=d.length;v<x;v++){var k=d[v].split("="),l=r(k.shift()),j=k.join("=");if(i&&i===l){p=n(j,c);break}i||void 0===(j=n(j))||(p[l]=j)}return p};t.defaults={},e.removeCookie=function(i,o){return void 0!==e.cookie(i)&&(e.cookie(i,"",e.extend({},o,{expires:-1})),!e.cookie(i))}});
|
||||
@@ -1,2 +0,0 @@
|
||||
/*! jQuery & Zepto Lazy v1.7.10 - http://jquery.eisbehr.de/lazy - MIT&GPL-2.0 license - Copyright 2012-2018 Daniel 'Eisbehr' Kern */
|
||||
!function(t,e){"use strict";function r(r,a,i,u,l){function f(){L=t.devicePixelRatio>1,i=c(i),a.delay>=0&&setTimeout(function(){s(!0)},a.delay),(a.delay<0||a.combined)&&(u.e=v(a.throttle,function(t){"resize"===t.type&&(w=B=-1),s(t.all)}),u.a=function(t){t=c(t),i.push.apply(i,t)},u.g=function(){return i=n(i).filter(function(){return!n(this).data(a.loadedName)})},u.f=function(t){for(var e=0;e<t.length;e++){var r=i.filter(function(){return this===t[e]});r.length&&s(!1,r)}},s(),n(a.appendScroll).on("scroll."+l+" resize."+l,u.e))}function c(t){var i=a.defaultImage,o=a.placeholder,u=a.imageBase,l=a.srcsetAttribute,f=a.loaderAttribute,c=a._f||{};t=n(t).filter(function(){var t=n(this),r=m(this);return!t.data(a.handledName)&&(t.attr(a.attribute)||t.attr(l)||t.attr(f)||c[r]!==e)}).data("plugin_"+a.name,r);for(var s=0,d=t.length;s<d;s++){var A=n(t[s]),g=m(t[s]),h=A.attr(a.imageBaseAttribute)||u;g===N&&h&&A.attr(l)&&A.attr(l,b(A.attr(l),h)),c[g]===e||A.attr(f)||A.attr(f,c[g]),g===N&&i&&!A.attr(E)?A.attr(E,i):g===N||!o||A.css(O)&&"none"!==A.css(O)||A.css(O,"url('"+o+"')")}return t}function s(t,e){if(!i.length)return void(a.autoDestroy&&r.destroy());for(var o=e||i,u=!1,l=a.imageBase||"",f=a.srcsetAttribute,c=a.handledName,s=0;s<o.length;s++)if(t||e||A(o[s])){var g=n(o[s]),h=m(o[s]),b=g.attr(a.attribute),v=g.attr(a.imageBaseAttribute)||l,p=g.attr(a.loaderAttribute);g.data(c)||a.visibleOnly&&!g.is(":visible")||!((b||g.attr(f))&&(h===N&&(v+b!==g.attr(E)||g.attr(f)!==g.attr(F))||h!==N&&v+b!==g.css(O))||p)||(u=!0,g.data(c,!0),d(g,h,v,p))}u&&(i=n(i).filter(function(){return!n(this).data(c)}))}function d(t,e,r,i){++z;var o=function(){y("onError",t),p(),o=n.noop};y("beforeLoad",t);var u=a.attribute,l=a.srcsetAttribute,f=a.sizesAttribute,c=a.retinaAttribute,s=a.removeAttribute,d=a.loadedName,A=t.attr(c);if(i){var g=function(){s&&t.removeAttr(a.loaderAttribute),t.data(d,!0),y(T,t),setTimeout(p,1),g=n.noop};t.off(I).one(I,o).one(D,g),y(i,t,function(e){e?(t.off(D),g()):(t.off(I),o())})||t.trigger(I)}else{var h=n(new Image);h.one(I,o).one(D,function(){t.hide(),e===N?t.attr(C,h.attr(C)).attr(F,h.attr(F)).attr(E,h.attr(E)):t.css(O,"url('"+h.attr(E)+"')"),t[a.effect](a.effectTime),s&&(t.removeAttr(u+" "+l+" "+c+" "+a.imageBaseAttribute),f!==C&&t.removeAttr(f)),t.data(d,!0),y(T,t),h.remove(),p()});var m=(L&&A?A:t.attr(u))||"";h.attr(C,t.attr(f)).attr(F,t.attr(l)).attr(E,m?r+m:null),h.complete&&h.trigger(D)}}function A(t){var e=t.getBoundingClientRect(),r=a.scrollDirection,n=a.threshold,i=h()+n>e.top&&-n<e.bottom,o=g()+n>e.left&&-n<e.right;return"vertical"===r?i:"horizontal"===r?o:i&&o}function g(){return w>=0?w:w=n(t).width()}function h(){return B>=0?B:B=n(t).height()}function m(t){return t.tagName.toLowerCase()}function b(t,e){if(e){var r=t.split(",");t="";for(var a=0,n=r.length;a<n;a++)t+=e+r[a].trim()+(a!==n-1?",":"")}return t}function v(t,e){var n,i=0;return function(o,u){function l(){i=+new Date,e.call(r,o)}var f=+new Date-i;n&&clearTimeout(n),f>t||!a.enableThrottle||u?l():n=setTimeout(l,t-f)}}function p(){--z,i.length||z||y("onFinishedAll")}function y(t,e,n){return!!(t=a[t])&&(t.apply(r,[].slice.call(arguments,1)),!0)}var z=0,w=-1,B=-1,L=!1,T="afterLoad",D="load",I="error",N="img",E="src",F="srcset",C="sizes",O="background-image";"event"===a.bind||o?f():n(t).on(D+"."+l,f)}function a(a,o){var u=this,l=n.extend({},u.config,o),f={},c=l.name+"-"+ ++i;return u.config=function(t,r){return r===e?l[t]:(l[t]=r,u)},u.addItems=function(t){return f.a&&f.a("string"===n.type(t)?n(t):t),u},u.getItems=function(){return f.g?f.g():{}},u.update=function(t){return f.e&&f.e({},!t),u},u.force=function(t){return f.f&&f.f("string"===n.type(t)?n(t):t),u},u.loadAll=function(){return f.e&&f.e({all:!0},!0),u},u.destroy=function(){return n(l.appendScroll).off("."+c,f.e),n(t).off("."+c),f={},e},r(u,l,a,f,c),l.chainable?a:u}var n=t.jQuery||t.Zepto,i=0,o=!1;n.fn.Lazy=n.fn.lazy=function(t){return new a(this,t)},n.Lazy=n.lazy=function(t,r,i){if(n.isFunction(r)&&(i=r,r=[]),n.isFunction(i)){t=n.isArray(t)?t:[t],r=n.isArray(r)?r:[r];for(var o=a.prototype.config,u=o._f||(o._f={}),l=0,f=t.length;l<f;l++)(o[t[l]]===e||n.isFunction(o[t[l]]))&&(o[t[l]]=i);for(var c=0,s=r.length;c<s;c++)u[r[c]]=t[0]}},a.prototype.config={name:"lazy",chainable:!0,autoDestroy:!0,bind:"load",threshold:500,visibleOnly:!1,appendScroll:t,scrollDirection:"both",imageBase:null,defaultImage:"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==",placeholder:null,delay:-1,combined:!1,attribute:"data-src",srcsetAttribute:"data-srcset",sizesAttribute:"data-sizes",retinaAttribute:"data-retina",loaderAttribute:"data-loader",imageBaseAttribute:"data-imagebase",removeAttribute:!0,handledName:"handled",loadedName:"loaded",effect:"show",effectTime:0,enableThrottle:!0,throttle:250,beforeLoad:e,afterLoad:e,onError:e,onFinishedAll:e},n(t).on("load",function(){o=!0})}(window);
|
||||
@@ -1,52 +0,0 @@
|
||||
$(document).on("click", "#btn-save", function (e) {
|
||||
e.preventDefault();
|
||||
if (db_name == null) {
|
||||
return;
|
||||
}
|
||||
let resp = tdb_query(db_name, 4);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
alert("Successfully saved!");
|
||||
});
|
||||
|
||||
$(document).on("click", "#btn-reload", function (e) {
|
||||
e.preventDefault();
|
||||
if (db_name == null) {
|
||||
return;
|
||||
}
|
||||
if (!confirm("All unsaved changes will be lost permanently. Are you sure?")) {
|
||||
return;
|
||||
}
|
||||
let resp = tdb_query(db_name, 2);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
localStorage["sasahyou_mts"] = sasahyou_mts = 0;
|
||||
localStorage["sappyou_mts"] = sappyou_mts = 0;
|
||||
localStorage["shoppyou_mts"] = shoppyou_mts = 0;
|
||||
alert("Successfully reloaded database!");
|
||||
});
|
||||
|
||||
$(document).on("click", "#btn-remove", function (e) {
|
||||
e.preventDefault();
|
||||
if (db_name == null) {
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Are you sure want to remove database "${db_name}"?`)) {
|
||||
return;
|
||||
}
|
||||
let resp = tdb_query(db_name, 1);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
localStorage.removeItem("db_name");
|
||||
db_name = null;
|
||||
localStorage["sasahyou_mts"] = sasahyou_mts = 0;
|
||||
localStorage["sappyou_mts"] = sappyou_mts = 0;
|
||||
localStorage["shoppyou_mts"] = shoppyou_mts = 0;
|
||||
alert("Successfully removed database!");
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
var db_name = localStorage["db_name"];
|
||||
|
||||
$(window).on("load", function (e) {
|
||||
if (db_name != null) {
|
||||
$(".db_name").text(db_name);
|
||||
}
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
db_name = localStorage["db_name"];
|
||||
if (db_name == null) {
|
||||
location.href = "/tdbms/settings";
|
||||
}
|
||||
|
||||
$(window).on("load", function (e) {
|
||||
$(".db_name").text(db_name);
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
$(document).on("submit", "#newdb", function (e) {
|
||||
e.preventDefault();
|
||||
let newdb_name = $("#newdb-name").val(), newdb_path = $("#newdb-path").val();
|
||||
let resp = tdb_query(newdb_name, 3);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Failed to initialize database!");
|
||||
return;
|
||||
}
|
||||
resp = tdb_query(newdb_name, 4, newdb_path);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Failed to save database!");
|
||||
return;
|
||||
}
|
||||
resp = tdb_query(newdb_name, 6, "path=" + newdb_path);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Failed to finalize database!");
|
||||
return;
|
||||
}
|
||||
alert("Successfully added database!");
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
$(window).on("load", function (e) {
|
||||
sappyou_load();
|
||||
sappyou.every(tanzaku => {
|
||||
$("#content").append(
|
||||
`<tr><td>${tanzaku.id}</td><td>${new Date(tanzaku.cts * 1000).toLocaleDateString()} ${new Date(tanzaku.cts * 1000).toLocaleTimeString()}</td><td>${new Date(tanzaku.mts * 1000).toLocaleDateString()} ${new Date(tanzaku.mts * 1000).toLocaleTimeString()}</td><td>${tanzaku.name}</td><td>${tanzaku.desc}</td></tr>`
|
||||
);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
$(window).on("load", function (e) {
|
||||
sasahyou_load();
|
||||
sasahyou.every(sasa => {
|
||||
$("#content").append(
|
||||
`<tr><td>${sasa.id}</td><td>${new Date(sasa.cts * 1000).toLocaleDateString()} ${new Date(sasa.cts * 1000).toLocaleTimeString()}</td><td>${sasa.path}</td></tr>`
|
||||
);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
function settings_load() {
|
||||
if (db_name != null) {
|
||||
$(`#db_name option[value="${db_name}"]`).prop("selected", true);
|
||||
} else {
|
||||
$("#db_name option[value=\"\"]").prop("selected", true);
|
||||
}
|
||||
if (sort_sasa != null) {
|
||||
let sort_s = sort_sasa;
|
||||
if (sort_s[0] === '!') {
|
||||
sort_s = sort_s.slice(1);
|
||||
}
|
||||
if (sort_s[0] === '-') {
|
||||
$("#sasa-reverse").prop("checked", true);
|
||||
sort_s = sort_s.slice(1);
|
||||
}
|
||||
$(`#sasa-by-${sort_s}`).prop("checked", true);
|
||||
}
|
||||
if (sort_tanzaku != null) {
|
||||
let sort_t = sort_tanzaku;
|
||||
if (sort_t[0] === '!') {
|
||||
sort_t = sort_t.slice(1);
|
||||
}
|
||||
if (sort_t[0] === '-') {
|
||||
$("#tanzaku-reverse").prop("checked", true);
|
||||
sort_t = sort_t.slice(1);
|
||||
}
|
||||
$(`#tanzaku-by-${sort_t}`).prop("checked", true);
|
||||
}
|
||||
}
|
||||
|
||||
$(window).on("load", function () {
|
||||
let resp = tdb_query();
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Failed to fetch databases");
|
||||
throw new Error("Failed to fetch databases");
|
||||
}
|
||||
resp.data.every(tdb => {
|
||||
$("#db_name").append($("<option>", {
|
||||
value: tdb.name,
|
||||
text: tdb.name
|
||||
}));
|
||||
return true;
|
||||
});
|
||||
settings_load();
|
||||
});
|
||||
|
||||
$(document).on("reset", "#settings", function (e) {
|
||||
e.preventDefault();
|
||||
settings_load();
|
||||
});
|
||||
|
||||
$(document).on("submit", "#settings", function (e) {
|
||||
e.preventDefault();
|
||||
let db_name_input = $("#db_name");
|
||||
let db_name_val = db_name_input.val();
|
||||
if (db_name_val !== db_name) {
|
||||
localStorage["db_name"] = db_name = db_name_val;
|
||||
localStorage["sasahyou_mts"] = sasahyou_mts = 0;
|
||||
localStorage["sappyou_mts"] = sappyou_mts = 0;
|
||||
localStorage["shoppyou_mts"] = shoppyou_mts = 0;
|
||||
}
|
||||
let sort_s = ($("#sasa-reverse")[0].checked ? '-' : '') + $("input[type=radio][name=sort-sasa]:checked").attr("id").slice(8);
|
||||
let sort_t = ($("#tanzaku-reverse")[0].checked ? '-' : '') + $("input[type=radio][name=sort-tanzaku]:checked").attr("id").slice(11);
|
||||
if (sort_s !== sort_sasa && '!' + sort_s !== sort_sasa) {
|
||||
localStorage["sort_sasa"] = sort_sasa = '!' + sort_s;
|
||||
}
|
||||
if (sort_t !== sort_tanzaku && '!' + sort_t !== sort_tanzaku) {
|
||||
localStorage["sort_tanzaku"] = sort_tanzaku = '!' + sort_t;
|
||||
}
|
||||
alert("Successfully updated settings!");
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
$(window).on("load", function (e) {
|
||||
shoppyou_load();
|
||||
shoppyou.every(kazari => {
|
||||
$("#content").append(
|
||||
`<tr><td>${new Date(kazari.cts * 1000).toLocaleDateString()} ${new Date(kazari.cts * 1000).toLocaleTimeString()}</td><td>${kazari.sasa_id}</td><td>${kazari.tanzaku_id}</td></tr>`
|
||||
);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
$(window).on("load", function (e) {
|
||||
let resp = tdb_query(db_name);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Failed to fetch database");
|
||||
throw new Error("Failed to fetch database");
|
||||
}
|
||||
$("#stats-sasahyou").append(
|
||||
`<tr><td>${new Date(resp.data[0].sasahyou.cts * 1000).toLocaleDateString()} ${new Date(resp.data[0].sasahyou.cts * 1000).toLocaleTimeString()}</td><td>${new Date(resp.data[0].sasahyou.mts * 1000).toLocaleDateString()} ${new Date(resp.data[0].sasahyou.mts * 1000).toLocaleTimeString()}</td><td>${resp.data[0].sasahyou.size}</td><td>${resp.data[0].sasahyou.holes}</td></tr>`
|
||||
);
|
||||
$("#stats-sappyou").append(
|
||||
`<tr><td>${new Date(resp.data[0].sappyou.cts * 1000).toLocaleDateString()} ${new Date(resp.data[0].sappyou.cts * 1000).toLocaleTimeString()}</td><td>${new Date(resp.data[0].sappyou.mts * 1000).toLocaleDateString()} ${new Date(resp.data[0].sappyou.mts * 1000).toLocaleTimeString()}</td><td>${resp.data[0].sappyou.size}</td><td>${resp.data[0].sappyou.holes}</td></tr>`
|
||||
);
|
||||
$("#stats-shoppyou").append(
|
||||
`<tr><td>${new Date(resp.data[0].shoppyou.cts * 1000).toLocaleDateString()} ${new Date(resp.data[0].shoppyou.cts * 1000).toLocaleTimeString()}</td><td>${new Date(resp.data[0].shoppyou.mts * 1000).toLocaleDateString()} ${new Date(resp.data[0].shoppyou.mts * 1000).toLocaleTimeString()}</td><td>${resp.data[0].shoppyou.size}</td><td>${resp.data[0].shoppyou.holes}</td></tr>`
|
||||
);
|
||||
});
|
||||
@@ -1,204 +0,0 @@
|
||||
var db_name = null;
|
||||
var sasahyou = localStorage["sasahyou"],
|
||||
sappyou = localStorage["sappyou"],
|
||||
shoppyou = localStorage["shoppyou"];
|
||||
var sort_sasa = localStorage["sort_sasa"],
|
||||
sort_tanzaku = localStorage["sort_tanzaku"];
|
||||
if (sasahyou != null) {
|
||||
sasahyou = JSON.parse(sasahyou);
|
||||
}
|
||||
if (sappyou != null) {
|
||||
sappyou = JSON.parse(sappyou);
|
||||
}
|
||||
if (shoppyou != null) {
|
||||
shoppyou = JSON.parse(shoppyou);
|
||||
}
|
||||
var sasahyou_mts = localStorage["sasahyou_mts"],
|
||||
sappyou_mts = localStorage["sappyou_mts"],
|
||||
shoppyou_mts = localStorage["shoppyou_mts"];
|
||||
if (sasahyou_mts != null) {
|
||||
sasahyou_mts = parseInt(sasahyou_mts);
|
||||
}
|
||||
if (sappyou_mts != null) {
|
||||
sappyou_mts = parseInt(sappyou_mts);
|
||||
}
|
||||
if (shoppyou_mts != null) {
|
||||
shoppyou_mts = parseInt(shoppyou_mts);
|
||||
}
|
||||
if (sort_sasa == null) {
|
||||
localStorage["sort_sasa"] = sort_sasa = "id";
|
||||
}
|
||||
if (sort_tanzaku == null) {
|
||||
localStorage["sort_tanzaku"] = sort_tanzaku = "id";
|
||||
}
|
||||
|
||||
function tdb_query(trdb, trc, trb) {
|
||||
if (trb == null) {
|
||||
trb = "";
|
||||
}
|
||||
if (trc == null) {
|
||||
trc = 0;
|
||||
}
|
||||
if (trdb == null) {
|
||||
trdb = "";
|
||||
}
|
||||
let output = null;
|
||||
$.ajax({
|
||||
url: "/TDBMS",
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
data: `{"trdb":${JSON.stringify(trdb)},"trc":${trc},"trb":${JSON.stringify(trb)}}`,
|
||||
dataType: "json",
|
||||
async: false,
|
||||
statusCode: {
|
||||
401: function () {
|
||||
location.href = "/auth";
|
||||
throw new Error("Unauthorized TDBMS request");
|
||||
}
|
||||
},
|
||||
success: function (resp) {
|
||||
output = resp;
|
||||
},
|
||||
failure: function (err) {
|
||||
alert(err);
|
||||
}
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
function sasahyou_load() {
|
||||
let db_info = tdb_query(db_name);
|
||||
if (db_info == null || !db_info.status) {
|
||||
alert("Failed to fetch database");
|
||||
throw new Error("Failed to fetch database");
|
||||
}
|
||||
if (sasahyou == null || sasahyou_mts !== db_info.data[0].sasahyou.mts) {
|
||||
let resp = tdb_query(db_name, 16);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Failed to get sasahyou");
|
||||
throw new Error("Failed to get sasahyou");
|
||||
}
|
||||
sasahyou = resp.data;
|
||||
localStorage["sasahyou_mts"] = sasahyou_mts = db_info.data[0].sasahyou.mts;
|
||||
localStorage["sasahyou"] = JSON.stringify(sasahyou);
|
||||
if (sort_sasa[0] !== '!') {
|
||||
sort_sasa = '!' + sort_sasa;
|
||||
}
|
||||
}
|
||||
sasahyou_sort();
|
||||
}
|
||||
|
||||
function sappyou_load() {
|
||||
let db_info = tdb_query(db_name);
|
||||
if (db_info == null || !db_info.status) {
|
||||
alert("Failed to fetch database");
|
||||
throw new Error("Failed to fetch database");
|
||||
}
|
||||
if (sappyou == null || sappyou_mts !== db_info.data[0].sappyou.mts) {
|
||||
let resp = tdb_query(db_name, 32);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Failed to get sappyou");
|
||||
throw new Error("Failed to get sappyou");
|
||||
}
|
||||
sappyou = resp.data;
|
||||
localStorage["sappyou_mts"] = sappyou_mts = db_info.data[0].sappyou.mts;
|
||||
localStorage["sappyou"] = JSON.stringify(sappyou);
|
||||
if (sort_tanzaku[0] !== '!') {
|
||||
sort_tanzaku = '!' + sort_tanzaku;
|
||||
}
|
||||
}
|
||||
sappyou_sort();
|
||||
}
|
||||
|
||||
function shoppyou_load() {
|
||||
let db_info = tdb_query(db_name);
|
||||
if (db_info == null || !db_info.status) {
|
||||
alert("Failed to fetch database");
|
||||
throw new Error("Failed to fetch database");
|
||||
}
|
||||
if (shoppyou == null || shoppyou_mts !== db_info.data[0].shoppyou.mts) {
|
||||
let resp = tdb_query(db_name, 8);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Failed to get shoppyou");
|
||||
throw new Error("Failed to get shoppyou");
|
||||
}
|
||||
shoppyou = resp.data;
|
||||
localStorage["shoppyou_mts"] = shoppyou_mts = db_info.data[0].shoppyou.mts;
|
||||
localStorage["shoppyou"] = JSON.stringify(shoppyou);
|
||||
}
|
||||
}
|
||||
|
||||
function sasahyou_sort() {
|
||||
if (sort_sasa[0] !== '!') {
|
||||
return;
|
||||
}
|
||||
let sort = localStorage["sort_sasa"] = sort_sasa = sort_sasa.slice(1);
|
||||
let order = 1;
|
||||
if (sort[0] === '-') {
|
||||
order = -1;
|
||||
sort = sort.slice(1);
|
||||
}
|
||||
sasahyou.sort((lhs, rhs) => {
|
||||
let l = lhs[sort], r = rhs[sort];
|
||||
if (l > r) {
|
||||
return order;
|
||||
}
|
||||
if (l < r) {
|
||||
return -order;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
localStorage["sasahyou"] = JSON.stringify(sasahyou);
|
||||
}
|
||||
|
||||
function sappyou_sort() {
|
||||
if (sort_tanzaku[0] !== '!') {
|
||||
return;
|
||||
}
|
||||
let sort = localStorage["sort_tanzaku"] = sort_tanzaku = sort_tanzaku.slice(1);
|
||||
let order = 1;
|
||||
if (sort[0] === '-') {
|
||||
order = -1;
|
||||
sort = sort.slice(1);
|
||||
}
|
||||
if (sort === "nkazari") {
|
||||
shoppyou_load();
|
||||
shoppyou.every(kazari => {
|
||||
sappyou.every((tanzaku, index) => {
|
||||
if (tanzaku.id === kazari.tanzaku_id) {
|
||||
if (tanzaku.nkazari == null) {
|
||||
sappyou[index].nkazari = 1;
|
||||
} else {
|
||||
sappyou[index].nkazari++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
});
|
||||
sappyou.every((tanzaku, index) => {
|
||||
if (tanzaku.nkazari == null) {
|
||||
sappyou[index].nkazari = 0;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
sappyou.sort((lhs, rhs) => {
|
||||
if (lhs.id === 0) {
|
||||
return -1;
|
||||
}
|
||||
if (rhs.id === 0) {
|
||||
return 1;
|
||||
}
|
||||
let l = lhs[sort], r = rhs[sort];
|
||||
if (l > r) {
|
||||
return order;
|
||||
}
|
||||
if (l < r) {
|
||||
return -order;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
localStorage["sappyou"] = JSON.stringify(sappyou);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
var db_name = localStorage["tfm_db_name"];
|
||||
if (db_name == null) {
|
||||
location.href = "/tfm/settings";
|
||||
}
|
||||
|
||||
$(document).on("click", "#btn-save", function (e) {
|
||||
e.preventDefault();
|
||||
if (db_name == null) {
|
||||
return;
|
||||
}
|
||||
let resp = tdb_query(db_name, 4);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
alert("Successfully saved!");
|
||||
});
|
||||
|
||||
$(document).on("click", "#btn-reload", function (e) {
|
||||
e.preventDefault();
|
||||
if (db_name == null) {
|
||||
return;
|
||||
}
|
||||
if (!confirm("All unsaved changes will be lost permanently. Are you sure?")) {
|
||||
return;
|
||||
}
|
||||
let resp = tdb_query(db_name, 2);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
localStorage["sasahyou_mts"] = sasahyou_mts = 0;
|
||||
localStorage["sappyou_mts"] = sappyou_mts = 0;
|
||||
localStorage["shoppyou_mts"] = shoppyou_mts = 0;
|
||||
alert("Successfully reloaded database!");
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
$(window).on("load", function () {
|
||||
$(function () {
|
||||
$(".thumb").Lazy({
|
||||
scrollDirection: "vertical",
|
||||
effect: "fadeIn",
|
||||
visibleOnly: true,
|
||||
appendScroll: $(".contents-wrapper")[0],
|
||||
});
|
||||
});
|
||||
sasahyou_load();
|
||||
sasahyou.forEach((sasa) => {
|
||||
$(".contents-wrapper").append(`<div class="item sasa" sid="${sasa.id}" title="${sasa.path.split('/').slice(-1)}"><img class="thumb" data-src="${"/thumbs/" + sasa.path}"><div class="overlay"></div></div>`);
|
||||
$("#menu-tag-view .list").append(`<div class="list-item sasa" sid="${sasa.id}" title="${sasa.path.split('/').slice(-1)}"><img class="thumb" data-src="${"/thumbs/" + sasa.path}"><div class="overlay"></div></div>`);
|
||||
});
|
||||
sappyou_load();
|
||||
sappyou.forEach((tanzaku) => {
|
||||
$("#menu-file-view .list").append(`<div class="list-item tanzaku" tid="${tanzaku.id}">${tanzaku.name}</div>`);
|
||||
});
|
||||
lazy_menu = $("#menu-tag-view .thumb").lazy({
|
||||
chainable: false,
|
||||
scrollDirection: "vertical",
|
||||
effect: "fadeIn",
|
||||
visibleOnly: true,
|
||||
appendScroll: $("#menu-tag-view .list")[0],
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on("submit", "#menu-add form", function (e) {
|
||||
e.preventDefault();
|
||||
let resp = tdb_query(db_name, 18, $("#new-name").val());
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
menu_add_close();
|
||||
location.reload(true);
|
||||
});
|
||||
@@ -1,364 +0,0 @@
|
||||
db_name = localStorage["tfm_db_name"];
|
||||
if (db_name == null) {
|
||||
location.href = "/tfm/settings";
|
||||
}
|
||||
sort_sasa = localStorage["sort_files"];
|
||||
sort_tanzaku = localStorage["sort_tags"];
|
||||
if (sort_sasa == null) {
|
||||
localStorage["sort_files"] = sort_sasa = "id";
|
||||
}
|
||||
if (sort_tanzaku == null) {
|
||||
localStorage["sort_tags"] = sort_tanzaku = "id";
|
||||
}
|
||||
var current_sasa = null, current_tanzaku = null;
|
||||
var current_sasa_index = -1;
|
||||
var menu_count = 0;
|
||||
var lazy_menu;
|
||||
|
||||
function menu_view_file_open() {
|
||||
if (menu_count > 1) {
|
||||
return;
|
||||
}
|
||||
menu_count++;
|
||||
$("#menu-file-view .selected").removeClass("selected");
|
||||
$("#menu-file-view").css("display", "flex");
|
||||
$("#preview").attr("src", "/preview/" + current_sasa.path);
|
||||
$("#file-name").val(decodeURI(current_sasa.path));
|
||||
$("#menu-file-view .list-item").css("display", "");
|
||||
$("#btn-full").attr("href", "/files/" + current_sasa.path);
|
||||
let resp = tdb_query(db_name, 24, '' + current_sasa.id);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
resp.data.forEach(tanzaku => {
|
||||
$(`.list-item[tid="${tanzaku.id}"]`).addClass("selected");
|
||||
});
|
||||
if ($("#file-selection-filter")[0].checked) {
|
||||
$("#menu-file-view .list-item:not(.selected)").css("display", "none");
|
||||
} else {
|
||||
$("#menu-file-view .list-item:not(.selected)").css("display", "block");
|
||||
}
|
||||
}
|
||||
|
||||
function menu_view_tag_open() {
|
||||
if (menu_count > 1) {
|
||||
return;
|
||||
}
|
||||
menu_count++;
|
||||
$("#menu-tag-view .selected").removeClass("selected");
|
||||
$("#menu-tag-view").css("display", "flex");
|
||||
$("#menu-tag-view .list-item").css("display", "");
|
||||
$("#tag-name").val(decodeURI(current_tanzaku.name));
|
||||
$("#description").val(current_tanzaku.desc);
|
||||
let resp = tdb_query(db_name, 40, '' + current_tanzaku.id);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
resp.data.forEach(sasa => {
|
||||
$(`.list-item[sid="${sasa.id}"]`).addClass("selected");
|
||||
});
|
||||
if ($("#tag-selection-filter")[0].checked) {
|
||||
$("#menu-tag-view .list-item:not(.selected)").css("display", "none");
|
||||
} else {
|
||||
$("#menu-tag-view .list-item:not(.selected)").css("display", "block");
|
||||
}
|
||||
lazy_menu.update();
|
||||
}
|
||||
|
||||
function menu_view_file_close() {
|
||||
menu_count--;
|
||||
$("#menu-file-view").css("display", "none");
|
||||
$("#menu-file-view .list-item").removeClass("selected").css("display", "");
|
||||
$("#file-name").val("");
|
||||
$("#text-filter").val("");
|
||||
current_sasa_index = -1;
|
||||
}
|
||||
|
||||
function menu_view_tag_close() {
|
||||
menu_count--;
|
||||
$("#menu-tag-view").css("display", "none");
|
||||
$("#menu-tag-view .list-item").removeClass("selected").css("display", "");
|
||||
$("#tag-name").val("");
|
||||
$("#description").val("");
|
||||
}
|
||||
|
||||
function menu_add_open() {
|
||||
$(".menu-wrapper").css("display", "flex");
|
||||
$("#menu-add").css("display", "flex");
|
||||
}
|
||||
|
||||
function menu_add_close() {
|
||||
$(".menu-wrapper").css("display", "none");
|
||||
$("#menu-add").css("display", "none");
|
||||
$("#new-name").val("");
|
||||
$("#new-description").val("");
|
||||
}
|
||||
|
||||
function file_next() {
|
||||
if (current_sasa_index === sasahyou.length - 1) {
|
||||
menu_view_file_close();
|
||||
return;
|
||||
}
|
||||
current_sasa_index++;
|
||||
current_sasa = sasahyou[current_sasa_index];
|
||||
menu_count--;
|
||||
menu_view_file_open();
|
||||
}
|
||||
|
||||
function file_prev() {
|
||||
if (current_sasa_index === 0) {
|
||||
menu_view_file_close();
|
||||
return;
|
||||
}
|
||||
current_sasa_index--;
|
||||
current_sasa = sasahyou[current_sasa_index];
|
||||
menu_count--;
|
||||
menu_view_file_open();
|
||||
}
|
||||
|
||||
$(document).keyup(function (e) {
|
||||
switch (e.key) {
|
||||
case "Esc":
|
||||
case "Escape":
|
||||
$(".selected").removeClass("selected");
|
||||
break;
|
||||
case "Left":
|
||||
case "ArrowLeft":
|
||||
if (current_sasa_index >= 0) {
|
||||
file_prev();
|
||||
}
|
||||
break;
|
||||
case "Right":
|
||||
case "ArrowRight":
|
||||
if (current_sasa_index >= 0) {
|
||||
file_next();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("selectstart", ".sasa,.tanzaku", function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$(document).on("click", ".item", function (e) {
|
||||
let wasSelected = $(this).hasClass("selected");
|
||||
if (!e.ctrlKey) {
|
||||
$(".item.selected").removeClass("selected");
|
||||
}
|
||||
if (wasSelected) {
|
||||
$(this).removeClass("selected");
|
||||
} else {
|
||||
$(this).addClass("selected");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("dblclick", ".sasa", function (e) {
|
||||
e.preventDefault();
|
||||
let id = parseInt($(this).attr("sid"));
|
||||
current_sasa_index = 0;
|
||||
sasahyou.every(sasa => {
|
||||
if (sasa.id === id) {
|
||||
current_sasa = sasa;
|
||||
return false;
|
||||
}
|
||||
current_sasa_index++;
|
||||
return true;
|
||||
});
|
||||
menu_view_file_open();
|
||||
});
|
||||
|
||||
$(document).on("dblclick", ".tanzaku", function (e) {
|
||||
e.preventDefault();
|
||||
let id = parseInt($(this).attr("tid"));
|
||||
sappyou.every(tanzaku => {
|
||||
if (tanzaku.id === id) {
|
||||
current_tanzaku = tanzaku;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
menu_view_tag_open();
|
||||
});
|
||||
|
||||
$(document).on("click", "#btn-new", function (e) {
|
||||
e.preventDefault();
|
||||
menu_add_open();
|
||||
});
|
||||
|
||||
$(document).on("click", ".list-item", function (e) {
|
||||
if ($(this).hasClass("selected")) {
|
||||
$(this).removeClass("selected");
|
||||
} else {
|
||||
$(this).addClass("selected");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("click", "#file-selection-filter", function (e) {
|
||||
let notselected = $("#menu-file-view .list-item:not(.selected)");
|
||||
if (this.checked) {
|
||||
notselected.css("display", "none");
|
||||
} else {
|
||||
notselected.css("display", "block");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("click", "#tag-selection-filter", function (e) {
|
||||
let notselected = $("#menu-tag-view .list-item:not(.selected)");
|
||||
if (this.checked) {
|
||||
notselected.css("display", "none");
|
||||
} else {
|
||||
notselected.css("display", "block");
|
||||
}
|
||||
lazy_menu.update();
|
||||
});
|
||||
|
||||
$(document).on("input", "#text-filter", function (e) {
|
||||
let filter = $(this).val().toLowerCase();
|
||||
let unfiltered;
|
||||
if ($("#file-selection-filter")[0].checked) {
|
||||
unfiltered = $(".list-item.selected");
|
||||
} else {
|
||||
unfiltered = $(".list-item");
|
||||
}
|
||||
if (filter === "") {
|
||||
unfiltered.css("display", "");
|
||||
return;
|
||||
}
|
||||
unfiltered.each((index, element) => {
|
||||
let current = $(element);
|
||||
if (current.text().toLowerCase().includes(filter)) {
|
||||
current.css("display", "");
|
||||
} else {
|
||||
current.css("display", "none");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on("reset", "#menu-file-view form", function (e) {
|
||||
e.preventDefault();
|
||||
menu_view_file_close();
|
||||
});
|
||||
|
||||
$(document).on("reset", "#menu-tag-view form", function (e) {
|
||||
e.preventDefault();
|
||||
menu_view_tag_close();
|
||||
});
|
||||
|
||||
$(document).on("reset", "#menu-add form", function (e) {
|
||||
e.preventDefault();
|
||||
menu_add_close();
|
||||
});
|
||||
|
||||
$(document).on("submit", "#menu-file-view form", function (e) {
|
||||
e.preventDefault();
|
||||
let resp = tdb_query(db_name, 24, '' + current_sasa.id);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
let toadd = "", toremove = "";
|
||||
resp.data.forEach(tanzaku => {
|
||||
let current = $(`.list-item[tid="${tanzaku.id}"]`);
|
||||
if (!current.hasClass("selected")) {
|
||||
toremove += ' ' + tanzaku.id;
|
||||
}
|
||||
});
|
||||
$(".list-item.tanzaku.selected").each(function (index, element) {
|
||||
let tid = parseInt($(element).attr("tid"));
|
||||
if (resp.data.find(t => t.id === tid) == null) {
|
||||
toadd += ' ' + tid;
|
||||
}
|
||||
});
|
||||
let status = true;
|
||||
if (toadd !== "") {
|
||||
resp = tdb_query(db_name, 26, '' + current_sasa.id + toadd);
|
||||
status = (resp != null && resp.status);
|
||||
}
|
||||
if (toremove !== "") {
|
||||
resp = tdb_query(db_name, 25, '' + current_sasa.id + toremove);
|
||||
status = (resp != null && resp.status);
|
||||
}
|
||||
if (status) {
|
||||
alert("Saved changes!");
|
||||
} else {
|
||||
alert("Something went wrong!");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("submit", "#menu-tag-view form", function (e) {
|
||||
e.preventDefault();
|
||||
let resp;
|
||||
let name = $("#tag-name").val(),
|
||||
desc = $("#description").val();
|
||||
if (name !== current_tanzaku.name || desc !== current_tanzaku.desc) {
|
||||
resp = tdb_query(db_name, 36, '' + current_tanzaku.id + ' ' + name + '\n' + desc);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
current_tanzaku.name = name;
|
||||
current_tanzaku.desc = desc;
|
||||
$(`.tanzaku[tid=${current_tanzaku.id}]`).text(name);
|
||||
}
|
||||
resp = tdb_query(db_name, 40, '' + current_tanzaku.id);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
let toadd = "", toremove = "";
|
||||
resp.data.forEach(sasa => {
|
||||
let current = $(`.list-item[sid="${sasa.id}"]`);
|
||||
if (!current.hasClass("selected")) {
|
||||
toremove += ' ' + sasa.id;
|
||||
}
|
||||
});
|
||||
$(".list-item.sasa.selected").each(function (index, element) {
|
||||
let sid = parseInt($(element).attr("sid"));
|
||||
if (resp.data.find(s => s.id === sid) == null) {
|
||||
toadd += ' ' + sid;
|
||||
}
|
||||
});
|
||||
let status = true;
|
||||
if (toadd !== "") {
|
||||
resp = tdb_query(db_name, 42, '' + current_tanzaku.id + toadd);
|
||||
status = (resp != null && resp.status);
|
||||
}
|
||||
if (toremove !== "") {
|
||||
resp = tdb_query(db_name, 41, '' + current_tanzaku.id + toremove);
|
||||
status = (resp != null && resp.status);
|
||||
}
|
||||
if (status) {
|
||||
alert("Saved changes!");
|
||||
} else {
|
||||
alert("Something went wrong!");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("click", "#btn-remove", function (e) {
|
||||
e.preventDefault();
|
||||
if (!confirm("This tag will be removed permanently. Are you sure?")) {
|
||||
return;
|
||||
}
|
||||
let resp = tdb_query(db_name, 33, '' + current_tanzaku.id);
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
menu_add_close();
|
||||
location.reload(true);
|
||||
});
|
||||
|
||||
$(document).on("click", "#file-next", function (e) {
|
||||
e.preventDefault();
|
||||
file_next();
|
||||
});
|
||||
|
||||
$(document).on("click", "#file-prev", function (e) {
|
||||
e.preventDefault();
|
||||
file_prev();
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
var db_name = localStorage["tfm_db_name"];
|
||||
sort_sasa = localStorage["sort_files"];
|
||||
sort_tanzaku = localStorage["sort_tags"];
|
||||
if (sort_sasa == null) {
|
||||
localStorage["sort_files"] = sort_sasa = "id";
|
||||
}
|
||||
if (sort_tanzaku == null) {
|
||||
localStorage["sort_tags"] = sort_tanzaku = "id";
|
||||
}
|
||||
|
||||
function settings_load() {
|
||||
if (db_name != null) {
|
||||
$(`#db_name option[value="${db_name}"]`).prop("selected", true);
|
||||
} else {
|
||||
$("#db_name option[value=\"\"]").prop("selected", true);
|
||||
}
|
||||
if (sort_sasa != null) {
|
||||
let sort_s = sort_sasa;
|
||||
if (sort_s[0] === '!') {
|
||||
sort_s = sort_s.slice(1);
|
||||
}
|
||||
if (sort_s[0] === '-') {
|
||||
$("#files-reverse").prop("checked", true);
|
||||
sort_s = sort_s.slice(1);
|
||||
}
|
||||
$(`#files-by-${sort_s}`).prop("checked", true);
|
||||
}
|
||||
if (sort_tanzaku != null) {
|
||||
let sort_t = sort_tanzaku;
|
||||
if (sort_t[0] === '!') {
|
||||
sort_t = sort_t.slice(1);
|
||||
}
|
||||
if (sort_t[0] === '-') {
|
||||
$("#tags-reverse").prop("checked", true);
|
||||
sort_t = sort_t.slice(1);
|
||||
}
|
||||
$(`#tags-by-${sort_t}`).prop("checked", true);
|
||||
}
|
||||
}
|
||||
|
||||
$(window).on("load", function () {
|
||||
let resp = tdb_query();
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Failed to fetch databases");
|
||||
throw new Error("Failed to fetch databases");
|
||||
}
|
||||
resp.data.every(tdb => {
|
||||
$("#db_name").append($("<option>", {
|
||||
value: tdb.name,
|
||||
text: tdb.name
|
||||
}));
|
||||
return true;
|
||||
});
|
||||
settings_load();
|
||||
});
|
||||
|
||||
$(document).on("reset", "#settings", function (e) {
|
||||
e.preventDefault();
|
||||
settings_load();
|
||||
});
|
||||
|
||||
$(document).on("submit", "#settings", function (e) {
|
||||
e.preventDefault();
|
||||
let db_name_input = $("#db_name");
|
||||
let db_name_val = db_name_input.val();
|
||||
if (db_name_val !== db_name) {
|
||||
let resp = tdb_query();
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Failed to fetch databases");
|
||||
return;
|
||||
}
|
||||
let found = false;
|
||||
resp.data.every(db => {
|
||||
if (db.name === db_name_val) {
|
||||
localStorage["tfm_db_name"] = db_name = db_name_val;
|
||||
found = true;
|
||||
db_name_input.removeClass("is-invalid");
|
||||
localStorage["sasahyou_mts"] = sasahyou_mts = 0;
|
||||
localStorage["sappyou_mts"] = sappyou_mts = 0;
|
||||
localStorage["shoppyou_mts"] = shoppyou_mts = 0;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!found) {
|
||||
db_name_input.addClass("is-invalid");
|
||||
return;
|
||||
}
|
||||
}
|
||||
let sort_f = ($("#files-reverse")[0].checked ? '-' : '') + $("input[type=radio][name=sort-files]:checked").attr("id").slice(9);
|
||||
let sort_t = ($("#tags-reverse")[0].checked ? '-' : '') + $("input[type=radio][name=sort-tags]:checked").attr("id").slice(8);
|
||||
if (sort_f !== sort_sasa && '!' + sort_f !== sort_sasa) {
|
||||
localStorage["sort_files"] = sort_sasa = '!' + sort_f;
|
||||
}
|
||||
if (sort_t !== sort_tanzaku && '!' + sort_t !== sort_tanzaku) {
|
||||
localStorage["sort_tags"] = sort_tanzaku = '!' + sort_t;
|
||||
}
|
||||
alert("Successfully updated settings!");
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
$(window).on("load", function () {
|
||||
sappyou_load();
|
||||
sappyou.forEach((tanzaku) => {
|
||||
$(".contents-wrapper").append(`<div class="item tanzaku" tid="${tanzaku.id}">${tanzaku.name}</div>`);
|
||||
$("#menu-file-view .list").append(`<div class="list-item tanzaku" tid="${tanzaku.id}">${tanzaku.name}</div>`);
|
||||
});
|
||||
sasahyou_load();
|
||||
sasahyou.forEach((sasa) => {
|
||||
$("#menu-tag-view .list").append(`<div class="list-item sasa" sid="${sasa.id}" title="${sasa.path.split('/').slice(-1)}"><img class="thumb" data-src="${"/thumbs/" + sasa.path}"><div class="overlay"></div></div>`);
|
||||
});
|
||||
lazy_menu = $("#menu-tag-view .thumb").lazy({
|
||||
chainable: false,
|
||||
scrollDirection: "vertical",
|
||||
effect: "fadeIn",
|
||||
visibleOnly: true,
|
||||
appendScroll: $("#menu-tag-view .list")[0],
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on("input", "#text-filter-all", function (e) {
|
||||
let filter = $(this).val().toLowerCase();
|
||||
let unfiltered = $(".item");
|
||||
if (filter === "") {
|
||||
unfiltered.css("display", "");
|
||||
return;
|
||||
}
|
||||
unfiltered.each((index, element) => {
|
||||
let current = $(element);
|
||||
if (current.text().toLowerCase().includes(filter)) {
|
||||
current.css("display", "");
|
||||
} else {
|
||||
current.css("display", "none");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on("submit", "#menu-add form", function (e) {
|
||||
e.preventDefault();
|
||||
let resp = tdb_query(db_name, 34, $("#new-name").val() + '\n' + $("#new-description").val());
|
||||
if (resp == null || !resp.status) {
|
||||
alert("Something went wrong!");
|
||||
return;
|
||||
}
|
||||
menu_add_close();
|
||||
location.reload(true);
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "Tanabata",
|
||||
"lang": "en-US",
|
||||
"description": "Tanabata Project PWA",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#615880",
|
||||
"icons": [
|
||||
{
|
||||
"src": "\/images\/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image\/png",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "\/images\/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image\/png",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "\/images\/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image\/png",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "\/images\/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image\/png",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "\/images\/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image\/png",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "\/images\/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image\/png",
|
||||
"density": "4.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Database Management | Tanabata</title>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/tanabata.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#615880">
|
||||
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#615880">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/general.css">
|
||||
<script src="/js/jquery-3.6.0.min.js"></script>
|
||||
<script src="/js/tdbms.js"></script>
|
||||
<script src="/js/tdbms-load.js"></script>
|
||||
<script src="/js/tdbms-database.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Tanabata Database Management</h1>
|
||||
<main>
|
||||
<h2><span>TDB: 「<span class="db_name"><i>none</i></span>」</span></h2>
|
||||
<div class="contents-wrapper button-flex">
|
||||
<a href="/tdbms/stats" class="btn btn-primary">Stats</a>
|
||||
<a href="/tdbms/sasahyou" class="btn btn-primary">Sasahyou</a>
|
||||
<a href="/tdbms/sappyou" class="btn btn-primary">Sappyou</a>
|
||||
<a href="/tdbms/shoppyou" class="btn btn-primary">Shoppyou</a>
|
||||
</div>
|
||||
<div class="contents-wrapper button-flex">
|
||||
<button class="btn btn-outline-success" id="btn-save">Save database</button>
|
||||
<button class="btn btn-outline-warning" id="btn-reload">Reload database</button>
|
||||
<a href="/tdbms/new" class="btn btn-outline-info">Create database</a>
|
||||
<button class="btn btn-outline-danger" id="btn-remove">Remove database</button>
|
||||
</div>
|
||||
<div class="contents-wrapper button-flex">
|
||||
<a href="/" class="btn btn-secondary">Home</a>
|
||||
<a href="/tdbms/settings" class="btn btn-secondary">Settings</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,53 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>New database | Tanabata Database Management</title>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/tanabata.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#615880">
|
||||
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#615880">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/general.css">
|
||||
<script src="/js/jquery-3.6.0.min.js"></script>
|
||||
<script src="/js/tdbms.js"></script>
|
||||
<script src="/js/tdbms-load.js"></script>
|
||||
<script src="/js/tdbms-database.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>TDBMS: add new database</h1>
|
||||
<main>
|
||||
<div class="contents-wrapper">
|
||||
<form id="newdb">
|
||||
<div class="form-group">
|
||||
<label for="newdb-name">Name</label>
|
||||
<input type="text" name="newdb-name" class="form-control" id="newdb-name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newdb-path">Location on server</label>
|
||||
<input type="text" name="newdb-path" class="form-control" id="newdb-path">
|
||||
</div>
|
||||
<div class="form-group button-flex">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="/tdbms" class="btn btn-outline-secondary">TDBMS home</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/tdbms-newdb.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,49 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sappyou | Tanabata Database Management</title>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/tanabata.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#615880">
|
||||
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#615880">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/general.css">
|
||||
<link rel="stylesheet" href="/css/tdbms.css">
|
||||
<script src="/js/jquery-3.6.0.min.js"></script>
|
||||
<script src="/js/tdbms.js"></script>
|
||||
<script src="/js/tdbms-load.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="/tdbms"><span class="db_name"></span>: sappyou</a></h1>
|
||||
<main>
|
||||
<div class="contents-wrapper">
|
||||
<table class="table table-striped table-dark" id="content">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Ctime</th>
|
||||
<th>Mtime</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/tdbms-management.js"></script>
|
||||
<script src="/js/tdbms-sappyou.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,47 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sasahyou | Tanabata Database Management</title>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/tanabata.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#615880">
|
||||
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#615880">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/general.css">
|
||||
<link rel="stylesheet" href="/css/tdbms.css">
|
||||
<script src="/js/jquery-3.6.0.min.js"></script>
|
||||
<script src="/js/tdbms.js"></script>
|
||||
<script src="/js/tdbms-load.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="/tdbms"><span class="db_name"></span>: sasahyou</a></h1>
|
||||
<main>
|
||||
<div class="contents-wrapper">
|
||||
<table class="table table-striped table-dark" id="content">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Ctime</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/tdbms-management.js"></script>
|
||||
<script src="/js/tdbms-sasahyou.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,111 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings | Tanabata Database Management</title>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/tanabata.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#615880">
|
||||
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#615880">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/general.css">
|
||||
<script src="/js/jquery-3.6.0.min.js"></script>
|
||||
<script src="/js/tdbms.js"></script>
|
||||
<script src="/js/tdbms-load.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>TDBMS: Settings</h1>
|
||||
<main>
|
||||
<div class="contents-wrapper">
|
||||
<form id="settings">
|
||||
<div class="form-group">
|
||||
<label for="db_name">Database name</label>
|
||||
<select name="db_name" id="db_name" class="form-control form-select form-select-sm">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<table class="form-group">
|
||||
<tr>
|
||||
<td>
|
||||
<fieldset class="form-group">
|
||||
<legend>Sasa sorting</legend>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-sasa" id="sasa-by-id">
|
||||
<label for="sasa-by-id">By ID</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-sasa" id="sasa-by-cts">
|
||||
<label for="sasa-by-cts">By ctime</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-sasa" id="sasa-by-path">
|
||||
<label for="sasa-by-path">By name</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</td>
|
||||
<td>
|
||||
<fieldset class="form-group">
|
||||
<legend>Tanzaku sorting</legend>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-tanzaku" id="tanzaku-by-id">
|
||||
<label for="tanzaku-by-id">By ID</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-tanzaku" id="tanzaku-by-cts">
|
||||
<label for="tanzaku-by-cts">By ctime</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-tanzaku" id="tanzaku-by-mts">
|
||||
<label for="tanzaku-by-mts">By mtime</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-tanzaku" id="tanzaku-by-name">
|
||||
<label for="tanzaku-by-name">By name</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-tanzaku" id="tanzaku-by-nkazari">
|
||||
<label for="tanzaku-by-nkazari">By kazari count</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" name="sort-sasa-reverse" class="form-check-input" id="sasa-reverse">
|
||||
<label class="form-check-label" for="sasa-reverse">Reverse sorting</label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" name="sort-tanzaku-reverse" class="form-check-input" id="tanzaku-reverse">
|
||||
<label class="form-check-label" for="tanzaku-reverse">Reverse sorting</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="button-flex">
|
||||
<button type="submit" class="btn btn-primary">Apply</button>
|
||||
<a href="/tdbms" class="btn btn-outline-secondary">TDBMS home</a>
|
||||
<button type="reset" class="btn btn-danger">Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/tdbms-settings.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,47 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Shoppyou | Tanabata Database Management</title>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/tanabata.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#615880">
|
||||
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#615880">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/general.css">
|
||||
<link rel="stylesheet" href="/css/tdbms.css">
|
||||
<script src="/js/jquery-3.6.0.min.js"></script>
|
||||
<script src="/js/tdbms.js"></script>
|
||||
<script src="/js/tdbms-load.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="/tdbms"><span class="db_name"></span>: shoppyou</a></h1>
|
||||
<main>
|
||||
<div class="contents-wrapper">
|
||||
<table class="table table-striped table-dark" id="content">
|
||||
<tr>
|
||||
<th>Ctime</th>
|
||||
<th>Sasa ID</th>
|
||||
<th>Tanzaku ID</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/tdbms-management.js"></script>
|
||||
<script src="/js/tdbms-shoppyou.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,67 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stats | Tanabata Database Management</title>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/tanabata.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#615880">
|
||||
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#615880">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/general.css">
|
||||
<link rel="stylesheet" href="/css/tdbms.css">
|
||||
<script src="/js/jquery-3.6.0.min.js"></script>
|
||||
<script src="/js/tdbms.js"></script>
|
||||
<script src="/js/tdbms-load.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="/tdbms"><span class="db_name"></span>: stats</a></h1>
|
||||
<main>
|
||||
<div class="contents-wrapper">
|
||||
<h3>Sasahyou</h3>
|
||||
<table class="table table-striped table-dark" id="stats-sasahyou">
|
||||
<tr>
|
||||
<th>Ctime</th>
|
||||
<th>Mtime</th>
|
||||
<th>Number of sasa</th>
|
||||
<th>Number of holes</th>
|
||||
</tr>
|
||||
</table>
|
||||
<h3>Sappyou</h3>
|
||||
<table class="table table-striped table-dark" id="stats-sappyou">
|
||||
<tr>
|
||||
<th>Ctime</th>
|
||||
<th>Mtime</th>
|
||||
<th>Number of tanzaku</th>
|
||||
<th>Number of holes</th>
|
||||
</tr>
|
||||
</table>
|
||||
<h3>Shoppyou</h3>
|
||||
<table class="table table-striped table-dark" id="stats-shoppyou">
|
||||
<tr>
|
||||
<th>Ctime</th>
|
||||
<th>Mtime</th>
|
||||
<th>Number of kazari</th>
|
||||
<th>Number of holes</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/tdbms-management.js"></script>
|
||||
<script src="/js/tdbms-stats.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,117 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Files | Tanabata File Manager</title>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/tanabata.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#615880">
|
||||
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#615880">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/general.css">
|
||||
<link rel="stylesheet" href="/css/tfm.css">
|
||||
<script src="/js/jquery-3.6.0.min.js"></script>
|
||||
<script src="/js/jquery.lazy.min.js"></script>
|
||||
<script src="/js/tdbms.js"></script>
|
||||
<script src="/js/tfm-management.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="/tfm" title="TFM: Home">TFM: Files</a></h1>
|
||||
<main>
|
||||
<div class="contents-wrapper">
|
||||
<button class="btn btn-outline-secondary sasa" id="btn-new"><b>NEW</b></button>
|
||||
</div>
|
||||
</main>
|
||||
<div class="menu-wrapper" id="menu-file-view">
|
||||
<div class="menu">
|
||||
<div class="preview">
|
||||
<img id="preview">
|
||||
<div class="file-nav-btn" id="file-prev"></div>
|
||||
<div class="file-nav-btn" id="file-next"></div>
|
||||
</div>
|
||||
<form>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label" for="file-name">File name</label>
|
||||
<div class="col-form-input">
|
||||
<input type="text" name="name" class="form-control" id="file-name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label" for="text-filter">Tag filter</label>
|
||||
<div class="col-form-input">
|
||||
<input type="text" name="text-filter" class="form-control" id="text-filter">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" name="selection-filter" class="form-check-input" id="file-selection-filter" checked>
|
||||
<label class="form-check-label" for="file-selection-filter">Show only selected</label>
|
||||
</div>
|
||||
<div class="form-group list"></div>
|
||||
<div class="form-group button-flex">
|
||||
<button type="submit" class="btn btn-primary">Confirm</button>
|
||||
<a target="_blank" class="btn btn-outline-info" id="btn-full">View full</a>
|
||||
<button type="reset" class="btn btn-outline-danger">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-wrapper" id="menu-tag-view">
|
||||
<div class="menu">
|
||||
<form>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label" for="tag-name">Tag name</label>
|
||||
<div class="col-form-input">
|
||||
<input type="text" name="name" class="form-control" id="tag-name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Tag description</label>
|
||||
<textarea class="form-control" name="description" id="description" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" name="selection-filter" class="form-check-input" id="tag-selection-filter" checked>
|
||||
<label class="form-check-label" for="tag-selection-filter">Show only selected</label>
|
||||
</div>
|
||||
<div class="form-group list"></div>
|
||||
<div class="form-group button-flex">
|
||||
<button type="submit" class="btn btn-primary">Confirm</button>
|
||||
<button class="btn btn-danger" id="btn-remove">Remove</button>
|
||||
<button type="reset" class="btn btn-outline-danger">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-wrapper" id="menu-add">
|
||||
<div class="menu">
|
||||
<h2>Add new file</h2>
|
||||
<form>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label" for="new-name">File name</label>
|
||||
<div class="col-form-input">
|
||||
<input type="text" name="new-name" class="form-control" id="new-name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group button-flex">
|
||||
<button type="submit" class="btn btn-primary">Add</button>
|
||||
<button type="reset" class="btn btn-outline-danger">Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/tfm-files.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,48 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Home | Tanabata File Manager</title>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/tanabata.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#615880">
|
||||
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#615880">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/general.css">
|
||||
<script src="/js/jquery-3.6.0.min.js"></script>
|
||||
<script src="/js/tdbms.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Tanabata File Manager</h1>
|
||||
<main>
|
||||
<h2>Come on, what to do?</h2>
|
||||
<div class="contents-wrapper button-flex">
|
||||
<a href="/tfm/files" class="btn btn-primary">Files</a>
|
||||
<a href="/tfm/tags" class="btn btn-primary">Tags</a>
|
||||
</div>
|
||||
<div class="contents-wrapper button-flex">
|
||||
<button class="btn btn-outline-success" id="btn-save">Save database</button>
|
||||
<button class="btn btn-outline-warning" id="btn-reload">Reload database</button>
|
||||
</div>
|
||||
<div class="contents-wrapper button-flex">
|
||||
<a href="/" class="btn btn-secondary">Home</a>
|
||||
<a href="/tfm/settings" class="btn btn-secondary">Settings</a>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/tfm-database.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,110 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings | Tanabata File Manager</title>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/tanabata.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#615880">
|
||||
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#615880">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/general.css">
|
||||
<script src="/js/jquery-3.6.0.min.js"></script>
|
||||
<script src="/js/tdbms.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>TFM: Settings</h1>
|
||||
<main>
|
||||
<div class="contents-wrapper">
|
||||
<form id="settings">
|
||||
<div class="form-group">
|
||||
<label for="db_name">Database name</label>
|
||||
<select name="db_name" id="db_name" class="form-control form-select form-select-sm">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<table class="form-group">
|
||||
<tr>
|
||||
<td>
|
||||
<fieldset class="form-group">
|
||||
<legend>Files sorting</legend>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-files" id="files-by-id">
|
||||
<label for="files-by-id">By ID</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-files" id="files-by-cts">
|
||||
<label for="files-by-cts">By ctime</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-files" id="files-by-path">
|
||||
<label for="files-by-path">By name</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</td>
|
||||
<td>
|
||||
<fieldset class="form-group">
|
||||
<legend>Tags sorting</legend>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-tags" id="tags-by-id">
|
||||
<label for="tags-by-id">By ID</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-tags" id="tags-by-cts">
|
||||
<label for="tags-by-cts">By ctime</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-tags" id="tags-by-mts">
|
||||
<label for="tags-by-mts">By mtime</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-tags" id="tags-by-name">
|
||||
<label for="tags-by-name">By name</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="sort-tags" id="tags-by-nkazari">
|
||||
<label for="tags-by-nkazari">By kazari count</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" name="sort-files-reverse" class="form-check-input" id="files-reverse">
|
||||
<label class="form-check-label" for="files-reverse">Reverse sorting</label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" name="sort-tags-reverse" class="form-check-input" id="tags-reverse">
|
||||
<label class="form-check-label" for="tags-reverse">Reverse sorting</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="button-flex">
|
||||
<button type="submit" class="btn btn-primary">Apply</button>
|
||||
<a href="/tfm" class="btn btn-outline-secondary">TFM home</a>
|
||||
<button type="reset" class="btn btn-danger">Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/tfm-settings.js"></script>
|
||||
</body>
|
||||
</html>
|
||||