diff --git a/server/internal/command/command.go b/server/internal/command/command.go index 61d53ac2..45dd0e91 100644 --- a/server/internal/command/command.go +++ b/server/internal/command/command.go @@ -5,6 +5,7 @@ import ( "path" "github.com/shroff/phylum/server/internal/command/fs" + "github.com/shroff/phylum/server/internal/command/public.go" "github.com/shroff/phylum/server/internal/command/schema" "github.com/shroff/phylum/server/internal/command/serve" storagecmd "github.com/shroff/phylum/server/internal/command/storage" @@ -64,6 +65,7 @@ func SetupCommand() { user.SetupCommand(), storagecmd.SetupCommand(), fs.SetupCommand(), + public.SetupCommand(), serve.SetupCommand(), }...) rootCmd.Execute() diff --git a/server/internal/command/fs/cmd.go b/server/internal/command/fs/cmd.go index 21254c13..39589113 100644 --- a/server/internal/command/fs/cmd.go +++ b/server/internal/command/fs/cmd.go @@ -17,7 +17,7 @@ func SetupCommand() *cobra.Command { flags.StringP("default-storage-dir", "S", "storage/default", "Default Storage Directory") viper.BindPFlag("default_storage_dir", flags.Lookup("default-storage-dir")) - flags.StringP("user", "u", "phylum", "Specify user for resource operations") + flags.StringP("user", "u", "phylum", "User") cmd.AddCommand( setupCatCommand(), diff --git a/server/internal/command/public.go/cmd.go b/server/internal/command/public.go/cmd.go new file mode 100644 index 00000000..6519f269 --- /dev/null +++ b/server/internal/command/public.go/cmd.go @@ -0,0 +1,24 @@ +package public + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func SetupCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "public", + Short: "Public Share", + } + flags := cmd.PersistentFlags() + flags.Bool("auto-migrate", true, "Automatically migrate database schema") + viper.BindPFlag("auto_migrate", flags.Lookup("auto-migrate")) + + flags.StringP("user", "u", "phylum", "User") + + cmd.AddCommand( + setupCreateCommand(), + ) + + return cmd +} diff --git a/server/internal/command/public.go/share.go b/server/internal/command/public.go/share.go new file mode 100644 index 00000000..3799c165 --- /dev/null +++ b/server/internal/command/public.go/share.go @@ -0,0 +1,31 @@ +package public + +import ( + "fmt" + + "github.com/shroff/phylum/server/internal/command/common" + "github.com/shroff/phylum/server/internal/core/fs" + "github.com/spf13/cobra" +) + +func setupCreateCommand() *cobra.Command { + cmd := cobra.Command{ + Use: "create ( | )", + Short: "Create Public Share", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + if err := createPublicShare(common.UserFileSystem(cmd), args[0], args[1]); err != nil { + fmt.Println("could not create public share '" + args[0] + "': " + err.Error()) + } + }, + } + return &cmd +} + +func createPublicShare(f fs.FileSystem, name, pathOrUUID string) error { + r, err := f.ResourceByPathOrUUID(pathOrUUID) + if err != nil { + return err + } + return r.CreatePublicShare(name, nil, nil, nil) +} diff --git a/server/internal/core/db/public.sql.go b/server/internal/core/db/public.sql.go index 0eb8cddc..71386e32 100644 --- a/server/internal/core/db/public.sql.go +++ b/server/internal/core/db/public.sql.go @@ -7,6 +7,9 @@ package db import ( "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" ) const accessPublicShare = `-- name: AccessPublicShare :exec @@ -18,6 +21,38 @@ func (q *Queries) AccessPublicShare(ctx context.Context, id string) error { return err } +const createPublicShare = `-- name: CreatePublicShare :exec +INSERT INTO public_shares(id, created_by, root, password_hash, expires, accesses_left) VALUES( + $1::text, + $2::text, + $3::uuid, + $4::text, + $5::timestamp, + $6::int +) +` + +type CreatePublicShareParams struct { + ID string + CreatedBy string + Root uuid.UUID + PasswordHash pgtype.Text + Expires pgtype.Timestamp + AccessesLeft pgtype.Int4 +} + +func (q *Queries) CreatePublicShare(ctx context.Context, arg CreatePublicShareParams) error { + _, err := q.db.Exec(ctx, createPublicShare, + arg.ID, + arg.CreatedBy, + arg.Root, + arg.PasswordHash, + arg.Expires, + arg.AccessesLeft, + ) + return err +} + const publicShare = `-- name: PublicShare :one SELECT id, created, deleted, created_by, root, password_hash, expires, accesses_left from public_shares WHERE id = $1 ` diff --git a/server/internal/core/fs/public.go b/server/internal/core/fs/public.go index fa434c28..314ae77e 100644 --- a/server/internal/core/fs/public.go +++ b/server/internal/core/fs/public.go @@ -1,5 +1,52 @@ package fs -func (f filesystem) CreatePublicShare(r Resource) { +import ( + "time" + "github.com/jackc/pgx/v5/pgtype" + "github.com/shroff/phylum/server/internal/core/db" + "github.com/shroff/phylum/server/internal/core/util/crypt" +) + +func (r Resource) CreatePublicShare(name string, password *string, maxAge *time.Time, maxDownloads *int) error { + if !r.hasPermission(PermissionShare | PermissionRead) { + return ErrInsufficientPermissions + } + + passwordHash := pgtype.Text{} + expires := pgtype.Timestamp{} + accessesLeft := pgtype.Int4{} + + if password != nil { + hash, err := crypt.GenerateArgon2EncodedHash(*password) + if err != nil { + return err + } + passwordHash = pgtype.Text{ + String: hash, + Valid: true, + } + } + if maxAge != nil { + expires = pgtype.Timestamp{ + Time: *maxAge, + Valid: true, + } + } + + if maxDownloads != nil { + accessesLeft = pgtype.Int4{ + Int32: int32(*maxDownloads), + Valid: true, + } + } + + return r.f.db.CreatePublicShare(r.f.ctx, db.CreatePublicShareParams{ + ID: name, + CreatedBy: r.f.username, + Root: r.ID, + PasswordHash: passwordHash, + Expires: expires, + AccessesLeft: accessesLeft, + }) } diff --git a/server/sql/queries/public.sql b/server/sql/queries/public.sql index f749f146..594510d8 100644 --- a/server/sql/queries/public.sql +++ b/server/sql/queries/public.sql @@ -3,3 +3,13 @@ SELECT * from public_shares WHERE id = $1; -- name: AccessPublicShare :exec UPDATE public_shares SET accesses_left = accesses_left - 1 WHERE id = $1; + +-- name: CreatePublicShare :exec +INSERT INTO public_shares(id, created_by, root, password_hash, expires, accesses_left) VALUES( + @id::text, + @created_by::text, + @root::uuid, + sqlc.narg('password_hash')::text, + sqlc.narg('expires')::timestamp, + sqlc.narg('accesses_left')::int +); \ No newline at end of file