Libraries

This commit is contained in:
Abhishek Shroff
2024-03-13 21:34:34 +05:30
parent 970a956f96
commit e29fd4535c
17 changed files with 236 additions and 109 deletions

View File

@@ -5,8 +5,8 @@ import (
"os"
"path"
"github.com/jackc/pgx/v5"
"github.com/shroff/phylum/server/internal/library"
"github.com/shroff/phylum/server/internal/sql"
"github.com/shroff/phylum/server/internal/user"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -35,7 +35,7 @@ func SetupCommand() {
flags.String("database-url", "postgres://phylum:phylum@localhost:5432/phylum", "Database URL or DSN")
viper.BindPFlag("database_url", flags.Lookup("database-url"))
var conn *pgx.Conn
var db *sql.DbHandler
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
workDir := viper.GetString("working_dir")
@@ -51,27 +51,20 @@ func SetupCommand() {
}
dsn := viper.GetString("database_url")
config, err := pgx.ParseConfig(dsn)
if viper.GetBool("trace_sql") {
config.Tracer = phylumTracer{}
}
var err error
db, err = sql.NewDb(dsn, debug && viper.GetBool("trace_sql"))
if err != nil {
logrus.Fatal("Unable to parse db connection String: " + err.Error())
logrus.Fatal(err)
}
conn, err = pgx.ConnectConfig(context.Background(), config)
if err != nil {
logrus.Fatal("Unable to connect to database: " + err.Error())
}
libraryManager = library.NewManager(conn)
userManager = user.NewManager(conn)
libraryManager = library.NewManager(db)
userManager = user.NewManager(db)
}
defer func() {
if conn != nil {
if db != nil {
logrus.Info("Closing datbase connection")
conn.Close(context.Background())
db.Close(context.Background())
}
}()
@@ -81,15 +74,3 @@ func SetupCommand() {
}...)
rootCmd.Execute()
}
type phylumTracer struct {
}
func (p phylumTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {
logrus.Trace(data)
return ctx
}
func (p phylumTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {
logrus.Trace(data)
}

View File

@@ -22,24 +22,36 @@ func setupLibraryCommand() *cobra.Command {
func setupLibraryCreateCommand() *cobra.Command {
return &cobra.Command{
Use: "create name",
Short: "Create Root Folder",
Args: cobra.ExactArgs(1),
Use: "create owner display-name",
Short: "Create Library",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
if err := libraryManager.Create(context.Background(), uuid.New(), args[0]); err != nil {
id := uuid.New()
username := args[0]
user, err := userManager.FindUser(context.Background(), username)
if err != nil {
logrus.Fatal("User not found: " + username)
}
name := args[1]
if err := libraryManager.Create(context.Background(), id, user.ID, name); err != nil {
logrus.Fatal(err)
}
logrus.Info("Created " + id.String())
},
}
}
func setupLibraryDeleteCommand() *cobra.Command {
return &cobra.Command{
Use: "delete name",
Short: "Delete Root Folder",
Use: "delete id",
Short: "Delete Library",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if err := libraryManager.Delete(context.Background(), args[0]); err != nil {
id, err := uuid.Parse(args[0])
if err != nil {
logrus.Fatal("Not an ID: " + args[0])
}
if err := libraryManager.Delete(context.Background(), id); err != nil {
logrus.Fatal(err)
}
},

View File

@@ -17,9 +17,9 @@ import (
)
type Library struct {
queries sql.Queries
root uuid.UUID
cs storage.Storage
db *sql.DbHandler
root uuid.UUID
cs storage.Storage
}
func (l Library) Open(id uuid.UUID, flag int) (io.ReadWriteCloser, error) {
@@ -27,13 +27,13 @@ func (l Library) Open(id uuid.UUID, flag int) (io.ReadWriteCloser, error) {
}
func (l Library) ReadDir(ctx context.Context, id uuid.UUID, includeRoot bool, recursive bool) ([]sql.ReadDirRow, error) {
return l.queries.ReadDir(ctx, sql.ReadDirParams{ID: id, IncludeRoot: includeRoot, Recursive: recursive})
return l.db.Queries().ReadDir(ctx, sql.ReadDirParams{ID: id, IncludeRoot: includeRoot, Recursive: recursive})
}
func (l Library) DeleteRecursive(ctx context.Context, id uuid.UUID, hardDelete bool) error {
query := l.queries.DeleteRecursive
query := l.db.Queries().DeleteRecursive
if hardDelete {
query = l.queries.HardDeleteRecursive
query = l.db.Queries().HardDeleteRecursive
}
deleted, err := query(ctx, id)
if err == nil && hardDelete {
@@ -47,18 +47,18 @@ func (l Library) DeleteRecursive(ctx context.Context, id uuid.UUID, hardDelete b
}
func (l Library) CreateResource(ctx context.Context, id uuid.UUID, parent uuid.UUID, name string, dir bool) error {
return l.queries.CreateResource(ctx, sql.CreateResourceParams{ID: id, Parent: &parent, Name: name, Dir: dir})
return l.db.Queries().CreateResource(ctx, sql.CreateResourceParams{ID: id, Parent: &parent, Name: name, Dir: dir})
}
func (l Library) Move(ctx context.Context, id uuid.UUID, parent uuid.UUID, name string) error {
return l.queries.Rename(ctx, sql.RenameParams{ID: id, Parent: parent, Name: name})
return l.db.Queries().Rename(ctx, sql.RenameParams{ID: id, Parent: parent, Name: name})
}
func (l Library) UpdateContents(ctx context.Context, id uuid.UUID) (io.WriteCloser, error) {
return l.cs.Open(id, os.O_CREATE|os.O_RDWR|os.O_TRUNC, func(h hash.Hash, len int, err error) {
if err == nil {
etag := base64.StdEncoding.EncodeToString(h.Sum(nil))
err = l.queries.UpdateResourceContents(ctx, sql.UpdateResourceContentsParams{
err = l.db.Queries().UpdateResourceContents(ctx, sql.UpdateResourceContentsParams{
ID: id,
Size: pgtype.Int4{Int32: int32(len), Valid: true},
Etag: pgtype.Text{String: string(etag), Valid: true},
@@ -79,7 +79,7 @@ func (l Library) ResourceByPath(ctx context.Context, path string) (sql.ResourceB
segments = []string{}
}
res, err := l.queries.ResourceByPath(ctx, sql.ResourceByPathParams{Search: segments, Root: l.root})
res, err := l.db.Queries().ResourceByPath(ctx, sql.ResourceByPathParams{Search: segments, Root: l.root})
if err != nil {
return sql.ResourceByPathRow{}, fs.ErrNotExist
}

View File

@@ -5,47 +5,49 @@ import (
"fmt"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/shroff/phylum/server/internal/sql"
"github.com/shroff/phylum/server/internal/storage"
)
type Manager struct {
queries sql.Queries
cs storage.Storage
db *sql.DbHandler
cs storage.Storage
}
func NewManager(conn *pgx.Conn) Manager {
queries := *sql.New(conn)
func NewManager(db *sql.DbHandler) Manager {
cs, err := storage.NewLocalStorage("srv")
if err != nil {
panic(err)
}
return Manager{queries: queries, cs: cs}
return Manager{db: db, cs: cs}
}
func (b Manager) Get(ctx context.Context, name string) (*Library, error) {
func (b Manager) Get(ctx context.Context, id uuid.UUID) (*Library, error) {
// TODO: Permissions checks
lib, err := b.queries.LibraryByName(ctx, name)
lib, err := b.db.Queries().LibraryById(ctx, id)
if err != nil {
return nil, err
}
return &Library{queries: b.queries, root: lib.ID, cs: b.cs}, nil
return &Library{db: b.db, root: lib.ID, cs: b.cs}, nil
}
func (b Manager) Create(ctx context.Context, id uuid.UUID, name string) error {
if _, err := b.queries.LibraryByName(ctx, name); err == nil {
return fmt.Errorf("root directory already exists: %s", name)
} else {
return b.queries.CreateResource(ctx, sql.CreateResourceParams{ID: id, Parent: nil, Name: name, Dir: true})
}
func (b Manager) Create(ctx context.Context, id uuid.UUID, owner int32, displayName string) error {
return b.db.RunInTx(ctx, func(q *sql.Queries) error {
if err := q.CreateLibrary(ctx, sql.CreateLibraryParams{ID: id, Owner: owner, DisplayName: displayName}); err != nil {
return err
}
if err := q.CreateResource(ctx, sql.CreateResourceParams{ID: id, Dir: true}); err != nil {
return err
}
return nil
})
}
func (b Manager) Delete(ctx context.Context, name string) error {
if lib, err := b.Get(ctx, name); err == nil {
func (b Manager) Delete(ctx context.Context, id uuid.UUID) error {
if lib, err := b.Get(ctx, id); err == nil {
return lib.DeleteRecursive(ctx, lib.root, true)
} else {
return fmt.Errorf("root directory does not exist: %s", name)
return fmt.Errorf("library does not exist: %s", id)
}
}

View File

@@ -0,0 +1,53 @@
package sql
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/sirupsen/logrus"
)
type DbHandler struct {
conn *pgx.Conn
queries *Queries
}
func NewDb(dsn string, trace bool) (*DbHandler, error) {
config, err := pgx.ParseConfig(dsn)
if err != nil {
return nil, errors.New("Unable to parse DSN: " + err.Error())
}
if trace {
config.Tracer = tracer{}
}
conn, err := pgx.ConnectConfig(context.Background(), config)
if err != nil {
return nil, errors.New("Unable to connect to database: " + err.Error())
}
logrus.Info("Connected to " + config.Database + " at " + config.Host + ":" + fmt.Sprint(config.Port))
return &DbHandler{
conn: conn,
queries: &Queries{db: conn},
}, nil
}
func (d DbHandler) Queries() *Queries {
return d.queries
}
func (d DbHandler) RunInTx(ctx context.Context, fn func(*Queries) error) error {
return pgx.BeginFunc(ctx, d.conn, func(tx pgx.Tx) error {
q := d.queries.WithTx(tx)
return fn(q)
})
}
func (d DbHandler) Close(ctx context.Context) {
d.conn.Close(ctx)
}

View File

@@ -0,0 +1,47 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// source: libraries.sql
package sql
import (
"context"
"github.com/google/uuid"
)
const createLibrary = `-- name: CreateLibrary :exec
INSERT INTO libraries(
id, owner, display_name
) VALUES(
$1, $2, $3
)
`
type CreateLibraryParams struct {
ID uuid.UUID
Owner int32
DisplayName string
}
func (q *Queries) CreateLibrary(ctx context.Context, arg CreateLibraryParams) error {
_, err := q.db.Exec(ctx, createLibrary, arg.ID, arg.Owner, arg.DisplayName)
return err
}
const libraryById = `-- name: LibraryById :one
SELECT id, owner, display_name, deleted from libraries where id = $1
`
func (q *Queries) LibraryById(ctx context.Context, id uuid.UUID) (Library, error) {
row := q.db.QueryRow(ctx, libraryById, id)
var i Library
err := row.Scan(
&i.ID,
&i.Owner,
&i.DisplayName,
&i.Deleted,
)
return i, err
}

View File

@@ -9,6 +9,13 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
type Library struct {
ID uuid.UUID
Owner int32
DisplayName string
Deleted pgtype.Timestamp
}
type Resource struct {
ID uuid.UUID
Parent *uuid.UUID
@@ -22,7 +29,7 @@ type Resource struct {
}
type User struct {
ID pgtype.Int4
ID int32
DisplayName string
Username string
PasswordHash string

View File

@@ -126,27 +126,6 @@ func (q *Queries) HardDeleteRecursive(ctx context.Context, id uuid.UUID) ([]Reso
return items, nil
}
const libraryByName = `-- name: LibraryByName :one
SELECT id, parent, name, dir, created, modified, deleted, size, etag from resources WHERE deleted IS NULL AND parent IS NULL AND name = $1
`
func (q *Queries) LibraryByName(ctx context.Context, name string) (Resource, error) {
row := q.db.QueryRow(ctx, libraryByName, name)
var i Resource
err := row.Scan(
&i.ID,
&i.Parent,
&i.Name,
&i.Dir,
&i.Created,
&i.Modified,
&i.Deleted,
&i.Size,
&i.Etag,
)
return i, err
}
const readDir = `-- name: ReadDir :many
WITH RECURSIVE nodes(id, parent, name, dir, created, modified, size, etag, depth, path) AS (
SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, 0, ''::text

20
internal/sql/tracer.go Normal file
View File

@@ -0,0 +1,20 @@
package sql
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/sirupsen/logrus"
)
type tracer struct {
}
func (t tracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {
logrus.Trace(data)
return ctx
}
func (c tracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {
logrus.Trace(data)
}

View File

@@ -3,21 +3,19 @@ package user
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/shroff/phylum/server/internal/sql"
)
type Manager struct {
queries sql.Queries
db *sql.DbHandler
}
func NewManager(conn *pgx.Conn) Manager {
queries := *sql.New(conn)
return Manager{queries: queries}
func NewManager(db *sql.DbHandler) Manager {
return Manager{db: db}
}
func (m Manager) CreateUser(ctx context.Context, username, displayName, passwordHash string) error {
_, err := m.queries.CreateUser(ctx, sql.CreateUserParams{Username: username, DisplayName: displayName, PasswordHash: passwordHash})
_, err := m.db.Queries().CreateUser(ctx, sql.CreateUserParams{Username: username, DisplayName: displayName, PasswordHash: passwordHash})
if err != nil {
return err
}
@@ -25,5 +23,5 @@ func (m Manager) CreateUser(ctx context.Context, username, displayName, password
}
func (m Manager) FindUser(ctx context.Context, username string) (sql.User, error) {
return m.queries.UserByUsername(ctx, username)
return m.db.Queries().UserByUsername(ctx, username)
}

View File

@@ -6,6 +6,7 @@ import (
webdav "github.com/emersion/go-webdav"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/shroff/phylum/server/internal/library"
"github.com/sirupsen/logrus"
)
@@ -25,17 +26,24 @@ func NewHandler(libraryManager library.Manager, prefix string) *handler {
func (h *handler) HandleRequest(c *gin.Context) {
path := c.Params.ByName("path")
libraryName := strings.TrimLeft(path, "/")
if libraryName == "" {
idStr := strings.TrimLeft(path, "/")
if idStr == "" {
// No path specified
c.Writer.WriteHeader(404)
return
}
index := strings.Index(libraryName, "/")
index := strings.Index(idStr, "/")
if index != -1 {
libraryName = libraryName[0:index]
idStr = idStr[0:index]
}
library, err := h.libraryManager.Get(context.Background(), libraryName)
id, err := uuid.Parse(idStr)
if err != nil {
c.Writer.WriteHeader(404)
return
}
library, err := h.libraryManager.Get(context.Background(), id)
if err != nil {
c.Writer.WriteHeader(404)
return
@@ -44,7 +52,7 @@ func (h *handler) HandleRequest(c *gin.Context) {
webdavHandler := webdav.Handler{
FileSystem: adapter{
lib: library,
prefix: h.prefix + "/" + libraryName,
prefix: h.prefix + "/" + idStr,
},
}
webdavHandler.ServeHTTP(c.Writer, c.Request)

View File

@@ -6,6 +6,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/shroff/phylum/server/internal/cryptutil"
"github.com/shroff/phylum/server/internal/library"
"github.com/shroff/phylum/server/internal/user"
@@ -66,24 +67,30 @@ func SetupHandler(r *gin.RouterGroup, libraryManager library.Manager, userManage
func (h *handler) HandleRequest(c *gin.Context) {
path := c.Params.ByName("path")
libraryName := strings.TrimLeft(path, "/")
if libraryName == "" {
idStr := strings.TrimLeft(path, "/")
if idStr == "" {
// No path specified
c.Writer.WriteHeader(404)
return
}
index := strings.Index(libraryName, "/")
index := strings.Index(idStr, "/")
if index != -1 {
libraryName = libraryName[0:index]
idStr = idStr[0:index]
}
library, err := h.libraryManager.Get(context.Background(), libraryName)
id, err := uuid.Parse(idStr)
if err != nil {
c.Writer.WriteHeader(404)
return
}
library, err := h.libraryManager.Get(context.Background(), id)
if err != nil {
c.Writer.WriteHeader(404)
return
}
webdavHandler := webdav.Handler{
Prefix: h.prefix + "/" + libraryName,
Prefix: h.prefix + "/" + idStr,
FileSystem: adapter{lib: library},
LockSystem: webdav.NewMemLS(),
}

View File

@@ -1,9 +1,9 @@
CREATE TABLE users(
id SERIAL,
id SERIAL PRIMARY KEY,
display_name TEXT NOT NULL,
username TEXT NOT NULL,
password_hash TEXT NOT NULL,
deleted TIMESTAMP
);
CREATE UNIQUE INDEX unique_username ON users(username) WHERE deleted IS NULL;
CREATE UNIQUE INDEX unique_username ON users(username);

View File

@@ -0,0 +1 @@
DROP TABLE libraries;

View File

@@ -0,0 +1,6 @@
CREATE TABLE libraries(
id uuid PRIMARY KEY,
owner SERIAL NOT NULL REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE,
display_name TEXT NOT NULL,
deleted TIMESTAMP
);

View File

@@ -0,0 +1,9 @@
-- name: LibraryById :one
SELECT * from libraries where id = $1;
-- name: CreateLibrary :exec
INSERT INTO libraries(
id, owner, display_name
) VALUES(
$1, $2, $3
);

View File

@@ -1,9 +1,6 @@
-- name: ResourceById :one
SELECT * from resources WHERE id = $1;
-- name: LibraryByName :one
SELECT * from resources WHERE deleted IS NULL AND parent IS NULL AND name = $1;
-- name: CreateResource :exec
INSERT INTO resources(
id, parent, name, dir, created, modified