diff --git a/internal/command/command.go b/internal/command/command.go index 416cea80..eda828fa 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -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) -} diff --git a/internal/command/library.go b/internal/command/library.go index 2a451cb0..0e0c89fd 100644 --- a/internal/command/library.go +++ b/internal/command/library.go @@ -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) } }, diff --git a/internal/library/library.go b/internal/library/library.go index f18a187a..988d7c53 100644 --- a/internal/library/library.go +++ b/internal/library/library.go @@ -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 } diff --git a/internal/library/library_manager.go b/internal/library/library_manager.go index 8742cc2c..841f842c 100644 --- a/internal/library/library_manager.go +++ b/internal/library/library_manager.go @@ -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) } } diff --git a/internal/sql/db_manager.go b/internal/sql/db_manager.go new file mode 100644 index 00000000..e7194403 --- /dev/null +++ b/internal/sql/db_manager.go @@ -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) +} diff --git a/internal/sql/libraries.sql.go b/internal/sql/libraries.sql.go new file mode 100644 index 00000000..e8fbc78e --- /dev/null +++ b/internal/sql/libraries.sql.go @@ -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 +} diff --git a/internal/sql/models.go b/internal/sql/models.go index 76c211d3..3888ed01 100644 --- a/internal/sql/models.go +++ b/internal/sql/models.go @@ -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 diff --git a/internal/sql/resources.sql.go b/internal/sql/resources.sql.go index 0b87e693..6b7c3611 100644 --- a/internal/sql/resources.sql.go +++ b/internal/sql/resources.sql.go @@ -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 diff --git a/internal/sql/tracer.go b/internal/sql/tracer.go new file mode 100644 index 00000000..c503c37f --- /dev/null +++ b/internal/sql/tracer.go @@ -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) +} diff --git a/internal/user/user_manager.go b/internal/user/user_manager.go index b5531383..d34823a7 100644 --- a/internal/user/user_manager.go +++ b/internal/user/user_manager.go @@ -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) } diff --git a/internal/webdav/handler.go b/internal/webdav/handler.go index 0b29ce85..812d53fa 100644 --- a/internal/webdav/handler.go +++ b/internal/webdav/handler.go @@ -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) diff --git a/internal/webdav_xnet/handler.go b/internal/webdav_xnet/handler.go index 7d6aa1df..580f675e 100644 --- a/internal/webdav_xnet/handler.go +++ b/internal/webdav_xnet/handler.go @@ -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(), } diff --git a/sql/migrations/002_users.up.sql b/sql/migrations/002_users.up.sql index 1f098ed2..e17cbef1 100644 --- a/sql/migrations/002_users.up.sql +++ b/sql/migrations/002_users.up.sql @@ -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; \ No newline at end of file +CREATE UNIQUE INDEX unique_username ON users(username); \ No newline at end of file diff --git a/sql/migrations/003_libraries.down.sql b/sql/migrations/003_libraries.down.sql new file mode 100644 index 00000000..436fbc33 --- /dev/null +++ b/sql/migrations/003_libraries.down.sql @@ -0,0 +1 @@ +DROP TABLE libraries; \ No newline at end of file diff --git a/sql/migrations/003_libraries.up.sql b/sql/migrations/003_libraries.up.sql new file mode 100644 index 00000000..66b637c4 --- /dev/null +++ b/sql/migrations/003_libraries.up.sql @@ -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 +); diff --git a/sql/queries/libraries.sql b/sql/queries/libraries.sql new file mode 100644 index 00000000..9c8d05e1 --- /dev/null +++ b/sql/queries/libraries.sql @@ -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 +); \ No newline at end of file diff --git a/sql/queries/resources.sql b/sql/queries/resources.sql index 9957e0cd..fb4ae45b 100644 --- a/sql/queries/resources.sql +++ b/sql/queries/resources.sql @@ -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