|
|
|
|
@@ -31,7 +31,7 @@ func scanNode(scan func(dest ...any) error) (*model.Node, error) {
|
|
|
|
|
&n.ID, &n.ParentID, &n.NodeType, &n.Title, &n.OwnerUserID, &n.OwnerDepartmentID,
|
|
|
|
|
&n.CreatedBy, &n.UpdatedBy, &n.StorageKey, &n.OriginalFilename, &n.MimeType,
|
|
|
|
|
&n.Extension, &n.SizeBytes, &n.OfficeFormat, &n.ExternalURL, &n.Version,
|
|
|
|
|
&n.EffectiveAccess, &n.CreatedAt, &n.UpdatedAt, &n.DeletedAt,
|
|
|
|
|
&n.EffectiveAccess, &n.CreatedAt, &n.UpdatedAt, &n.TrashedAt, &n.PurgeAfter, &n.DeletedAt,
|
|
|
|
|
)
|
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
return nil, ErrNotFound
|
|
|
|
|
@@ -67,7 +67,7 @@ func (r *NodeRepository) List(ctx context.Context, userID string, subordinateIDs
|
|
|
|
|
n.created_by, n.updated_by, n.storage_key, n.original_filename, n.mime_type,
|
|
|
|
|
n.extension, n.size_bytes, n.office_format, n.external_url, n.version,
|
|
|
|
|
effective_node_access(n.id, $1, $2::text[]),
|
|
|
|
|
n.created_at, n.updated_at, n.deleted_at
|
|
|
|
|
n.created_at, n.updated_at, n.trashed_at, n.purge_after, n.deleted_at
|
|
|
|
|
FROM files_nodes n
|
|
|
|
|
WHERE ` + strings.Join(where, " AND ") + `
|
|
|
|
|
ORDER BY CASE WHEN n.node_type = 'folder' THEN 0 ELSE 1 END, lower(n.title), n.created_at DESC`
|
|
|
|
|
@@ -95,7 +95,7 @@ func (r *NodeRepository) GetForUser(ctx context.Context, id, userID string, subo
|
|
|
|
|
n.created_by, n.updated_by, n.storage_key, n.original_filename, n.mime_type,
|
|
|
|
|
n.extension, n.size_bytes, n.office_format, n.external_url, n.version,
|
|
|
|
|
effective_node_access(n.id, $2, $3::text[]),
|
|
|
|
|
n.created_at, n.updated_at, n.deleted_at
|
|
|
|
|
n.created_at, n.updated_at, n.trashed_at, n.purge_after, n.deleted_at
|
|
|
|
|
FROM files_nodes n
|
|
|
|
|
WHERE n.id = $1
|
|
|
|
|
AND n.deleted_at IS NULL
|
|
|
|
|
@@ -110,7 +110,7 @@ func (r *NodeRepository) CreateFolder(ctx context.Context, title string, parentI
|
|
|
|
|
RETURNING id, parent_id, node_type, title, owner_user_id, owner_department_id,
|
|
|
|
|
created_by, updated_by, storage_key, original_filename, mime_type,
|
|
|
|
|
extension, size_bytes, office_format, external_url, version,
|
|
|
|
|
'edit' AS effective_access, created_at, updated_at, deleted_at
|
|
|
|
|
'edit' AS effective_access, created_at, updated_at, trashed_at, purge_after, deleted_at
|
|
|
|
|
`, parentID, title, ownerID).Scan)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -123,7 +123,7 @@ func (r *NodeRepository) CreateFile(ctx context.Context, n *model.Node) (*model.
|
|
|
|
|
RETURNING id, parent_id, node_type, title, owner_user_id, owner_department_id,
|
|
|
|
|
created_by, updated_by, storage_key, original_filename, mime_type,
|
|
|
|
|
extension, size_bytes, office_format, external_url, version,
|
|
|
|
|
'edit' AS effective_access, created_at, updated_at, deleted_at
|
|
|
|
|
'edit' AS effective_access, created_at, updated_at, trashed_at, purge_after, deleted_at
|
|
|
|
|
`, n.ParentID, n.Title, n.OwnerUserID, n.StorageKey, n.OriginalFilename, n.MimeType, n.Extension, n.SizeBytes).Scan)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -141,11 +141,44 @@ func (r *NodeRepository) Update(ctx context.Context, id, actorID string, req mod
|
|
|
|
|
RETURNING id, parent_id, node_type, title, owner_user_id, owner_department_id,
|
|
|
|
|
created_by, updated_by, storage_key, original_filename, mime_type,
|
|
|
|
|
extension, size_bytes, office_format, external_url, version,
|
|
|
|
|
'edit' AS effective_access, created_at, updated_at, deleted_at
|
|
|
|
|
'edit' AS effective_access, created_at, updated_at, trashed_at, purge_after, deleted_at
|
|
|
|
|
`, id, actorID, req.Title, req.ParentID).Scan)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *NodeRepository) SoftDelete(ctx context.Context, id, actorID string) error {
|
|
|
|
|
func (r *NodeRepository) Move(ctx context.Context, id, actorID string, subordinateIDs []string, parentID *string) (*model.Node, error) {
|
|
|
|
|
if parentID != nil && *parentID != "" {
|
|
|
|
|
var wouldCycle bool
|
|
|
|
|
if err := r.pool.QueryRow(ctx, `
|
|
|
|
|
WITH RECURSIVE subtree AS (
|
|
|
|
|
SELECT id FROM files_nodes WHERE id = $1 AND deleted_at IS NULL
|
|
|
|
|
UNION ALL
|
|
|
|
|
SELECT c.id FROM files_nodes c JOIN subtree s ON c.parent_id = s.id WHERE c.deleted_at IS NULL
|
|
|
|
|
)
|
|
|
|
|
SELECT EXISTS (SELECT 1 FROM subtree WHERE id = $2)
|
|
|
|
|
`, id, *parentID).Scan(&wouldCycle); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if wouldCycle {
|
|
|
|
|
return nil, model.ErrInvalidMove
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return scanNode(r.pool.QueryRow(ctx, `
|
|
|
|
|
UPDATE files_nodes
|
|
|
|
|
SET parent_id = $4,
|
|
|
|
|
updated_by = $2,
|
|
|
|
|
updated_at = now(),
|
|
|
|
|
version = version + 1
|
|
|
|
|
WHERE id = $1
|
|
|
|
|
AND deleted_at IS NULL
|
|
|
|
|
AND effective_node_access(id, $2, $3::text[]) = 'edit'
|
|
|
|
|
RETURNING id, parent_id, node_type, title, owner_user_id, owner_department_id,
|
|
|
|
|
created_by, updated_by, storage_key, original_filename, mime_type,
|
|
|
|
|
extension, size_bytes, office_format, external_url, version,
|
|
|
|
|
'edit' AS effective_access, created_at, updated_at, trashed_at, purge_after, deleted_at
|
|
|
|
|
`, id, actorID, subordinateIDs, parentID).Scan)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *NodeRepository) SoftDelete(ctx context.Context, id, actorID string, purgeAfter time.Time) error {
|
|
|
|
|
tag, err := r.pool.Exec(ctx, `
|
|
|
|
|
WITH RECURSIVE subtree AS (
|
|
|
|
|
SELECT id FROM files_nodes WHERE id = $1 AND deleted_at IS NULL
|
|
|
|
|
@@ -153,10 +186,10 @@ func (r *NodeRepository) SoftDelete(ctx context.Context, id, actorID string) err
|
|
|
|
|
SELECT c.id FROM files_nodes c JOIN subtree s ON c.parent_id = s.id WHERE c.deleted_at IS NULL
|
|
|
|
|
)
|
|
|
|
|
UPDATE files_nodes
|
|
|
|
|
SET deleted_at = now(), updated_by = $2, updated_at = now()
|
|
|
|
|
SET deleted_at = now(), trashed_at = now(), purge_after = $3, updated_by = $2, updated_at = now()
|
|
|
|
|
WHERE id IN (SELECT id FROM subtree)
|
|
|
|
|
AND effective_node_access($1, $2, '{}'::text[]) = 'edit'
|
|
|
|
|
`, id, actorID)
|
|
|
|
|
`, id, actorID, purgeAfter)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
@@ -166,6 +199,41 @@ func (r *NodeRepository) SoftDelete(ctx context.Context, id, actorID string) err
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *NodeRepository) ListPurgeableStorageKeys(ctx context.Context) ([]string, error) {
|
|
|
|
|
rows, err := r.pool.Query(ctx, `
|
|
|
|
|
SELECT storage_key
|
|
|
|
|
FROM files_nodes
|
|
|
|
|
WHERE purge_after IS NOT NULL
|
|
|
|
|
AND purge_after <= now()
|
|
|
|
|
AND storage_key IS NOT NULL
|
|
|
|
|
`)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
var out []string
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var key string
|
|
|
|
|
if err := rows.Scan(&key); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
out = append(out, key)
|
|
|
|
|
}
|
|
|
|
|
return out, rows.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *NodeRepository) PurgeExpired(ctx context.Context) (int64, error) {
|
|
|
|
|
tag, err := r.pool.Exec(ctx, `
|
|
|
|
|
DELETE FROM files_nodes
|
|
|
|
|
WHERE purge_after IS NOT NULL
|
|
|
|
|
AND purge_after <= now()
|
|
|
|
|
`)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
return tag.RowsAffected(), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *NodeRepository) ListAccess(ctx context.Context, nodeID string) ([]model.Access, error) {
|
|
|
|
|
rows, err := r.pool.Query(ctx, `
|
|
|
|
|
SELECT user_id, access_level, granted_by, created_at
|
|
|
|
|
@@ -260,7 +328,7 @@ func (r *NodeRepository) GetByPublicToken(ctx context.Context, token string) (*m
|
|
|
|
|
SELECT n.id, n.parent_id, n.node_type, n.title, n.owner_user_id, n.owner_department_id,
|
|
|
|
|
n.created_by, n.updated_by, n.storage_key, n.original_filename, n.mime_type,
|
|
|
|
|
n.extension, n.size_bytes, n.office_format, n.external_url, n.version,
|
|
|
|
|
'view' AS effective_access, n.created_at, n.updated_at, n.deleted_at
|
|
|
|
|
'view' AS effective_access, n.created_at, n.updated_at, n.trashed_at, n.purge_after, n.deleted_at
|
|
|
|
|
FROM files_public_links l
|
|
|
|
|
JOIN files_nodes n ON n.id = l.node_id
|
|
|
|
|
WHERE l.token_hash = $1
|
|
|
|
|
|