Compare commits
No commits in common. "master" and "archive-c" have entirely different histories.
57
CMakeLists.txt
Normal 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
@ -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>© 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
@ -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
|
||||
@ -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
|
||||
)
|
||||
@ -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=
|
||||
@ -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"`
|
||||
}
|
||||
@ -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",
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
@ -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"
|
||||
@ -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
|
||||
--
|
||||
|
||||
212
docs/erd.puml
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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(¤t_tanzaku->created_ts, 8, 1, temp.file) != 1 ||
|
||||
fread(¤t_tanzaku->modified_ts, 8, 1, temp.file) != 1 ||
|
||||
getdelim(¤t_tanzaku->name, &max_string_len, 0, temp.file) == -1 ||
|
||||
getdelim(¤t_tanzaku->description, &max_string_len, 0, temp.file) == -1) {
|
||||
temp.file = NULL;
|
||||
sappyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
current_tanzaku->id = HOLE_ID;
|
||||
if (r == 0) {
|
||||
temp.file = NULL;
|
||||
sappyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
r--;
|
||||
temp.holes[r] = current_tanzaku;
|
||||
}
|
||||
}
|
||||
if (fflush(temp.file) == 0) {
|
||||
sappyou->file = NULL;
|
||||
sappyou_free(sappyou);
|
||||
*sappyou = temp;
|
||||
return 0;
|
||||
}
|
||||
temp.file = NULL;
|
||||
sappyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sappyou_save(Sappyou *sappyou) {
|
||||
if (sappyou->file == NULL ||
|
||||
(sappyou->file = freopen(NULL, "wb", sappyou->file)) == NULL ||
|
||||
fwrite(SAPPYOU_SIG, 2, 4, sappyou->file) != 4 ||
|
||||
fwrite(&sappyou->created_ts, 8, 1, sappyou->file) != 1 ||
|
||||
fwrite(&sappyou->modified_ts, 8, 1, sappyou->file) != 1 ||
|
||||
fwrite(&sappyou->size, 8, 1, sappyou->file) != 1 ||
|
||||
fwrite(&sappyou->hole_cnt, 8, 1, sappyou->file) != 1 ||
|
||||
fflush(sappyou->file) != 0) {
|
||||
return 1;
|
||||
}
|
||||
Tanzaku *current_tanzaku = sappyou->database;
|
||||
for (uint64_t i = 0; i < sappyou->size; i++, current_tanzaku++) {
|
||||
if (current_tanzaku->id != HOLE_ID) {
|
||||
if (fputc(0xff, sappyou->file) == EOF ||
|
||||
fwrite(¤t_tanzaku->created_ts, 8, 1, sappyou->file) != 1 ||
|
||||
fwrite(¤t_tanzaku->modified_ts, 8, 1, sappyou->file) != 1 ||
|
||||
fputs(current_tanzaku->name, sappyou->file) == EOF ||
|
||||
fputc(0, sappyou->file) == EOF ||
|
||||
fputs(current_tanzaku->description, sappyou->file) == EOF ||
|
||||
fputc(0, sappyou->file) == EOF) {
|
||||
return 1;
|
||||
}
|
||||
} else if (fputc(0, sappyou->file) == EOF) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return fflush(sappyou->file);
|
||||
}
|
||||
|
||||
int sappyou_open(Sappyou *sappyou, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (sappyou->file == NULL && (sappyou->file = fopen(path, "rb")) == NULL ||
|
||||
sappyou->file != NULL && (sappyou->file = freopen(path, "rb", sappyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
return sappyou_load(sappyou);
|
||||
}
|
||||
|
||||
int sappyou_dump(Sappyou *sappyou, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (sappyou->file == NULL && (sappyou->file = fopen(path, "wb")) == NULL ||
|
||||
sappyou->file != NULL && (sappyou->file = freopen(path, "wb", sappyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
return sappyou_save(sappyou);
|
||||
}
|
||||
|
||||
Tanzaku tanzaku_add(Sappyou *sappyou, const char *name, const char *description) {
|
||||
if (name == NULL || description == NULL || sappyou->size == -1 && sappyou->hole_cnt == 0) {
|
||||
return HOLE_TANZAKU;
|
||||
}
|
||||
Tanzaku newbie;
|
||||
newbie.created_ts = time(NULL);
|
||||
newbie.modified_ts = newbie.created_ts;
|
||||
newbie.name = malloc(strlen(name) + 1);
|
||||
strcpy(newbie.name, name);
|
||||
newbie.description = malloc(strlen(description) + 1);
|
||||
strcpy(newbie.description, description);
|
||||
if (sappyou->hole_cnt > 0) {
|
||||
sappyou->hole_cnt--;
|
||||
Tanzaku **hole_ptr = sappyou->holes + sappyou->hole_cnt;
|
||||
newbie.id = *hole_ptr - sappyou->database;
|
||||
**hole_ptr = newbie;
|
||||
sappyou->holes = reallocarray(sappyou->holes, sappyou->hole_cnt, sizeof(Tanzaku *));
|
||||
} else {
|
||||
newbie.id = sappyou->size;
|
||||
sappyou->size++;
|
||||
sappyou->database = reallocarray(sappyou->database, sappyou->size, sizeof(Tanzaku));
|
||||
sappyou->database[newbie.id] = newbie;
|
||||
}
|
||||
sappyou->modified_ts = newbie.created_ts;
|
||||
return newbie;
|
||||
}
|
||||
|
||||
int tanzaku_rem(Sappyou *sappyou, uint64_t tanzaku_id) {
|
||||
if (tanzaku_id == HOLE_ID || tanzaku_id >= sappyou->size) {
|
||||
return 1;
|
||||
}
|
||||
Tanzaku *current_tanzaku = sappyou->database + tanzaku_id;
|
||||
if (current_tanzaku->id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
current_tanzaku->id = HOLE_ID;
|
||||
free(current_tanzaku->name);
|
||||
free(current_tanzaku->description);
|
||||
if (tanzaku_id == sappyou->size - 1) {
|
||||
sappyou->size--;
|
||||
sappyou->database = reallocarray(sappyou->database, sappyou->size, sizeof(Tanzaku));
|
||||
} else {
|
||||
sappyou->hole_cnt++;
|
||||
sappyou->holes = reallocarray(sappyou->holes, sappyou->hole_cnt, sizeof(Tanzaku *));
|
||||
sappyou->holes[sappyou->hole_cnt - 1] = current_tanzaku;
|
||||
}
|
||||
sappyou->modified_ts = time(NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tanzaku_upd(Sappyou *sappyou, uint64_t tanzaku_id, const char *name, const char *description) {
|
||||
if (tanzaku_id == HOLE_ID || tanzaku_id >= sappyou->size || name == NULL && description == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Tanzaku *current_tanzaku = sappyou->database + tanzaku_id;
|
||||
if (current_tanzaku->id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
if (name != NULL) {
|
||||
current_tanzaku->name = realloc(current_tanzaku->name, strlen(name) + 1);
|
||||
strcpy(current_tanzaku->name, name);
|
||||
}
|
||||
if (description != NULL) {
|
||||
current_tanzaku->description = realloc(current_tanzaku->description, strlen(description) + 1);
|
||||
strcpy(current_tanzaku->description, description);
|
||||
}
|
||||
sappyou->modified_ts = time(NULL);
|
||||
current_tanzaku->modified_ts = sappyou->modified_ts;
|
||||
return 0;
|
||||
}
|
||||
206
tanabata/core/sasahyou.c
Normal 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(¤t_sasa->created_ts, 8, 1, temp.file) != 1 ||
|
||||
getdelim(¤t_sasa->path, &max_path_len, 0, temp.file) == -1) {
|
||||
temp.file = NULL;
|
||||
sasahyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
current_sasa->id = HOLE_ID;
|
||||
if (r == 0) {
|
||||
temp.file = NULL;
|
||||
sasahyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
r--;
|
||||
temp.holes[r] = current_sasa;
|
||||
}
|
||||
}
|
||||
if (fflush(temp.file) == 0) {
|
||||
sasahyou->file = NULL;
|
||||
sasahyou_free(sasahyou);
|
||||
*sasahyou = temp;
|
||||
return 0;
|
||||
}
|
||||
temp.file = NULL;
|
||||
sasahyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sasahyou_save(Sasahyou *sasahyou) {
|
||||
if (sasahyou->file == NULL ||
|
||||
(sasahyou->file = freopen(NULL, "wb", sasahyou->file)) == NULL ||
|
||||
fwrite(SASAHYOU_SIG, 2, 4, sasahyou->file) != 4 ||
|
||||
fwrite(&sasahyou->created_ts, 8, 1, sasahyou->file) != 1 ||
|
||||
fwrite(&sasahyou->modified_ts, 8, 1, sasahyou->file) != 1 ||
|
||||
fwrite(&sasahyou->size, 8, 1, sasahyou->file) != 1 ||
|
||||
fwrite(&sasahyou->hole_cnt, 8, 1, sasahyou->file) != 1 ||
|
||||
fflush(sasahyou->file) != 0) {
|
||||
return 1;
|
||||
}
|
||||
Sasa *current_sasa = sasahyou->database;
|
||||
for (uint64_t i = 0; i < sasahyou->size; i++, current_sasa++) {
|
||||
if (current_sasa->id != HOLE_ID) {
|
||||
if (fputc(0xff, sasahyou->file) == EOF ||
|
||||
fwrite(¤t_sasa->created_ts, 8, 1, sasahyou->file) != 1 ||
|
||||
fputs(current_sasa->path, sasahyou->file) == EOF ||
|
||||
fputc(0, sasahyou->file) == EOF) {
|
||||
return 1;
|
||||
}
|
||||
} else if (fputc(0, sasahyou->file) == EOF) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return fflush(sasahyou->file);
|
||||
}
|
||||
|
||||
int sasahyou_open(Sasahyou *sasahyou, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (sasahyou->file == NULL && (sasahyou->file = fopen(path, "rb")) == NULL ||
|
||||
sasahyou->file != NULL && (sasahyou->file = freopen(path, "rb", sasahyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
return sasahyou_load(sasahyou);
|
||||
}
|
||||
|
||||
int sasahyou_dump(Sasahyou *sasahyou, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (sasahyou->file == NULL && (sasahyou->file = fopen(path, "wb")) == NULL ||
|
||||
sasahyou->file != NULL && (sasahyou->file = freopen(path, "wb", sasahyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
return sasahyou_save(sasahyou);
|
||||
}
|
||||
|
||||
Sasa sasa_add(Sasahyou *sasahyou, const char *path) {
|
||||
if (path == NULL || sasahyou->size == -1 && sasahyou->hole_cnt == 0) {
|
||||
return HOLE_SASA;
|
||||
}
|
||||
Sasa newbie;
|
||||
newbie.created_ts = time(NULL);
|
||||
newbie.path = malloc(strlen(path) + 1);
|
||||
strcpy(newbie.path, path);
|
||||
if (sasahyou->hole_cnt > 0) {
|
||||
sasahyou->hole_cnt--;
|
||||
Sasa **hole_ptr = sasahyou->holes + sasahyou->hole_cnt;
|
||||
newbie.id = *hole_ptr - sasahyou->database;
|
||||
**hole_ptr = newbie;
|
||||
sasahyou->holes = reallocarray(sasahyou->holes, sasahyou->hole_cnt, sizeof(Sasa *));
|
||||
} else {
|
||||
newbie.id = sasahyou->size;
|
||||
sasahyou->size++;
|
||||
sasahyou->database = reallocarray(sasahyou->database, sasahyou->size, sizeof(Sasa));
|
||||
sasahyou->database[newbie.id] = newbie;
|
||||
}
|
||||
sasahyou->modified_ts = newbie.created_ts;
|
||||
return newbie;
|
||||
}
|
||||
|
||||
int sasa_rem(Sasahyou *sasahyou, uint64_t sasa_id) {
|
||||
if (sasa_id == HOLE_ID || sasa_id >= sasahyou->size) {
|
||||
return 1;
|
||||
}
|
||||
Sasa *current_sasa = sasahyou->database + sasa_id;
|
||||
if (current_sasa->id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
current_sasa->id = HOLE_ID;
|
||||
free(current_sasa->path);
|
||||
current_sasa->path = NULL;
|
||||
if (sasa_id == sasahyou->size - 1) {
|
||||
sasahyou->size--;
|
||||
sasahyou->database = reallocarray(sasahyou->database, sasahyou->size, sizeof(Sasa));
|
||||
} else {
|
||||
sasahyou->hole_cnt++;
|
||||
sasahyou->holes = reallocarray(sasahyou->holes, sasahyou->hole_cnt, sizeof(Sasa *));
|
||||
sasahyou->holes[sasahyou->hole_cnt - 1] = current_sasa;
|
||||
}
|
||||
sasahyou->modified_ts = time(NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sasa_upd(Sasahyou *sasahyou, uint64_t sasa_id, const char *path) {
|
||||
if (sasa_id == HOLE_ID || sasa_id >= sasahyou->size || path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Sasa *current_sasa = sasahyou->database + sasa_id;
|
||||
if (current_sasa->id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
current_sasa->path = realloc(current_sasa->path, strlen(path) + 1);
|
||||
strcpy(current_sasa->path, path);
|
||||
sasahyou->modified_ts = time(NULL);
|
||||
return 0;
|
||||
}
|
||||
208
tanabata/core/shoppyou.c
Normal 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(¤t_kazari->created_ts, 8, 1, temp.file) != 1 ||
|
||||
fread(¤t_kazari->sasa_id, 8, 1, temp.file) != 1 ||
|
||||
fread(¤t_kazari->tanzaku_id, 8, 1, temp.file) != 1) {
|
||||
temp.file = NULL;
|
||||
shoppyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (fflush(temp.file) == 0) {
|
||||
shoppyou->file = NULL;
|
||||
shoppyou_free(shoppyou);
|
||||
*shoppyou = temp;
|
||||
return 0;
|
||||
}
|
||||
temp.file = NULL;
|
||||
shoppyou_free(&temp);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int shoppyou_save(Shoppyou *shoppyou) {
|
||||
if (shoppyou->file == NULL ||
|
||||
(shoppyou->file = freopen(NULL, "wb", shoppyou->file)) == NULL ||
|
||||
fwrite(SHOPPYOU_SIG, 2, 4, shoppyou->file) != 4 ||
|
||||
fwrite(&shoppyou->created_ts, 8, 1, shoppyou->file) != 1 ||
|
||||
fwrite(&shoppyou->modified_ts, 8, 1, shoppyou->file) != 1) {
|
||||
return 1;
|
||||
}
|
||||
uint64_t size = shoppyou->size - shoppyou->hole_cnt;
|
||||
if (fwrite(&size, 8, 1, shoppyou->file) != 1 ||
|
||||
fflush(shoppyou->file) != 0) {
|
||||
return 1;
|
||||
}
|
||||
Kazari *current_kazari = shoppyou->database;
|
||||
for (uint64_t i = 0; i < shoppyou->size; i++, current_kazari++) {
|
||||
if (shoppyou->database[i].sasa_id != HOLE_ID && shoppyou->database[i].tanzaku_id != HOLE_ID) {
|
||||
if (fwrite(¤t_kazari->created_ts, 8, 1, shoppyou->file) != 1 ||
|
||||
fwrite(¤t_kazari->sasa_id, 8, 1, shoppyou->file) != 1 ||
|
||||
fwrite(¤t_kazari->tanzaku_id, 8, 1, shoppyou->file) != 1) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fflush(shoppyou->file);
|
||||
}
|
||||
|
||||
int shoppyou_open(Shoppyou *shoppyou, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (shoppyou->file == NULL && (shoppyou->file = fopen(path, "rb")) == NULL ||
|
||||
shoppyou->file != NULL && (shoppyou->file = freopen(path, "rb", shoppyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
return shoppyou_load(shoppyou);
|
||||
}
|
||||
|
||||
int shoppyou_dump(Shoppyou *shoppyou, const char *path) {
|
||||
if (path == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (shoppyou->file == NULL && (shoppyou->file = fopen(path, "wb")) == NULL ||
|
||||
shoppyou->file != NULL && (shoppyou->file = freopen(path, "wb", shoppyou->file)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
return shoppyou_save(shoppyou);
|
||||
}
|
||||
|
||||
int kazari_add(Shoppyou *shoppyou, uint64_t sasa_id, uint64_t tanzaku_id) {
|
||||
if (sasa_id == HOLE_ID || tanzaku_id == HOLE_ID || shoppyou->size == -1 && shoppyou->hole_cnt == 0) {
|
||||
return 1;
|
||||
}
|
||||
Kazari newbie;
|
||||
newbie.created_ts = time(NULL);
|
||||
newbie.sasa_id = sasa_id;
|
||||
newbie.tanzaku_id = tanzaku_id;
|
||||
if (shoppyou->hole_cnt > 0) {
|
||||
shoppyou->hole_cnt--;
|
||||
**(shoppyou->holes + shoppyou->hole_cnt) = newbie;
|
||||
shoppyou->holes = reallocarray(shoppyou->holes, shoppyou->hole_cnt, sizeof(Kazari *));
|
||||
} else {
|
||||
shoppyou->size++;
|
||||
shoppyou->database = reallocarray(shoppyou->database, shoppyou->size, sizeof(Kazari));
|
||||
shoppyou->database[shoppyou->size - 1] = newbie;
|
||||
}
|
||||
shoppyou->modified_ts = newbie.created_ts;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int kazari_rem(Shoppyou *shoppyou, uint64_t sasa_id, uint64_t tanzaku_id) {
|
||||
if (sasa_id == HOLE_ID || tanzaku_id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
Kazari *current_kazari = shoppyou->database;
|
||||
for (uint64_t i = 0; i < shoppyou->size; i++, current_kazari++) {
|
||||
if (current_kazari->sasa_id == sasa_id && current_kazari->tanzaku_id == tanzaku_id) {
|
||||
current_kazari->sasa_id = HOLE_ID;
|
||||
current_kazari->tanzaku_id = HOLE_ID;
|
||||
shoppyou->hole_cnt++;
|
||||
shoppyou->holes = reallocarray(shoppyou->holes, shoppyou->hole_cnt, sizeof(Kazari *));
|
||||
shoppyou->holes[shoppyou->hole_cnt - 1] = current_kazari;
|
||||
shoppyou->modified_ts = time(NULL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int kazari_rem_by_sasa(Shoppyou *shoppyou, uint64_t sasa_id) {
|
||||
if (sasa_id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
Kazari *current_kazari = shoppyou->database;
|
||||
_Bool changed = 0;
|
||||
for (uint64_t i = 0; i < shoppyou->size; i++, current_kazari++) {
|
||||
if (current_kazari->sasa_id == sasa_id) {
|
||||
current_kazari->sasa_id = HOLE_ID;
|
||||
current_kazari->tanzaku_id = HOLE_ID;
|
||||
shoppyou->hole_cnt++;
|
||||
shoppyou->holes = reallocarray(shoppyou->holes, shoppyou->hole_cnt, sizeof(Kazari *));
|
||||
shoppyou->holes[shoppyou->hole_cnt - 1] = current_kazari;
|
||||
changed = 1;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
shoppyou->modified_ts = time(NULL);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int kazari_rem_by_tanzaku(Shoppyou *shoppyou, uint64_t tanzaku_id) {
|
||||
if (tanzaku_id == HOLE_ID) {
|
||||
return 1;
|
||||
}
|
||||
Kazari *current_kazari = shoppyou->database;
|
||||
_Bool changed = 0;
|
||||
for (uint64_t i = 0; i < shoppyou->size; i++, current_kazari++) {
|
||||
if (current_kazari->tanzaku_id == tanzaku_id) {
|
||||
current_kazari->sasa_id = HOLE_ID;
|
||||
current_kazari->tanzaku_id = HOLE_ID;
|
||||
shoppyou->hole_cnt++;
|
||||
shoppyou->holes = reallocarray(shoppyou->holes, shoppyou->hole_cnt, sizeof(Kazari *));
|
||||
shoppyou->holes[shoppyou->hole_cnt - 1] = current_kazari;
|
||||
changed = 1;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
shoppyou->modified_ts = time(NULL);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
199
tanabata/lib/database.c
Normal 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
@ -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
@ -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
@ -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
@ -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;
|
||||
}
|
||||
90
tdbms/client/tdbms-client.c
Normal 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
@ -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
14
tdbms/tdbms.service
Normal 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
@ -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) "[7;36m"s"[0m"
|
||||
#define HIGHLIGHT(s) "[0;36m"s"[0m"
|
||||
#define SUCCESS(s) "[0;32m"s"[0m"
|
||||
#define ERROR(s) "[0;31m"s"[0m"
|
||||
|
||||
#define DT_FORMAT "%F %T"
|
||||
|
||||
static Tanabata tanabata;
|
||||
|
||||
// Print the list of all sasa
|
||||
void print_sasa_all() {
|
||||
printf(TABLE_HEADER(" Sasa ID\tFile path")"\n");
|
||||
for (uint64_t i = 0; i < tanabata.sasahyou.size; i++) {
|
||||
if (tanabata.sasahyou.database[i].id != HOLE_ID) {
|
||||
printf("%16lx\t%s\n", tanabata.sasahyou.database[i].id, tanabata.sasahyou.database[i].path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print the list of all tanzaku
|
||||
void print_tanzaku_all() {
|
||||
printf(TABLE_HEADER(" Tanzaku ID\tName")"\n");
|
||||
for (uint64_t i = 0; i < tanabata.sappyou.size; i++) {
|
||||
if (tanabata.sappyou.database[i].id != HOLE_ID) {
|
||||
printf("%16lx\t%s\n", tanabata.sappyou.database[i].id, tanabata.sappyou.database[i].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sasa view menu handler
|
||||
int menu_view_sasa(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(arg, ".") == 0) {
|
||||
print_sasa_all();
|
||||
return 0;
|
||||
}
|
||||
char *endptr;
|
||||
uint64_t sasa_id = strtoull(arg, &endptr, 16);
|
||||
if (*endptr == 0) {
|
||||
Sasa current_sasa = tanabata_sasa_get(&tanabata, sasa_id);
|
||||
if (current_sasa.id != HOLE_ID) {
|
||||
char datetime[20];
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) ¤t_sasa.created_ts));
|
||||
printf(HIGHLIGHT("Sasa ID")" %lx\n"
|
||||
HIGHLIGHT("File path")" %s\n"
|
||||
HIGHLIGHT("Added datetime")" %s\n\n",
|
||||
sasa_id, current_sasa.path, datetime);
|
||||
Tanzaku *related_tanzaku = tanabata_tanzaku_get_by_sasa(&tanabata, current_sasa.id);
|
||||
if (related_tanzaku != NULL) {
|
||||
printf(HIGHLIGHT("↓ Related tanzaku ↓")"\n"
|
||||
HIGHLIGHT(" Tanzaku ID\tName")"\n");
|
||||
for (Tanzaku *current_tanzaku = related_tanzaku;
|
||||
current_tanzaku->id != HOLE_ID; current_tanzaku++) {
|
||||
printf("%16lx\t%s\n", current_tanzaku->id, current_tanzaku->name);
|
||||
}
|
||||
printf(HIGHLIGHT("↑ Related tanzaku ↑")"\n");
|
||||
} else {
|
||||
printf(HIGHLIGHT("No related tanzaku")"\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("No sasa with this ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, ERROR("Invalid ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Tanzaku view menu handler
|
||||
int menu_view_tanzaku(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(arg, ".") == 0) {
|
||||
print_tanzaku_all();
|
||||
return 0;
|
||||
}
|
||||
char *endptr;
|
||||
uint64_t tanzaku_id = strtoull(arg, &endptr, 16);
|
||||
if (*endptr == 0) {
|
||||
Tanzaku current_tanzaku = tanabata_tanzaku_get(&tanabata, tanzaku_id);
|
||||
if (current_tanzaku.id != HOLE_ID) {
|
||||
char datetime[20];
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) ¤t_tanzaku.created_ts));
|
||||
printf(HIGHLIGHT("Tanzaku ID")" %lx\n"
|
||||
HIGHLIGHT("Name")" %s\n"
|
||||
HIGHLIGHT("Created datetime")" %s\n",
|
||||
tanzaku_id, current_tanzaku.name, datetime);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) ¤t_tanzaku.modified_ts));
|
||||
printf(HIGHLIGHT("Modified datetime")" %s\n\n", datetime);
|
||||
if (*current_tanzaku.description != 0) {
|
||||
printf(HIGHLIGHT("↓ Description ↓")"\n"
|
||||
"%s\n"
|
||||
HIGHLIGHT("↑ Description ↑")"\n\n", current_tanzaku.description);
|
||||
} else {
|
||||
printf(HIGHLIGHT("No description")"\n\n");
|
||||
}
|
||||
Sasa *related_sasa = tanabata_sasa_get_by_tanzaku(&tanabata, tanzaku_id);
|
||||
if (related_sasa != NULL) {
|
||||
printf(HIGHLIGHT("↓ Related sasa ↓")"\n"
|
||||
HIGHLIGHT(" Sasa ID\tFile path")"\n");
|
||||
for (Sasa *current_sasa = related_sasa;
|
||||
current_sasa->id != HOLE_ID; current_sasa++) {
|
||||
printf("%16lx\t%s\n", current_sasa->id, current_sasa->path);
|
||||
}
|
||||
printf(HIGHLIGHT("↑ Related sasa ↑")"\n");
|
||||
} else {
|
||||
printf(HIGHLIGHT("No related sasa")"\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("No tanzaku with this ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, ERROR("Invalid ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Sasa add menu handler
|
||||
int menu_add_sasa(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (tanabata.sasahyou.size == -1 && tanabata.sasahyou.hole_cnt == 0) {
|
||||
fprintf(stderr, ERROR("Failed to add file to database: sasahyou is full")"\n");
|
||||
return 1;
|
||||
}
|
||||
char *path = realpath(arg, NULL);
|
||||
if (path == NULL) {
|
||||
fprintf(stderr, ERROR("Invalid file path")"\n");
|
||||
free(path);
|
||||
return 1;
|
||||
}
|
||||
if (tanabata_sasa_add(&tanabata, path).id != HOLE_ID &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully added file to database")"\n");
|
||||
free(path);
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to add file to database")"\n");
|
||||
free(path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Tanzaku add menu handler
|
||||
int menu_add_tanzaku(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (tanabata.sappyou.size == -1 && tanabata.sappyou.hole_cnt == 0) {
|
||||
fprintf(stderr, ERROR("Failed to add tanzaku: sappyou is full")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (*arg != 0) {
|
||||
char description[4096];
|
||||
printf(HIGHLIGHT("Enter tanzaku description:")"\n");
|
||||
fgets(description, 4096, stdin);
|
||||
description[strlen(description) - 1] = 0;
|
||||
if (tanabata_tanzaku_add(&tanabata, arg, description).id != HOLE_ID &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully added tanzaku to database")"\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to add tanzaku to database")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Kazari add menu handler
|
||||
int menu_add_kazari(char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
if (tanabata.shoppyou.size == -1 && tanabata.shoppyou.hole_cnt == 0) {
|
||||
fprintf(stderr, ERROR("Failed to add kazari: shoppyou is full")"\n");
|
||||
return 1;
|
||||
}
|
||||
char *left = arg, *right = "\0", *endptr;
|
||||
for (size_t i = 0; i < strlen(arg); i++) {
|
||||
if (arg[i] == '-') {
|
||||
arg[i] = 0;
|
||||
right = arg + i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (*left == 0 || *right == 0) {
|
||||
fprintf(stderr, ERROR("Failed to add kazari: invalid argument")"\n");
|
||||
return 1;
|
||||
}
|
||||
uint64_t sasa_id = strtoull(left, &endptr, 16);
|
||||
if (*endptr != 0) {
|
||||
fprintf(stderr, ERROR("Failed to add kazari: invalid sasa ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
uint64_t tanzaku_id = strtoull(right, &endptr, 16);
|
||||
if (*endptr != 0) {
|
||||
fprintf(stderr, ERROR("Failed to add kazari: invalid tanzaku ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (tanabata_kazari_add(&tanabata, sasa_id, tanzaku_id) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully added kazari")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to add kazari")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Sasa remove menu handler
|
||||
int menu_rem_sasa(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
char *endptr;
|
||||
uint64_t sasa_id = strtoull(arg, &endptr, 16);
|
||||
if (*endptr == 0) {
|
||||
if (tanabata_sasa_rem(&tanabata, sasa_id) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully removed sasa")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to remove sasa")"\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, ERROR("Invalid ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Tanzaku remove menu handler
|
||||
int menu_rem_tanzaku(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
char *endptr;
|
||||
uint64_t tanzaku_id = strtoull(arg, &endptr, 16);
|
||||
if (*endptr == 0) {
|
||||
if (tanabata_tanzaku_rem(&tanabata, tanzaku_id) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully removed tanzaku")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to remove tanzaku")"\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, ERROR("Invalid ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Kazari remove menu handler
|
||||
int menu_rem_kazari(char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
char *left = arg, *right = "\0", *endptr;
|
||||
for (size_t i = 0; i < strlen(arg); i++) {
|
||||
if (arg[i] == '-') {
|
||||
arg[i] = 0;
|
||||
right = arg + i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (*left == 0 || *right == 0) {
|
||||
fprintf(stderr, ERROR("Failed to remove kazari: invalid argument")"\n");
|
||||
return 1;
|
||||
}
|
||||
uint64_t sasa_id = strtoull(left, &endptr, 16);
|
||||
if (*endptr != 0) {
|
||||
fprintf(stderr, ERROR("Failed to remove kazari: invalid sasa ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
uint64_t tanzaku_id = strtoull(right, &endptr, 16);
|
||||
if (*endptr != 0) {
|
||||
fprintf(stderr, ERROR("Failed to remove kazari: invalid tanzaku ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (tanabata_kazari_rem(&tanabata, sasa_id, tanzaku_id) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully removed kazari")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to remove kazari")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Sasa update menu handler
|
||||
int menu_upd_sasa(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
char *endptr;
|
||||
uint64_t sasa_id = strtoull(arg, &endptr, 16);
|
||||
if (*endptr == 0) {
|
||||
char *path = malloc(4096);
|
||||
printf(HIGHLIGHT("Enter the new file path (leave blank to keep current):")"\n");
|
||||
fgets(path, 4096, stdin);
|
||||
if (*path == '\n') {
|
||||
free(path);
|
||||
path = NULL;
|
||||
} else {
|
||||
path[strlen(path) - 1] = 0;
|
||||
}
|
||||
if (tanabata_sasa_upd(&tanabata, sasa_id, path) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully updated sasa")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to update sasa")"\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, ERROR("Invalid ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Tanzaku update menu handler
|
||||
int menu_upd_tanzaku(const char *arg) {
|
||||
if (arg == NULL) {
|
||||
return 1;
|
||||
}
|
||||
char *endptr;
|
||||
uint64_t tanzaku_id = strtoull(arg, &endptr, 16);
|
||||
if (*endptr == 0) {
|
||||
char *name = malloc(4096), *description = malloc(4096);
|
||||
printf(HIGHLIGHT("Enter the new name of tanzaku (leave blank to keep current):")"\n");
|
||||
fgets(name, 4096, stdin);
|
||||
if (*name == '\n') {
|
||||
free(name);
|
||||
name = NULL;
|
||||
} else {
|
||||
name[strlen(name) - 1] = 0;
|
||||
}
|
||||
printf(HIGHLIGHT("Enter the new description of tanzaku (leave blank to keep current):")"\n");
|
||||
fgets(description, 4096, stdin);
|
||||
if (*description == '\n') {
|
||||
free(description);
|
||||
description = NULL;
|
||||
} else {
|
||||
description[strlen(description) - 1] = 0;
|
||||
}
|
||||
if (tanabata_tanzaku_upd(&tanabata, tanzaku_id, name, description) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully updated tanzaku")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to update tanzaku")"\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, ERROR("Invalid ID")"\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc == 1) {
|
||||
fprintf(stderr, ERROR("No options provided")"\n");
|
||||
return 1;
|
||||
}
|
||||
char *tanabata_path;
|
||||
FILE *config = fopen(TFM_CONFIG_DIR"tfm-cli.conf", "r");
|
||||
if (config == NULL) {
|
||||
tanabata_path = NULL;
|
||||
struct stat st;
|
||||
if (stat(TFM_CONFIG_DIR, &st) == -1) {
|
||||
if (mkdir(TFM_CONFIG_DIR, 0755) != 0) {
|
||||
fprintf(stderr, ERROR("Failed to create %s directory. "
|
||||
"Try again with 'sudo' or check your permissions")"\n", TFM_CONFIG_DIR);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
config = fopen(TFM_CONFIG_DIR"tfm-cli.conf", "w");
|
||||
if (config == NULL) {
|
||||
fprintf(stderr, ERROR("Failed to create config file. "
|
||||
"Try again with 'sudo' or check your permissions")"\n");
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
fseek(config, 0L, SEEK_END);
|
||||
long fsize = ftell(config);
|
||||
rewind(config);
|
||||
if (fsize == 0) {
|
||||
tanabata_path = NULL;
|
||||
} else {
|
||||
tanabata_path = malloc(fsize + 1);
|
||||
if (fgets(tanabata_path, INT32_MAX, config) == NULL) {
|
||||
fprintf(stderr, ERROR("Failed to read config file")"\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const char *shortopts = "hI:O:isuef:t:c:w";
|
||||
char *abspath = NULL;
|
||||
int opt;
|
||||
_Bool opt_i = 0;
|
||||
_Bool opt_s = 0;
|
||||
_Bool opt_u = 0;
|
||||
_Bool opt_e = 0;
|
||||
_Bool opt_f = 0;
|
||||
_Bool opt_t = 0;
|
||||
_Bool opt_c = 0;
|
||||
_Bool opt_w = 0;
|
||||
char *opt_f_arg;
|
||||
char *opt_t_arg;
|
||||
char *opt_c_arg;
|
||||
while ((opt = getopt(argc, argv, shortopts)) != -1) {
|
||||
switch (opt) {
|
||||
case 'h':
|
||||
printf(
|
||||
HIGHLIGHT("(C) Masahiko AMANO aka H1K0, 2022—present")"\n"
|
||||
HIGHLIGHT("(https://github.com/H1K0/tanabata)")"\n\n"
|
||||
HIGHLIGHT("Usage:")"\n"
|
||||
"tfm <options>\n\n"
|
||||
HIGHLIGHT("Options:")"\n"
|
||||
HIGHLIGHT("-h")" Print this help and exit\n"
|
||||
HIGHLIGHT("-I <dir>")" Initialize new Tanabata database in directory <dir>\n"
|
||||
HIGHLIGHT("-O <dir>")" Open existing Tanabata database from directory <dir>\n"
|
||||
HIGHLIGHT("-i")" View database info\n"
|
||||
HIGHLIGHT("-s")" Set or add\n"
|
||||
HIGHLIGHT("-u")" Unset or remove\n"
|
||||
HIGHLIGHT("-e")" Edit or update\n"
|
||||
HIGHLIGHT("-f <sasa_id or path>")" File-sasa menu\n"
|
||||
HIGHLIGHT("-t <tanzaku_id or name>")" Tanzaku menu\n"
|
||||
HIGHLIGHT("-c <sasa_id>-<tanzaku_id>")" Kazari menu "
|
||||
"(can only be used with the '-s' or '-u' option)\n"
|
||||
HIGHLIGHT("-w")" Weed (defragment) database\n"
|
||||
);
|
||||
if (tanabata_path != NULL) {
|
||||
printf(HIGHLIGHT("Current database location: %s")"\n", tanabata_path);
|
||||
} else {
|
||||
printf(HIGHLIGHT("No database connected")"\n");
|
||||
}
|
||||
return 0;
|
||||
case 'I':
|
||||
abspath = realpath(optarg, abspath);
|
||||
if (abspath == NULL) {
|
||||
fprintf(stderr, ERROR("Invalid path")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (tanabata_init(&tanabata) == 0 &&
|
||||
tanabata_dump(&tanabata, abspath) == 0) {
|
||||
config = freopen(NULL, "w", config);
|
||||
if (config == NULL) {
|
||||
fprintf(stderr, ERROR("Failed to update config file. "
|
||||
"Try again with 'sudo' or check your permissions")"\n");
|
||||
return 1;
|
||||
}
|
||||
fputs(abspath, config);
|
||||
fclose(config);
|
||||
printf(SUCCESS("Successfully initialized Tanabata database")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to initialize Tanabata database")"\n");
|
||||
return 1;
|
||||
case 'O':
|
||||
abspath = realpath(optarg, abspath);
|
||||
if (abspath == NULL) {
|
||||
fprintf(stderr, ERROR("Invalid path")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (tanabata_open(&tanabata, abspath) == 0) {
|
||||
config = freopen(NULL, "w", config);
|
||||
if (config == NULL) {
|
||||
fprintf(stderr, ERROR("Failed to update config file. "
|
||||
"Try again with 'sudo' or check your permissions")"\n");
|
||||
return 1;
|
||||
}
|
||||
fputs(abspath, config);
|
||||
fclose(config);
|
||||
printf(SUCCESS("Successfully opened Tanabata database")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to open Tanabata database")"\n");
|
||||
return 1;
|
||||
case 'i':
|
||||
opt_i = 1;
|
||||
break;
|
||||
case 's':
|
||||
opt_s = 1;
|
||||
break;
|
||||
case 'u':
|
||||
opt_u = 1;
|
||||
break;
|
||||
case 'e':
|
||||
opt_e = 1;
|
||||
break;
|
||||
case 'f':
|
||||
opt_f = 1;
|
||||
opt_f_arg = optarg;
|
||||
break;
|
||||
case 't':
|
||||
opt_t = 1;
|
||||
opt_t_arg = optarg;
|
||||
break;
|
||||
case 'c':
|
||||
opt_c = 1;
|
||||
opt_c_arg = optarg;
|
||||
break;
|
||||
case 'w':
|
||||
opt_w = 1;
|
||||
break;
|
||||
case '?':
|
||||
return 1;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tanabata_path == NULL) {
|
||||
fprintf(stderr, ERROR("No connected database")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (tanabata_open(&tanabata, tanabata_path) != 0) {
|
||||
fprintf(stderr, ERROR("Failed to load database")"\n");
|
||||
return 1;
|
||||
}
|
||||
fclose(config);
|
||||
if (opt_i) {
|
||||
char datetime[20];
|
||||
printf(HIGHLIGHT("Current database location: %s")"\n\n"
|
||||
HIGHLIGHT("SASAHYOU")"\n", tanabata_path);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) &tanabata.sasahyou.created_ts));
|
||||
printf(" "HIGHLIGHT("Created")" %s\n", datetime);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) &tanabata.sasahyou.modified_ts));
|
||||
printf(" "HIGHLIGHT("Last modified")" %s\n"
|
||||
" "HIGHLIGHT("Number of sasa")" %lu\n"
|
||||
" "HIGHLIGHT("Number of holes")" %lu\n\n"
|
||||
HIGHLIGHT("SAPPYOU")"\n", datetime, tanabata.sasahyou.size, tanabata.sasahyou.hole_cnt);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) &tanabata.sappyou.created_ts));
|
||||
printf(" "HIGHLIGHT("Created")" %s\n", datetime);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) &tanabata.sappyou.modified_ts));
|
||||
printf(" "HIGHLIGHT("Last modified")" %s\n"
|
||||
" "HIGHLIGHT("Number of tanzaku")" %lu\n"
|
||||
" "HIGHLIGHT("Number of holes")" %lu\n\n"
|
||||
HIGHLIGHT("SHOPPYOU")"\n", datetime, tanabata.sappyou.size, tanabata.sappyou.hole_cnt);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) &tanabata.shoppyou.created_ts));
|
||||
printf(" "HIGHLIGHT("Created")" %s\n", datetime);
|
||||
strftime(datetime, 20, DT_FORMAT,
|
||||
localtime((const time_t *) &tanabata.shoppyou.modified_ts));
|
||||
printf(" "HIGHLIGHT("Last modified")" %s\n"
|
||||
" "HIGHLIGHT("Number of kazari")" %lu\n"
|
||||
" "HIGHLIGHT("Number of holes")" %lu\n",
|
||||
datetime, tanabata.shoppyou.size, tanabata.shoppyou.hole_cnt);
|
||||
return 0;
|
||||
}
|
||||
free(tanabata_path);
|
||||
if (opt_w) {
|
||||
if (tanabata_weed(&tanabata) == 0 &&
|
||||
tanabata_save(&tanabata) == 0) {
|
||||
printf(SUCCESS("Successfully weeded database")"\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, ERROR("Failed to weed database")"\n");
|
||||
return 1;
|
||||
}
|
||||
if (opt_s && opt_u) {
|
||||
opt_s = 0;
|
||||
opt_u = 0;
|
||||
}
|
||||
if (opt_s) {
|
||||
if (opt_f) {
|
||||
return menu_add_sasa(opt_f_arg);
|
||||
}
|
||||
if (opt_t) {
|
||||
return menu_add_tanzaku(opt_t_arg);
|
||||
}
|
||||
if (opt_c) {
|
||||
return menu_add_kazari(opt_c_arg);
|
||||
}
|
||||
} else if (opt_u) {
|
||||
if (opt_f) {
|
||||
return menu_rem_sasa(opt_f_arg);
|
||||
}
|
||||
if (opt_t) {
|
||||
return menu_rem_tanzaku(opt_t_arg);
|
||||
}
|
||||
if (opt_c) {
|
||||
return menu_rem_kazari(opt_c_arg);
|
||||
}
|
||||
} else if (opt_e) {
|
||||
if (opt_f) {
|
||||
return menu_upd_sasa(opt_f_arg);
|
||||
}
|
||||
if (opt_t) {
|
||||
return menu_upd_tanzaku(opt_t_arg);
|
||||
}
|
||||
} else {
|
||||
if (opt_f) {
|
||||
return menu_view_sasa(opt_f_arg);
|
||||
}
|
||||
if (opt_t) {
|
||||
return menu_view_tanzaku(opt_t_arg);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
59
web/install.sh
Normal 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
@ -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>
|
||||
2
web/public/browserconfig.xml
Normal 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
@ -0,0 +1,3 @@
|
||||
.btn-secondary {
|
||||
display: none;
|
||||
}
|
||||
7
web/public/css/bootstrap.min.css
vendored
Normal file
151
web/public/css/general.css
Normal 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
@ -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
@ -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
|
After Width: | Height: | Size: 1.1 KiB |
BIN
web/public/images/android-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
web/public/images/android-icon-192x192.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
web/public/images/android-icon-36x36.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
web/public/images/android-icon-48x48.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
web/public/images/android-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
web/public/images/android-icon-96x96.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
web/public/images/apple-icon-114x114.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
web/public/images/apple-icon-120x120.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
web/public/images/apple-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
web/public/images/apple-icon-152x152.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
web/public/images/apple-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
web/public/images/apple-icon-57x57.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
web/public/images/apple-icon-60x60.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
web/public/images/apple-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
web/public/images/apple-icon-76x76.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
web/public/images/apple-icon-precomposed.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
web/public/images/apple-icon.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
web/public/images/bg-1920x1440-bright.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
web/public/images/bg-1920x1440-dark.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
web/public/images/bg-bright.png
Normal file
|
After Width: | Height: | Size: 421 KiB |
BIN
web/public/images/bg-dark.png
Normal file
|
After Width: | Height: | Size: 712 KiB |
BIN
web/public/images/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
web/public/images/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
web/public/images/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
web/public/images/favicon-bg.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
web/public/images/favicon.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
web/public/images/ms-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
web/public/images/ms-icon-150x150.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
web/public/images/ms-icon-310x310.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
web/public/images/ms-icon-70x70.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
39
web/public/index.html
Normal 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
@ -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
8
web/public/js/jquery.cookie.js
Normal 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
@ -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);
|
||||
52
web/public/js/tdbms-database.js
Normal 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!");
|
||||
});
|
||||
7
web/public/js/tdbms-load.js
Normal 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);
|
||||
}
|
||||
});
|
||||
8
web/public/js/tdbms-management.js
Normal 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);
|
||||
});
|
||||
20
web/public/js/tdbms-newdb.js
Normal 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!");
|
||||
});
|
||||
9
web/public/js/tdbms-sappyou.js
Normal 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;
|
||||
});
|
||||
});
|
||||
9
web/public/js/tdbms-sasahyou.js
Normal 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;
|
||||
});
|
||||
});
|
||||
71
web/public/js/tdbms-settings.js
Normal 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!");
|
||||
});
|
||||
9
web/public/js/tdbms-shoppyou.js
Normal 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;
|
||||
});
|
||||
});
|
||||
16
web/public/js/tdbms-stats.js
Normal 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
@ -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);
|
||||
}
|
||||
36
web/public/js/tfm-database.js
Normal 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!");
|
||||
});
|
||||
37
web/public/js/tfm-files.js
Normal 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);
|
||||
});
|
||||
364
web/public/js/tfm-management.js
Normal 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();
|
||||
});
|
||||
99
web/public/js/tfm-settings.js
Normal 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
@ -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
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
47
web/public/tanabata.webmanifest
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
53
web/public/tdbms/index.html
Normal 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
@ -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>
|
||||
49
web/public/tdbms/sappyou.html
Normal 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>
|
||||
47
web/public/tdbms/sasahyou.html
Normal 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>
|
||||
111
web/public/tdbms/settings.html
Normal 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>
|
||||