Compare commits

...

No commits in common. "master" and "archive-c" have entirely different histories.

110 changed files with 6258 additions and 1566 deletions

57
CMakeLists.txt Normal file
View File

@ -0,0 +1,57 @@
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})

64
README.md Normal file
View File

@ -0,0 +1,64 @@
<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>&copy; 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

5
_config.yml Normal file
View File

@ -0,0 +1,5 @@
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

View File

@ -1,19 +0,0 @@
module tanabata
go 1.23.0
toolchain go1.23.10
require github.com/jackc/pgx/v5 v5.7.5
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx v3.6.2+incompatible // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/stretchr/testify v1.9.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/text v0.24.0 // indirect
)

View File

@ -1,32 +0,0 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,119 +0,0 @@
package domain
import (
"encoding/json"
"time"
)
type User struct {
Name string `json:"name"`
IsAdmin bool `json:"isAdmin"`
CanCreate bool `json:"canCreate"`
}
type MIME struct {
Name string `json:"name"`
Extension string `json:"extension"`
}
type (
CategoryCore struct {
ID *string `json:"id"`
Name *string `json:"name"`
Color *string `json:"color"`
}
CategoryItem struct {
CategoryCore
}
CategoryFull struct {
CategoryCore
CreatedAt time.Time `json:"createdAt"`
Creator User `json:"creator"`
Notes *string `json:"notes"`
}
)
type (
FileCore struct {
ID string `json:"id"`
Name *string `json:"name"`
MIME MIME `json:"mime"`
}
FileItem struct {
FileCore
CreatedAt time.Time `json:"createdAt"`
Creator User `json:"creator"`
}
FileFull struct {
FileCore
CreatedAt time.Time `json:"createdAt"`
Creator User `json:"creator"`
Notes *string `json:"notes"`
Metadata json.RawMessage `json:"metadata"`
Viewed int `json:"viewed"`
}
)
type (
TagCore struct {
ID string `json:"id"`
Name string `json:"name"`
Color *string `json:"color"`
}
TagItem struct {
TagCore
Category CategoryCore `json:"category"`
}
TagFull struct {
TagCore
Category CategoryCore `json:"category"`
CreatedAt time.Time `json:"createdAt"`
Creator User `json:"creator"`
Notes *string `json:"notes"`
UsedIncl int `json:"usedIncl"`
UsedExcl int `json:"usedExcl"`
}
)
type Autotag struct {
TriggerTag TagCore `json:"triggerTag"`
AddTag TagCore `json:"addTag"`
IsActive bool `json:"isActive"`
}
type (
PoolCore struct {
ID string `json:"id"`
Name string `json:"name"`
}
PoolItem struct {
PoolCore
}
PoolFull struct {
PoolCore
CreatedAt time.Time `json:"createdAt"`
Creator User `json:"creator"`
Notes *string `json:"notes"`
Viewed int `json:"viewed"`
}
)
type Session struct {
ID int `json:"id"`
UserAgent string `json:"userAgent"`
StartedAt time.Time `json:"startedAt"`
ExpiresAt time.Time `json:"expiresAt"`
LastActivity time.Time `json:"lastActivity"`
}
type Pagination struct {
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Count int `json:"count"`
}
type Slice[T any] struct {
Pagination Pagination `json:"pagination"`
Data []T `json:"data"`
}

View File

@ -1,65 +0,0 @@
package domain
import "fmt"
type ErrorCode string
const (
// File errors
ErrCodeFileNotFound ErrorCode = "FILE_NOT_FOUND"
ErrCodeMIMENotSupported ErrorCode = "MIME_NOT_SUPPORTED"
// Tag errors
ErrCodeTagNotFound ErrorCode = "TAG_NOT_FOUND"
// General errors
ErrCodeBadRequest ErrorCode = "BAD_REQUEST"
ErrCodeInternal ErrorCode = "INTERNAL_SERVER_ERROR"
)
type DomainError struct {
Err error `json:"-"`
Code ErrorCode `json:"code"`
Message string `json:"message"`
Details []any `json:"-"`
}
func (e *DomainError) Wrap(err error) *DomainError {
e.Err = err
return e
}
func NewErrorFileNotFound(file_id string) *DomainError {
return &DomainError{
Code: ErrCodeFileNotFound,
Message: fmt.Sprintf("File not found: %q", file_id),
}
}
func NewErrorMIMENotSupported(mime string) *DomainError {
return &DomainError{
Code: ErrCodeMIMENotSupported,
Message: fmt.Sprintf("MIME not supported: %q", mime),
}
}
func NewErrorTagNotFound(tag_id string) *DomainError {
return &DomainError{
Code: ErrCodeTagNotFound,
Message: fmt.Sprintf("Tag not found: %q", tag_id),
}
}
func NewErrorBadRequest(message string) *DomainError {
return &DomainError{
Code: ErrCodeBadRequest,
Message: message,
}
}
func NewErrorUnexpected() *DomainError {
return &DomainError{
Code: ErrCodeInternal,
Message: "An unexpected error occured",
}
}

View File

@ -1,17 +0,0 @@
package domain
import (
"context"
"encoding/json"
"time"
)
type FileRepository interface {
GetAccess(ctx context.Context, user_id int, file_id string) (canView, canEdit bool, domainErr *DomainError)
GetSlice(ctx context.Context, user_id int, filter, sort string, limit, offset int) (files Slice[FileItem], domainErr *DomainError)
Get(ctx context.Context, user_id int, file_id string) (file FileFull, domainErr *DomainError)
Add(ctx context.Context, user_id int, name, mime string, datetime time.Time, notes string, metadata json.RawMessage) (file FileCore, domainErr *DomainError)
Update(ctx context.Context, file_id string, updates map[string]interface{}) (domainErr *DomainError)
Delete(ctx context.Context, file_id string) (domainErr *DomainError)
GetTags(ctx context.Context, user_id int, file_id string) (tags []TagItem, domainErr *DomainError)
}

View File

@ -1,54 +0,0 @@
package postgres
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"tanabata/internal/domain"
)
// Initialize PostgreSQL database driver
func New(dbURL string) (*pgxpool.Pool, error) {
poolConfig, err := pgxpool.ParseConfig(dbURL)
if err != nil {
return nil, fmt.Errorf("failed to parse connection string: %w", err)
}
poolConfig.MaxConns = 100
poolConfig.MinConns = 0
poolConfig.MaxConnLifetime = time.Hour
poolConfig.HealthCheckPeriod = 30 * time.Second
ctx := context.Background()
db, err := pgxpool.NewWithConfig(ctx, poolConfig)
if err != nil {
return nil, fmt.Errorf("failed to initialize DB connections pool: %w", err)
}
if err = db.Ping(ctx); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return db, nil
}
// Transaction wrapper
func transaction(ctx context.Context, db *pgxpool.Pool, handler func(context.Context, pgx.Tx) *domain.DomainError) (domainErr *domain.DomainError) {
tx, err := db.Begin(ctx)
if err != nil {
domainErr = domain.NewErrorUnexpected().Wrap(err)
return
}
domainErr = handler(ctx, tx)
if domainErr != nil {
tx.Rollback(ctx)
return
}
err = tx.Commit(ctx)
if err != nil {
domainErr = domain.NewErrorUnexpected().Wrap(err)
}
return
}

View File

@ -1,331 +0,0 @@
package postgres
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
"tanabata/internal/domain"
)
type FileRepository struct {
db *pgxpool.Pool
}
func NewFileRepository(db *pgxpool.Pool) *FileRepository {
return &FileRepository{db: db}
}
// Get user permissions on file
func (s *FileRepository) GetAccess(ctx context.Context, user_id int, file_id string) (canView, canEdit bool, domainErr *domain.DomainError) {
row := s.db.QueryRow(ctx, `
SELECT
COALESCE(a.view, FALSE) OR f.creator_id=$1 OR COALESCE(u.is_admin, FALSE),
COALESCE(a.edit, FALSE) OR f.creator_id=$1 OR COALESCE(u.is_admin, FALSE)
FROM data.files f
LEFT JOIN acl.files a ON a.file_id=f.id AND a.user_id=$1
LEFT JOIN system.users u ON u.id=$1
WHERE f.id=$2
`, user_id, file_id)
err := row.Scan(&canView, &canEdit)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
domainErr = domain.NewErrorFileNotFound(file_id).Wrap(err)
return
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case "22P02":
domainErr = domain.NewErrorBadRequest(fmt.Sprintf("Invalid file id: %q", file_id)).Wrap(err)
return
}
}
domainErr = domain.NewErrorUnexpected().Wrap(err)
}
return
}
// Get a set of files
func (s *FileRepository) GetSlice(ctx context.Context, user_id int, filter, sort string, limit, offset int) (files domain.Slice[domain.FileItem], domainErr *domain.DomainError) {
filterCond, err := filterToSQL(filter)
if err != nil {
domainErr = domain.NewErrorBadRequest(fmt.Sprintf("Invalid filter string: %q", filter)).Wrap(err)
return
}
sortExpr, err := sortToSQL(sort)
if err != nil {
domainErr = domain.NewErrorBadRequest(fmt.Sprintf("Invalid sorting parameter: %q", sort)).Wrap(err)
return
}
// prepare query
query := `
SELECT
f.id,
f.name,
m.name,
m.extension,
uuid_extract_timestamp(f.id),
u.name,
u.is_admin
FROM data.files f
JOIN system.mime m ON m.id=f.mime_id
JOIN system.users u ON u.id=f.creator_id
WHERE f.is_deleted IS FALSE AND (f.creator_id=$1 OR (SELECT view FROM acl.files WHERE file_id=f.id AND user_id=$1) OR (SELECT is_admin FROM system.users WHERE id=$1)) AND
`
query += filterCond
queryCount := query
query += sortExpr
if limit >= 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
}
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
// execute query
domainErr = transaction(ctx, s.db, func(ctx context.Context, tx pgx.Tx) (domainErr *domain.DomainError) {
rows, err := tx.Query(ctx, query, user_id)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case "42P10":
domainErr = domain.NewErrorBadRequest(fmt.Sprintf("Invalid sorting field: %q", sort[1:])).Wrap(err)
return
}
}
domainErr = domain.NewErrorUnexpected().Wrap(err)
return
}
defer rows.Close()
count := 0
for rows.Next() {
var file domain.FileItem
err = rows.Scan(&file.ID, &file.Name, &file.MIME.Name, &file.MIME.Extension, &file.CreatedAt, &file.Creator.Name, &file.Creator.IsAdmin)
if err != nil {
domainErr = domain.NewErrorUnexpected().Wrap(err)
return
}
files.Data = append(files.Data, file)
count++
}
err = rows.Err()
if err != nil {
domainErr = domain.NewErrorUnexpected().Wrap(err)
return
}
files.Pagination.Limit = limit
files.Pagination.Offset = offset
files.Pagination.Count = count
row := tx.QueryRow(ctx, fmt.Sprintf("SELECT COUNT(*) FROM (%s) tmp", queryCount), user_id)
err = row.Scan(&files.Pagination.Total)
if err != nil {
domainErr = domain.NewErrorUnexpected().Wrap(err)
}
return
})
return
}
// Get file
func (s *FileRepository) Get(ctx context.Context, user_id int, file_id string) (file domain.FileFull, domainErr *domain.DomainError) {
row := s.db.QueryRow(ctx, `
SELECT
f.id,
f.name,
m.name,
m.extension,
uuid_extract_timestamp(f.id),
u.name,
u.is_admin,
f.notes,
f.metadata,
(SELECT COUNT(*) FROM activity.file_views fv WHERE fv.file_id=$2 AND fv.user_id=$1)
FROM data.files f
JOIN system.mime m ON m.id=f.mime_id
JOIN system.users u ON u.id=f.creator_id
WHERE f.is_deleted IS FALSE
`, user_id, file_id)
err := row.Scan(&file.ID, &file.Name, &file.MIME.Name, &file.MIME.Extension, &file.CreatedAt, &file.Creator.Name, &file.Creator.IsAdmin, &file.Notes, &file.Metadata, &file.Viewed)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
domainErr = domain.NewErrorFileNotFound(file_id).Wrap(err)
return
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case "22P02":
domainErr = domain.NewErrorBadRequest(fmt.Sprintf("Invalid file id: %q", file_id)).Wrap(err)
return
}
}
domainErr = domain.NewErrorUnexpected().Wrap(err)
return
}
return
}
// Add file
func (s *FileRepository) Add(ctx context.Context, user_id int, name, mime string, datetime time.Time, notes string, metadata json.RawMessage) (file domain.FileCore, domainErr *domain.DomainError) {
var mime_id int
var extension string
row := s.db.QueryRow(ctx, "SELECT id, extension FROM system.mime WHERE name=$1", mime)
err := row.Scan(&mime_id, &extension)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
domainErr = domain.NewErrorMIMENotSupported(mime).Wrap(err)
return
}
domainErr = domain.NewErrorUnexpected().Wrap(err)
return
}
row = s.db.QueryRow(ctx, `
INSERT INTO data.files (name, mime_id, datetime, creator_id, notes, metadata)
VALUES (NULLIF($1, ''), $2, $3, $4, NULLIF($5 ,''), $6)
RETURNING id
`, name, mime_id, datetime, user_id, notes, metadata)
err = row.Scan(&file.ID)
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case "22007":
domainErr = domain.NewErrorBadRequest(fmt.Sprintf("Invalid datetime: %q", datetime)).Wrap(err)
return
case "23502":
domainErr = domain.NewErrorBadRequest("Unable to set NULL to some fields").Wrap(err)
return
}
}
domainErr = domain.NewErrorUnexpected().Wrap(err)
return
}
file.Name = &name
file.MIME.Name = mime
file.MIME.Extension = extension
return
}
// Update file
func (s *FileRepository) Update(ctx context.Context, file_id string, updates map[string]interface{}) (domainErr *domain.DomainError) {
if len(updates) == 0 {
// domainErr = domain.NewErrorBadRequest(nil, "No fields provided for update")
return
}
query := "UPDATE data.files SET"
newValues := []interface{}{file_id}
count := 2
for field, value := range updates {
switch field {
case "name", "notes":
query += fmt.Sprintf(" %s=NULLIF($%d, '')", field, count)
case "datetime":
query += fmt.Sprintf(" %s=NULLIF($%d, '')::timestamptz", field, count)
case "metadata":
query += fmt.Sprintf(" %s=NULLIF($%d, '')::jsonb", field, count)
default:
domainErr = domain.NewErrorBadRequest(fmt.Sprintf("Unknown field: %q", field))
return
}
newValues = append(newValues, value)
count++
}
query += fmt.Sprintf(" WHERE id=$1 AND is_deleted IS FALSE")
commandTag, err := s.db.Exec(ctx, query, newValues...)
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case "22P02":
domainErr = domain.NewErrorBadRequest("Invalid format of some values").Wrap(err)
return
case "22007":
domainErr = domain.NewErrorBadRequest(fmt.Sprintf("Invalid datetime: %q", updates["datetime"])).Wrap(err)
return
case "23502":
domainErr = domain.NewErrorBadRequest("Some fields cannot be empty").Wrap(err)
return
}
}
domainErr = domain.NewErrorUnexpected().Wrap(err)
return
}
if commandTag.RowsAffected() == 0 {
domainErr = domain.NewErrorFileNotFound(file_id).Wrap(err)
return
}
return
}
// Delete file
func (s *FileRepository) Delete(ctx context.Context, file_id string) (domainErr *domain.DomainError) {
commandTag, err := s.db.Exec(ctx,
"UPDATE data.files SET is_deleted=true WHERE id=$1 AND is_deleted IS FALSE",
file_id)
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case "22P02":
domainErr = domain.NewErrorBadRequest(fmt.Sprintf("Invalid file id: %q", file_id)).Wrap(err)
return
}
}
domainErr = domain.NewErrorUnexpected().Wrap(err)
return
}
if commandTag.RowsAffected() == 0 {
domainErr = domain.NewErrorFileNotFound(file_id).Wrap(err)
return
}
return
}
// Get list of tags of file
func (s *FileRepository) GetTags(ctx context.Context, user_id int, file_id string) (tags []domain.TagItem, domainErr *domain.DomainError) {
rows, err := s.db.Query(ctx, `
SELECT
t.id,
t.name,
t.color,
c.id,
c.name,
c.color
FROM data.tags t
LEFT JOIN data.categories c ON c.id=t.category_id
JOIN data.file_tag ft ON ft.tag_id=t.id AND ft.file_id=$2
JOIN data.files f ON f.id=$2
WHERE NOT f.is_deleted AND (f.creator_id=$1 OR (SELECT view FROM acl.files WHERE file_id=$2 AND user_id=$1) OR (SELECT is_admin FROM system.users WHERE id=$1))
`, user_id, file_id)
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && (pgErr.Code == "22P02" || pgErr.Code == "22007") {
domainErr = domain.NewErrorBadRequest(pgErr.Message).Wrap(err)
return
}
domainErr = domain.NewErrorUnexpected().Wrap(err)
return
}
defer rows.Close()
for rows.Next() {
var tag domain.TagItem
err = rows.Scan(&tag.ID, &tag.Name, &tag.Color, &tag.Category.ID, &tag.Category.Name, &tag.Category.Color)
if err != nil {
domainErr = domain.NewErrorUnexpected().Wrap(err)
return
}
tags = append(tags, tag)
}
err = rows.Err()
if err != nil {
domainErr = domain.NewErrorUnexpected().Wrap(err)
}
return
}

View File

@ -1,82 +0,0 @@
package postgres
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
// Handle database error
func handleDBError(errIn error) (statusCode int, err error) {
if errIn == nil {
statusCode = http.StatusOK
return
}
if errors.Is(errIn, pgx.ErrNoRows) {
err = fmt.Errorf("not found")
statusCode = http.StatusNotFound
return
}
var pgErr *pgconn.PgError
if errors.As(errIn, &pgErr) {
switch pgErr.Code {
case "22P02", "22007": // Invalid data format
err = fmt.Errorf("%s", pgErr.Message)
statusCode = http.StatusBadRequest
return
case "23505": // Unique constraint violation
err = fmt.Errorf("already exists")
statusCode = http.StatusConflict
return
}
}
return http.StatusInternalServerError, errIn
}
// Convert "filter" URL param to SQL "WHERE" condition
func filterToSQL(filter string) (sql string, err error) {
// filterTokens := strings.Split(string(filter), ";")
sql = "(true)"
return
}
// Convert "sort" URL param to SQL "ORDER BY"
func sortToSQL(sort string) (sql string, err error) {
if sort == "" {
return
}
sortOptions := strings.Split(sort, ",")
sql = " ORDER BY "
for i, sortOption := range sortOptions {
sortOrder := sortOption[:1]
sortColumn := sortOption[1:]
// parse sorting order marker
switch sortOrder {
case "+":
sortOrder = "ASC"
case "-":
sortOrder = "DESC"
default:
err = fmt.Errorf("invalid sorting order mark: %q", sortOrder)
return
}
// validate sorting column
var n int
n, err = strconv.Atoi(sortColumn)
if err != nil || n < 0 {
err = fmt.Errorf("invalid sorting column: %q", sortColumn)
return
}
// add sorting option to query
if i > 0 {
sql += ","
}
sql += fmt.Sprintf("%s %s NULLS LAST", sortColumn, sortOrder)
}
return
}

View File

@ -1,43 +0,0 @@
package rest
import (
"net/http"
"tanabata/internal/domain"
)
type ErrorResponse struct {
Error string `json:"error"`
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
type ErrorMapper struct{}
func (m *ErrorMapper) MapError(err domain.DomainError) (int, ErrorResponse) {
switch err.Code {
case domain.ErrCodeFileNotFound:
return http.StatusNotFound, ErrorResponse{
Error: "Not Found",
Code: string(err.Code),
Message: err.Message,
}
case domain.ErrCodeMIMENotSupported:
return http.StatusNotFound, ErrorResponse{
Error: "MIME not supported",
Code: string(err.Code),
Message: err.Message,
}
case domain.ErrCodeBadRequest:
return http.StatusNotFound, ErrorResponse{
Error: "Bad Request",
Code: string(err.Code),
Message: err.Message,
}
}
return http.StatusInternalServerError, ErrorResponse{
Error: "Internal Server Error",
Code: string(err.Code),
Message: err.Message,
}
}

25
build.sh Normal file
View File

@ -0,0 +1,25 @@
#!/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"

View File

@ -1,592 +0,0 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 14.18 (Ubuntu 14.18-0ubuntu0.22.04.1)
-- Dumped by pg_dump version 17.4
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET transaction_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Name: acl; Type: SCHEMA; Schema: -; Owner: -
--
CREATE SCHEMA acl;
--
-- Name: activity; Type: SCHEMA; Schema: -; Owner: -
--
CREATE SCHEMA activity;
--
-- Name: data; Type: SCHEMA; Schema: -; Owner: -
--
CREATE SCHEMA data;
--
-- Name: public; Type: SCHEMA; Schema: -; Owner: -
--
-- *not* creating schema, since initdb creates it
--
-- Name: system; Type: SCHEMA; Schema: -; Owner: -
--
CREATE SCHEMA system;
--
-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: -
--
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
--
-- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner: -
--
COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions';
--
-- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: -
--
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public;
--
-- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: -
--
COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)';
--
-- Name: add_file_to_tag_recursive(uuid, uuid); Type: FUNCTION; Schema: data; Owner: -
--
CREATE FUNCTION data.add_file_to_tag_recursive(f_id uuid, t_id uuid) RETURNS SETOF uuid
LANGUAGE plpgsql
AS $$
DECLARE
tmp uuid;
tt_id uuid;
ttt_id uuid;
BEGIN
INSERT INTO data.file_tag VALUES (f_id, t_id) ON CONFLICT DO NOTHING RETURNING tag_id INTO tmp;
IF tmp IS NULL THEN
RETURN;
END IF;
RETURN NEXT t_id;
FOR tt_id IN
SELECT a.add_tag_id FROM data.autotags a WHERE a.trigger_tag_id=t_id AND a.is_active
LOOP
FOR ttt_id IN SELECT data.add_file_to_tag_recursive(f_id, tt_id)
LOOP
RETURN NEXT ttt_id;
END LOOP;
END LOOP;
END;
$$;
--
-- Name: uuid_extract_timestamp(uuid); Type: FUNCTION; Schema: public; Owner: -
--
CREATE FUNCTION public.uuid_extract_timestamp(uuid_val uuid) RETURNS timestamp with time zone
LANGUAGE sql IMMUTABLE
AS $$
SELECT to_timestamp(
('x' || LEFT(REPLACE(uuid_val::TEXT, '-', ''), 12))::BIT(48)::BIGINT
/ 1000.0
);
$$;
--
-- Name: uuid_v7(timestamp with time zone); Type: FUNCTION; Schema: public; Owner: -
--
CREATE FUNCTION public.uuid_v7(cts timestamp with time zone 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
$$;
SET default_table_access_method = heap;
--
-- Name: categories; Type: TABLE; Schema: acl; Owner: -
--
CREATE TABLE acl.categories (
user_id smallint NOT NULL,
category_id uuid NOT NULL,
view boolean NOT NULL,
edit boolean NOT NULL
);
--
-- Name: files; Type: TABLE; Schema: acl; Owner: -
--
CREATE TABLE acl.files (
user_id smallint NOT NULL,
file_id uuid NOT NULL,
view boolean NOT NULL,
edit boolean NOT NULL
);
--
-- Name: pools; Type: TABLE; Schema: acl; Owner: -
--
CREATE TABLE acl.pools (
user_id smallint NOT NULL,
pool_id uuid NOT NULL,
view boolean NOT NULL,
edit boolean NOT NULL
);
--
-- Name: tags; Type: TABLE; Schema: acl; Owner: -
--
CREATE TABLE acl.tags (
user_id smallint NOT NULL,
tag_id uuid NOT NULL,
view boolean NOT NULL,
edit boolean NOT NULL
);
--
-- Name: file_views; Type: TABLE; Schema: activity; Owner: -
--
CREATE TABLE activity.file_views (
file_id uuid NOT NULL,
"timestamp" timestamp with time zone NOT NULL,
user_id smallint NOT NULL
);
--
-- Name: pool_views; Type: TABLE; Schema: activity; Owner: -
--
CREATE TABLE activity.pool_views (
pool_id uuid NOT NULL,
"timestamp" timestamp with time zone NOT NULL,
user_id smallint NOT NULL
);
--
-- Name: sessions; Type: TABLE; Schema: activity; Owner: -
--
CREATE TABLE activity.sessions (
id integer NOT NULL,
token text NOT NULL,
user_id smallint NOT NULL,
user_agent character varying(256) NOT NULL,
started_at timestamp with time zone DEFAULT statement_timestamp() NOT NULL,
expires_at timestamp with time zone,
last_activity timestamp with time zone DEFAULT statement_timestamp() NOT NULL
);
--
-- Name: sessions_id_seq; Type: SEQUENCE; Schema: activity; Owner: -
--
CREATE SEQUENCE activity.sessions_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: sessions_id_seq; Type: SEQUENCE OWNED BY; Schema: activity; Owner: -
--
ALTER SEQUENCE activity.sessions_id_seq OWNED BY activity.sessions.id;
--
-- Name: tag_uses; Type: TABLE; Schema: activity; Owner: -
--
CREATE TABLE activity.tag_uses (
tag_id uuid NOT NULL,
"timestamp" timestamp with time zone NOT NULL,
user_id smallint NOT NULL,
included boolean NOT NULL
);
--
-- Name: autotags; Type: TABLE; Schema: data; Owner: -
--
CREATE TABLE data.autotags (
trigger_tag_id uuid NOT NULL,
add_tag_id uuid NOT NULL,
is_active boolean DEFAULT true NOT NULL
);
--
-- Name: categories; Type: TABLE; Schema: data; Owner: -
--
CREATE TABLE data.categories (
id uuid DEFAULT public.uuid_v7() NOT NULL,
name character varying(256) NOT NULL,
notes text DEFAULT ''::text NOT NULL,
color character(6),
creator_id smallint NOT NULL
);
--
-- Name: file_pool; Type: TABLE; Schema: data; Owner: -
--
CREATE TABLE data.file_pool (
file_id uuid NOT NULL,
pool_id uuid NOT NULL,
number smallint NOT NULL
);
--
-- Name: file_tag; Type: TABLE; Schema: data; Owner: -
--
CREATE TABLE data.file_tag (
file_id uuid NOT NULL,
tag_id uuid NOT NULL
);
--
-- Name: files; Type: TABLE; Schema: data; Owner: -
--
CREATE TABLE data.files (
id uuid DEFAULT public.uuid_v7() NOT NULL,
name character varying(256),
mime_id smallint NOT NULL,
datetime timestamp with time zone DEFAULT clock_timestamp() NOT NULL,
notes text,
metadata jsonb NOT NULL,
creator_id smallint NOT NULL,
is_deleted boolean DEFAULT false NOT NULL
);
--
-- Name: pools; Type: TABLE; Schema: data; Owner: -
--
CREATE TABLE data.pools (
id uuid DEFAULT public.uuid_v7() NOT NULL,
name character varying(256) NOT NULL,
notes text,
creator_id smallint NOT NULL
);
--
-- Name: tags; Type: TABLE; Schema: data; Owner: -
--
CREATE TABLE data.tags (
id uuid DEFAULT public.uuid_v7() NOT NULL,
name character varying(256) NOT NULL,
notes text,
color character(6),
category_id uuid,
creator_id smallint NOT NULL
);
--
-- Name: mime; Type: TABLE; Schema: system; Owner: -
--
CREATE TABLE system.mime (
id smallint NOT NULL,
name character varying(127) NOT NULL,
extension character varying(16) NOT NULL
);
--
-- Name: mime_id_seq; Type: SEQUENCE; Schema: system; Owner: -
--
CREATE SEQUENCE system.mime_id_seq
AS smallint
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: mime_id_seq; Type: SEQUENCE OWNED BY; Schema: system; Owner: -
--
ALTER SEQUENCE system.mime_id_seq OWNED BY system.mime.id;
--
-- Name: users; Type: TABLE; Schema: system; Owner: -
--
CREATE TABLE system.users (
id smallint NOT NULL,
name character varying(32) NOT NULL,
password text NOT NULL,
is_admin boolean DEFAULT false NOT NULL,
can_create boolean DEFAULT false NOT NULL
);
--
-- Name: users_id_seq; Type: SEQUENCE; Schema: system; Owner: -
--
CREATE SEQUENCE system.users_id_seq
AS smallint
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: system; Owner: -
--
ALTER SEQUENCE system.users_id_seq OWNED BY system.users.id;
--
-- Name: sessions id; Type: DEFAULT; Schema: activity; Owner: -
--
ALTER TABLE ONLY activity.sessions ALTER COLUMN id SET DEFAULT nextval('activity.sessions_id_seq'::regclass);
--
-- Name: mime id; Type: DEFAULT; Schema: system; Owner: -
--
ALTER TABLE ONLY system.mime ALTER COLUMN id SET DEFAULT nextval('system.mime_id_seq'::regclass);
--
-- Name: users id; Type: DEFAULT; Schema: system; Owner: -
--
ALTER TABLE ONLY system.users ALTER COLUMN id SET DEFAULT nextval('system.users_id_seq'::regclass);
--
-- Name: categories categories_pkey; Type: CONSTRAINT; Schema: acl; Owner: -
--
ALTER TABLE ONLY acl.categories
ADD CONSTRAINT categories_pkey PRIMARY KEY (user_id, category_id);
--
-- Name: files files_pkey; Type: CONSTRAINT; Schema: acl; Owner: -
--
ALTER TABLE ONLY acl.files
ADD CONSTRAINT files_pkey PRIMARY KEY (user_id, file_id);
--
-- Name: pools pools_pkey; Type: CONSTRAINT; Schema: acl; Owner: -
--
ALTER TABLE ONLY acl.pools
ADD CONSTRAINT pools_pkey PRIMARY KEY (user_id, pool_id);
--
-- Name: tags tags_pkey; Type: CONSTRAINT; Schema: acl; Owner: -
--
ALTER TABLE ONLY acl.tags
ADD CONSTRAINT tags_pkey PRIMARY KEY (user_id, tag_id);
--
-- Name: file_views file_views_pkey; Type: CONSTRAINT; Schema: activity; Owner: -
--
ALTER TABLE ONLY activity.file_views
ADD CONSTRAINT file_views_pkey PRIMARY KEY (file_id, "timestamp", user_id);
--
-- Name: pool_views pool_views_pkey; Type: CONSTRAINT; Schema: activity; Owner: -
--
ALTER TABLE ONLY activity.pool_views
ADD CONSTRAINT pool_views_pkey PRIMARY KEY (pool_id, "timestamp", user_id);
--
-- Name: sessions sessions_pkey; Type: CONSTRAINT; Schema: activity; Owner: -
--
ALTER TABLE ONLY activity.sessions
ADD CONSTRAINT sessions_pkey PRIMARY KEY (id);
--
-- Name: tag_uses tag_uses_pkey; Type: CONSTRAINT; Schema: activity; Owner: -
--
ALTER TABLE ONLY activity.tag_uses
ADD CONSTRAINT tag_uses_pkey PRIMARY KEY (tag_id, "timestamp", user_id);
--
-- Name: autotags autotags_pkey; Type: CONSTRAINT; Schema: data; Owner: -
--
ALTER TABLE ONLY data.autotags
ADD CONSTRAINT autotags_pkey PRIMARY KEY (trigger_tag_id, add_tag_id);
--
-- Name: categories categories_pkey; Type: CONSTRAINT; Schema: data; Owner: -
--
ALTER TABLE ONLY data.categories
ADD CONSTRAINT categories_pkey PRIMARY KEY (id);
--
-- Name: file_pool file_pool_pkey; Type: CONSTRAINT; Schema: data; Owner: -
--
ALTER TABLE ONLY data.file_pool
ADD CONSTRAINT file_pool_pkey PRIMARY KEY (file_id, pool_id, number);
--
-- Name: file_tag file_tag_pkey; Type: CONSTRAINT; Schema: data; Owner: -
--
ALTER TABLE ONLY data.file_tag
ADD CONSTRAINT file_tag_pkey PRIMARY KEY (file_id, tag_id);
--
-- Name: files files_pkey; Type: CONSTRAINT; Schema: data; Owner: -
--
ALTER TABLE ONLY data.files
ADD CONSTRAINT files_pkey PRIMARY KEY (id);
--
-- Name: pools pools_pkey; Type: CONSTRAINT; Schema: data; Owner: -
--
ALTER TABLE ONLY data.pools
ADD CONSTRAINT pools_pkey PRIMARY KEY (id);
--
-- Name: tags tags_pkey; Type: CONSTRAINT; Schema: data; Owner: -
--
ALTER TABLE ONLY data.tags
ADD CONSTRAINT tags_pkey PRIMARY KEY (id);
--
-- Name: mime mime_pkey; Type: CONSTRAINT; Schema: system; Owner: -
--
ALTER TABLE ONLY system.mime
ADD CONSTRAINT mime_pkey PRIMARY KEY (id);
--
-- Name: users users_pkey; Type: CONSTRAINT; Schema: system; Owner: -
--
ALTER TABLE ONLY system.users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
--
-- PostgreSQL database dump complete
--

View File

@ -1,212 +0,0 @@
@startuml Tanabata File Manager entity relationship diagram
' skinparam linetype ortho
' ========== SYSTEM ==========
entity "system.users" as usr {
* id : smallserial <<generated>>
--
* name : varchar(32)
* password : text
* is_admin : boolean
* can_create : boolean
}
entity "system.mime" as mime {
* id : smallserial <<generated>>
--
* name : varchar(127)
* extension : varchar(16)
}
' ========== DATA ==========
entity "data.categories" as cty {
* id : uuid <<generated>>
--
* name : varchar(256)
notes : text
color : char(6)
' * created_at : timestamptz <<generated>>
* creator_id : smallint
' * is_private : boolean
}
cty::creator_id }o--|| usr::id
entity "data.files" as fle {
* id : uuid <<generated>>
--
name : varchar(256)
* mime_id : smallint
* datetime : timestamptz
notes : text
* metadata : jsonb
' * created_at : timestamptz <<generated>>
* creator_id : smallint
' * is_private : boolean
* is_deleted : boolean
}
fle::mime_id }o--|| mime::id
fle::creator_id }o--|| usr::id
entity "data.tags" as tag {
* id : uuid <<generated>>
--
* name : varchar(256)
notes : text
color : char(6)
category_id : uuid
' * created_at : timestamptz <<generated>>
* creator_id : smallint
' * is_private : boolean
}
tag::category_id }o--o| cty::id
tag::creator_id }o--|| usr::id
entity "data.file_tag" as ft {
* file_id : uuid
* tag_id : uuid
}
ft::file_id }o--|| fle::id
ft::tag_id }o--|| tag::id
entity "data.autotags" as atg {
* trigger_tag_id : uuid
* add_tag_id : uuid
--
* is_active : boolean
}
atg::trigger_tag_id }o--|| tag::id
atg::add_tag_id }o--|| tag::id
entity "data.pools" as pool {
* id : uuid <<generated>>
--
* name : varchar(256)
notes : text
' parent_id : uuid
' * created_at : timestamptz
* creator_id : smallint
' * is_private : boolean
}
pool::creator_id }o--|| usr::id
' pool::parent_id }o--o| pool::id
entity "data.file_pool" as fp {
* file_id : uuid
* pool_id : uuid
* number : smallint
}
fp::file_id }o--|| fle::id
fp::pool_id }o--|| pool::id
' ========== ACL ==========
entity "acl.files" as acl_f {
* user_id : smallint
* file_id : uuid
--
* view : boolean
* edit : boolean
}
acl_f::user_id }o--|| usr::id
acl_f::file_id }o--|| fle::id
entity "acl.tags" as acl_t {
* user_id : smallint
* tag_id : uuid
--
* view : boolean
* edit : boolean
' * files_view : boolean
' * files_edit : boolean
}
acl_t::user_id }o--|| usr::id
acl_t::tag_id }o--|| tag::id
entity "acl.categories" as acl_c {
* user_id : smallint
* category_id : uuid
--
* view : boolean
* edit : boolean
' * tags_view : boolean
' * tags_edit : boolean
}
acl_c::user_id }o--|| usr::id
acl_c::category_id }o--|| cty::id
entity "acl.pools" as acl_p {
* user_id : smallint
* pool_id : uuid
--
* view : boolean
* edit : boolean
' * files_view : boolean
' * files_edit : boolean
}
acl_p::user_id }o--|| usr::id
acl_p::pool_id }o--|| pool::id
' ========== ACTIVITY ==========
entity "activity.sessions" as ssn {
* id : serial <<generated>>
--
* token : text
* user_id : smallint
* user_agent : varchar(512)
* started_at : timestamptz
expires_at : timestamptz
* last_activity : timestamptz
}
ssn::user_id }o--|| usr::id
entity "activity.file_views" as fv {
* file_id : uuid
* timestamp : timestamptz
* user_id : smallint
}
fv::file_id }o--|| fle::id
fv::user_id }o--|| usr::id
entity "activity.tag_uses" as tu {
* tag_id : uuid
* timestamp : timestamptz
* user_id : smallint
--
* included : boolean
}
tu::tag_id }o--|| tag::id
tu::user_id }o--|| usr::id
entity "activity.pool_views" as pv {
* pool_id : uuid
* timestamp : timestamptz
* user_id : smallint
}
pv::pool_id }o--|| pool::id
pv::user_id }o--|| usr::id
@enduml

56
docs/fm.md Normal file
View File

@ -0,0 +1,56 @@
# 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.

103
include/core.h Normal file
View File

@ -0,0 +1,103 @@
// 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

86
include/tanabata.h Normal file
View File

@ -0,0 +1,86 @@
// 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

27
include/tdbms-client.h Normal file
View File

@ -0,0 +1,27 @@
// 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

58
include/tdbms.h Normal file
View File

@ -0,0 +1,58 @@
// 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

111
tanabata/core/core_func.h Normal file
View File

@ -0,0 +1,111 @@
// 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

222
tanabata/core/sappyou.c Normal file
View File

@ -0,0 +1,222 @@
#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(&current_tanzaku->created_ts, 8, 1, temp.file) != 1 ||
fread(&current_tanzaku->modified_ts, 8, 1, temp.file) != 1 ||
getdelim(&current_tanzaku->name, &max_string_len, 0, temp.file) == -1 ||
getdelim(&current_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(&current_tanzaku->created_ts, 8, 1, sappyou->file) != 1 ||
fwrite(&current_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;
}

206
tanabata/core/sasahyou.c Normal file
View File

@ -0,0 +1,206 @@
#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(&current_sasa->created_ts, 8, 1, temp.file) != 1 ||
getdelim(&current_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(&current_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;
}

208
tanabata/core/shoppyou.c Normal file
View File

@ -0,0 +1,208 @@
#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(&current_kazari->created_ts, 8, 1, temp.file) != 1 ||
fread(&current_kazari->sasa_id, 8, 1, temp.file) != 1 ||
fread(&current_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(&current_kazari->created_ts, 8, 1, shoppyou->file) != 1 ||
fwrite(&current_kazari->sasa_id, 8, 1, shoppyou->file) != 1 ||
fwrite(&current_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;
}

199
tanabata/lib/database.c Normal file
View File

@ -0,0 +1,199 @@
#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;
}

68
tanabata/lib/kazari.c Normal file
View File

@ -0,0 +1,68 @@
#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;
}

25
tanabata/lib/sasa.c Normal file
View File

@ -0,0 +1,25 @@
#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];
}

25
tanabata/lib/tanzaku.c Normal file
View File

@ -0,0 +1,25 @@
#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];
}

74
tdbms/cli/tdbms-cli.c Normal file
View File

@ -0,0 +1,74 @@
#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;
}

View File

@ -0,0 +1,90 @@
#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;
}

92
tdbms/install.sh Normal file
View File

@ -0,0 +1,92 @@
#!/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'"

1028
tdbms/server/tdbms-server.c Normal file

File diff suppressed because it is too large Load Diff

14
tdbms/tdbms.service Normal file
View File

@ -0,0 +1,14 @@
[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

613
tfm/cli/tfm-cli.c Normal file
View File

@ -0,0 +1,613 @@
#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) ""s""
#define HIGHLIGHT(s) ""s""
#define SUCCESS(s) ""s""
#define ERROR(s) ""s""
#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 *) &current_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 *) &current_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 *) &current_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;
}

59
web/install.sh Normal file
View File

@ -0,0 +1,59 @@
#!/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'"

51
web/public/auth.html Normal file
View File

@ -0,0 +1,51 @@
<!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>

View File

@ -0,0 +1,2 @@
<?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>

3
web/public/css/auth.css Normal file
View File

@ -0,0 +1,3 @@
.btn-secondary {
display: none;
}

7
web/public/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

151
web/public/css/general.css Normal file
View File

@ -0,0 +1,151 @@
@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;
}

46
web/public/css/tdbms.css Normal file
View File

@ -0,0 +1,46 @@
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;
}

194
web/public/css/tfm.css Normal file
View File

@ -0,0 +1,194 @@
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;
}

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

39
web/public/index.html Normal file
View File

@ -0,0 +1,39 @@
<!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>

37
web/public/js/auth.js Normal file
View File

@ -0,0 +1,37 @@
$("#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;
}
});

2
web/public/js/jquery-3.6.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
/*!
* 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))}});

2
web/public/js/jquery.lazy.min.js vendored Normal file
View File

@ -0,0 +1,2 @@
/*! 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);

View File

@ -0,0 +1,52 @@
$(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!");
});

View File

@ -0,0 +1,7 @@
var db_name = localStorage["db_name"];
$(window).on("load", function (e) {
if (db_name != null) {
$(".db_name").text(db_name);
}
});

View File

@ -0,0 +1,8 @@
db_name = localStorage["db_name"];
if (db_name == null) {
location.href = "/tdbms/settings";
}
$(window).on("load", function (e) {
$(".db_name").text(db_name);
});

View File

@ -0,0 +1,20 @@
$(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!");
});

View File

@ -0,0 +1,9 @@
$(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;
});
});

View File

@ -0,0 +1,9 @@
$(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;
});
});

View File

@ -0,0 +1,71 @@
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!");
});

View File

@ -0,0 +1,9 @@
$(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;
});
});

View File

@ -0,0 +1,16 @@
$(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>`
);
});

204
web/public/js/tdbms.js Normal file
View File

@ -0,0 +1,204 @@
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);
}

View File

@ -0,0 +1,36 @@
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!");
});

View File

@ -0,0 +1,37 @@
$(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);
});

View File

@ -0,0 +1,364 @@
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();
});

View File

@ -0,0 +1,99 @@
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!");
});

46
web/public/js/tfm-tags.js Normal file
View File

@ -0,0 +1,46 @@
$(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);
});

2
web/public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@ -0,0 +1,47 @@
{
"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"
}
]
}

View File

@ -0,0 +1,53 @@
<!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>

53
web/public/tdbms/new.html Normal file
View File

@ -0,0 +1,53 @@
<!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>

View File

@ -0,0 +1,49 @@
<!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>

View File

@ -0,0 +1,47 @@
<!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>

View File

@ -0,0 +1,111 @@
<!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>

Some files were not shown because too many files have changed in this diff Show More