mirror of
https://github.com/get-drexa/drive.git
synced 2026-02-02 16:01:17 +00:00
feat: impl files endpoints
This commit is contained in:
34
apps/backend/internal/virtualfs/file_content.go
Normal file
34
apps/backend/internal/virtualfs/file_content.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package virtualfs
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/get-drexa/drexa/internal/blob"
|
||||
)
|
||||
|
||||
type FileContent struct {
|
||||
Size int64
|
||||
Reader io.ReadCloser
|
||||
BlobKey blob.Key
|
||||
URL string
|
||||
}
|
||||
|
||||
func EmptyFileContent() FileContent {
|
||||
return FileContent{}
|
||||
}
|
||||
|
||||
func FileContentFromReader(reader io.Reader) FileContent {
|
||||
return FileContent{Reader: io.NopCloser(reader)}
|
||||
}
|
||||
|
||||
func FileContentFromReaderWithSize(reader io.Reader, size int64) FileContent {
|
||||
return FileContent{Reader: io.NopCloser(reader), Size: size}
|
||||
}
|
||||
|
||||
func FileContentFromBlobKey(blobKey blob.Key) FileContent {
|
||||
return FileContent{BlobKey: blobKey}
|
||||
}
|
||||
|
||||
func FileContentFromURL(url string) FileContent {
|
||||
return FileContent{URL: url}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/get-drexa/drexa/internal/blob"
|
||||
"github.com/google/uuid"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type FlatKeyResolver struct{}
|
||||
@@ -19,7 +20,7 @@ func (r *FlatKeyResolver) ShouldPersistKey() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *FlatKeyResolver) Resolve(ctx context.Context, node *Node) (blob.Key, error) {
|
||||
func (r *FlatKeyResolver) Resolve(ctx context.Context, db bun.IDB, node *Node) (blob.Key, error) {
|
||||
if node.BlobKey == "" {
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
|
||||
@@ -22,8 +22,8 @@ func (r *HierarchicalKeyResolver) ShouldPersistKey() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *HierarchicalKeyResolver) Resolve(ctx context.Context, node *Node) (blob.Key, error) {
|
||||
path, err := buildNodeAbsolutePath(ctx, r.db, node.ID)
|
||||
func (r *HierarchicalKeyResolver) Resolve(ctx context.Context, db bun.IDB, node *Node) (blob.Key, error) {
|
||||
path, err := buildNodeAbsolutePath(ctx, db, node)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func (r *HierarchicalKeyResolver) Resolve(ctx context.Context, node *Node) (blob
|
||||
}
|
||||
|
||||
func (r *HierarchicalKeyResolver) ResolveDeletionKeys(ctx context.Context, node *Node, allKeys []blob.Key) (*DeletionPlan, error) {
|
||||
path, err := buildNodeAbsolutePath(ctx, r.db, node.ID)
|
||||
path, err := buildNodeAbsolutePath(ctx, r.db, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/get-drexa/drexa/internal/blob"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type BlobKeyResolver interface {
|
||||
@@ -11,7 +12,7 @@ type BlobKeyResolver interface {
|
||||
// Flat keys (e.g. UUIDs) return true - key is generated once and stored.
|
||||
// Hierarchical keys return false - key is derived from path each time.
|
||||
ShouldPersistKey() bool
|
||||
Resolve(ctx context.Context, node *Node) (blob.Key, error)
|
||||
Resolve(ctx context.Context, db bun.IDB, node *Node) (blob.Key, error)
|
||||
ResolveDeletionKeys(ctx context.Context, node *Node, allKeys []blob.Key) (*DeletionPlan, error)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,9 @@ type Node struct {
|
||||
Size int64 `bun:"size"`
|
||||
MimeType string `bun:"mime_type,nullzero"`
|
||||
|
||||
CreatedAt time.Time `bun:"created_at,notnull,nullzero"`
|
||||
UpdatedAt time.Time `bun:"updated_at,notnull,nullzero"`
|
||||
DeletedAt time.Time `bun:"deleted_at,nullzero"`
|
||||
CreatedAt time.Time `bun:"created_at,notnull,nullzero"`
|
||||
UpdatedAt time.Time `bun:"updated_at,notnull,nullzero"`
|
||||
DeletedAt *time.Time `bun:"deleted_at,nullzero"`
|
||||
}
|
||||
|
||||
func newNodeID() (uuid.UUID, error) {
|
||||
@@ -49,5 +49,5 @@ func newNodeID() (uuid.UUID, error) {
|
||||
// IsAccessible returns true if the node can be accessed.
|
||||
// If the node is not ready or if it is soft deleted, it cannot be accessed.
|
||||
func (n *Node) IsAccessible() bool {
|
||||
return n.DeletedAt.IsZero() && n.Status == NodeStatusReady
|
||||
return n.DeletedAt == nil && n.Status == NodeStatusReady
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
@@ -19,7 +18,6 @@ const absolutePathQuery = `WITH RECURSIVE path AS (
|
||||
SELECT n.id, n.parent_id, n.name, p.depth + 1
|
||||
FROM vfs_nodes n
|
||||
JOIN path p ON n.id = p.parent_id
|
||||
WHERE n.deleted_at IS NULL
|
||||
)
|
||||
SELECT name FROM path
|
||||
WHERE EXISTS (SELECT 1 FROM path WHERE parent_id IS NULL)
|
||||
@@ -29,9 +27,9 @@ func JoinPath(parts ...string) string {
|
||||
return strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
func buildNodeAbsolutePath(ctx context.Context, db bun.IDB, nodeID uuid.UUID) (string, error) {
|
||||
func buildNodeAbsolutePath(ctx context.Context, db bun.IDB, node *Node) (string, error) {
|
||||
var path []string
|
||||
err := db.NewRaw(absolutePathQuery, nodeID).Scan(ctx, &path)
|
||||
err := db.NewRaw(absolutePathQuery, node.ID).Scan(ctx, &path)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", ErrNodeNotFound
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/get-drexa/drexa/internal/blob"
|
||||
@@ -36,22 +37,9 @@ type CreateFileOptions struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type FileContent struct {
|
||||
reader io.Reader
|
||||
blobKey blob.Key
|
||||
}
|
||||
|
||||
const RootDirectoryName = "root"
|
||||
|
||||
func FileContentFromReader(reader io.Reader) FileContent {
|
||||
return FileContent{reader: reader}
|
||||
}
|
||||
|
||||
func FileContentFromBlobKey(blobKey blob.Key) FileContent {
|
||||
return FileContent{blobKey: blobKey}
|
||||
}
|
||||
|
||||
func NewVirtualFS(blobStore blob.Store, keyResolver BlobKeyResolver) (*VirtualFS, error) {
|
||||
func New(blobStore blob.Store, keyResolver BlobKeyResolver) (*VirtualFS, error) {
|
||||
sqid, err := sqids.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -86,7 +74,6 @@ func (vfs *VirtualFS) FindNodeByPublicID(ctx context.Context, db bun.IDB, accoun
|
||||
Where("account_id = ?", accountID).
|
||||
Where("public_id = ?", publicID).
|
||||
Where("status = ?", NodeStatusReady).
|
||||
Where("deleted_at IS NULL").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -141,7 +128,7 @@ func (vfs *VirtualFS) CreateFile(ctx context.Context, db bun.IDB, accountID uuid
|
||||
}
|
||||
|
||||
if vfs.keyResolver.ShouldPersistKey() {
|
||||
node.BlobKey, err = vfs.keyResolver.Resolve(ctx, &node)
|
||||
node.BlobKey, err = vfs.keyResolver.Resolve(ctx, db, &node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -159,31 +146,31 @@ func (vfs *VirtualFS) CreateFile(ctx context.Context, db bun.IDB, accountID uuid
|
||||
}
|
||||
|
||||
func (vfs *VirtualFS) WriteFile(ctx context.Context, db bun.IDB, node *Node, content FileContent) error {
|
||||
if content.reader == nil && content.blobKey.IsNil() {
|
||||
if content.Reader == nil && content.BlobKey.IsNil() {
|
||||
return blob.ErrInvalidFileContent
|
||||
}
|
||||
|
||||
if !node.DeletedAt.IsZero() {
|
||||
if node.DeletedAt != nil {
|
||||
return ErrNodeNotFound
|
||||
}
|
||||
|
||||
setCols := make([]string, 0, 4)
|
||||
|
||||
if content.reader != nil {
|
||||
key, err := vfs.keyResolver.Resolve(ctx, node)
|
||||
if content.Reader != nil {
|
||||
key, err := vfs.keyResolver.Resolve(ctx, db, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := make([]byte, 3072)
|
||||
n, err := io.ReadFull(content.reader, buf)
|
||||
n, err := io.ReadFull(content.Reader, buf)
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
return err
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
mt := mimetype.Detect(buf)
|
||||
cr := ioext.NewCountingReader(io.MultiReader(bytes.NewReader(buf), content.reader))
|
||||
cr := ioext.NewCountingReader(io.MultiReader(bytes.NewReader(buf), content.Reader))
|
||||
|
||||
err = vfs.blobStore.Put(ctx, key, cr)
|
||||
if err != nil {
|
||||
@@ -201,9 +188,9 @@ func (vfs *VirtualFS) WriteFile(ctx context.Context, db bun.IDB, node *Node, con
|
||||
|
||||
setCols = append(setCols, "mime_type", "size", "status")
|
||||
} else {
|
||||
node.BlobKey = content.blobKey
|
||||
node.BlobKey = content.BlobKey
|
||||
|
||||
b, err := vfs.blobStore.ReadRange(ctx, content.blobKey, 0, 3072)
|
||||
b, err := vfs.blobStore.ReadRange(ctx, content.BlobKey, 0, 3072)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -219,7 +206,7 @@ func (vfs *VirtualFS) WriteFile(ctx context.Context, db bun.IDB, node *Node, con
|
||||
node.MimeType = mt.String()
|
||||
node.Status = NodeStatusReady
|
||||
|
||||
s, err := vfs.blobStore.ReadSize(ctx, content.blobKey)
|
||||
s, err := vfs.blobStore.ReadSize(ctx, content.BlobKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -239,6 +226,34 @@ func (vfs *VirtualFS) WriteFile(ctx context.Context, db bun.IDB, node *Node, con
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vfs *VirtualFS) ReadFile(ctx context.Context, db bun.IDB, node *Node) (FileContent, error) {
|
||||
if node.Kind != NodeKindFile {
|
||||
return EmptyFileContent(), ErrUnsupportedOperation
|
||||
}
|
||||
|
||||
key, err := vfs.keyResolver.Resolve(ctx, db, node)
|
||||
if err != nil {
|
||||
return EmptyFileContent(), err
|
||||
}
|
||||
|
||||
if vfs.blobStore.SupportsDirectDownload() {
|
||||
url, err := vfs.blobStore.GenerateDownloadURL(ctx, key, blob.DownloadURLOptions{
|
||||
Duration: 1 * time.Hour,
|
||||
})
|
||||
if err != nil {
|
||||
return EmptyFileContent(), err
|
||||
}
|
||||
return FileContentFromURL(url), nil
|
||||
}
|
||||
|
||||
reader, err := vfs.blobStore.Read(ctx, key)
|
||||
if err != nil {
|
||||
return EmptyFileContent(), err
|
||||
}
|
||||
|
||||
return FileContentFromReaderWithSize(reader, node.Size), nil
|
||||
}
|
||||
|
||||
func (vfs *VirtualFS) CreateDirectory(ctx context.Context, db bun.IDB, accountID uuid.UUID, parentID uuid.UUID, name string) (*Node, error) {
|
||||
pid, err := vfs.generatePublicID()
|
||||
if err != nil {
|
||||
@@ -319,7 +334,12 @@ func (vfs *VirtualFS) RenameNode(ctx context.Context, db bun.IDB, node *Node, na
|
||||
return ErrNodeNotFound
|
||||
}
|
||||
|
||||
_, err := db.NewUpdate().Model(node).
|
||||
oldKey, err := vfs.keyResolver.Resolve(ctx, db, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.NewUpdate().Model(node).
|
||||
WherePK().
|
||||
Where("status = ?", NodeStatusReady).
|
||||
Where("deleted_at IS NULL").
|
||||
@@ -332,6 +352,30 @@ func (vfs *VirtualFS) RenameNode(ctx context.Context, db bun.IDB, node *Node, na
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
newKey, err := vfs.keyResolver.Resolve(ctx, db, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldKey != newKey {
|
||||
err = vfs.blobStore.Move(ctx, oldKey, newKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if vfs.keyResolver.ShouldPersistKey() {
|
||||
node.BlobKey = newKey
|
||||
_, err = db.NewUpdate().Model(node).
|
||||
WherePK().
|
||||
Set("blob_key = ?", newKey).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -340,7 +384,7 @@ func (vfs *VirtualFS) MoveNode(ctx context.Context, db bun.IDB, node *Node, pare
|
||||
return ErrNodeNotFound
|
||||
}
|
||||
|
||||
oldKey, err := vfs.keyResolver.Resolve(ctx, node)
|
||||
oldKey, err := vfs.keyResolver.Resolve(ctx, db, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -362,7 +406,7 @@ func (vfs *VirtualFS) MoveNode(ctx context.Context, db bun.IDB, node *Node, pare
|
||||
return err
|
||||
}
|
||||
|
||||
newKey, err := vfs.keyResolver.Resolve(ctx, node)
|
||||
newKey, err := vfs.keyResolver.Resolve(ctx, db, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -390,13 +434,10 @@ func (vfs *VirtualFS) AbsolutePath(ctx context.Context, db bun.IDB, node *Node)
|
||||
if !node.IsAccessible() {
|
||||
return "", ErrNodeNotFound
|
||||
}
|
||||
return buildNodeAbsolutePath(ctx, db, node.ID)
|
||||
return buildNodeAbsolutePath(ctx, db, node)
|
||||
}
|
||||
|
||||
func (vfs *VirtualFS) PermanentlyDeleteNode(ctx context.Context, db bun.IDB, node *Node) error {
|
||||
if !node.IsAccessible() {
|
||||
return ErrNodeNotFound
|
||||
}
|
||||
switch node.Kind {
|
||||
case NodeKindFile:
|
||||
return vfs.permanentlyDeleteFileNode(ctx, db, node)
|
||||
@@ -408,7 +449,12 @@ func (vfs *VirtualFS) PermanentlyDeleteNode(ctx context.Context, db bun.IDB, nod
|
||||
}
|
||||
|
||||
func (vfs *VirtualFS) permanentlyDeleteFileNode(ctx context.Context, db bun.IDB, node *Node) error {
|
||||
err := vfs.blobStore.Delete(ctx, node.BlobKey)
|
||||
key, err := vfs.keyResolver.Resolve(ctx, db, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = vfs.blobStore.Delete(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user