Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/api/graph_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ func (m *mockNodeRepoForGraph) EnsureParentDirectoryExists(ctx context.Context,
func (m *mockNodeRepoForGraph) TouchAncestors(ctx context.Context, userID int64, paths ...string) error {
return nil
}
func (m *mockNodeRepoForGraph) RecalculateDirectorySizes(ctx context.Context, userID int64) error {
return nil
}

func TestMeDrivesHandler(t *testing.T) {
handler := api.NewGraphHandler(nil, &mockNodeRepoForGraph{})
Expand Down
3 changes: 3 additions & 0 deletions internal/api/shares_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ func (m *mockNodeRepo) EnsureParentDirectoryExists(ctx context.Context, userID i
func (m *mockNodeRepo) TouchAncestors(ctx context.Context, userID int64, paths ...string) error {
return nil
}
func (m *mockNodeRepo) RecalculateDirectorySizes(ctx context.Context, userID int64) error {
return nil
}

type mockShareRepo struct {
shares map[string]*domain.Share
Expand Down
19 changes: 19 additions & 0 deletions internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,25 @@ func Migrate(db *sql.DB) error {
return nil
},
},
{
version: 5,
description: "Calculate initial sizes for all directory nodes",
run: func(db *sql.DB) error {
query := `UPDATE nodes d
JOIN (
SELECT d2.id, COALESCE(SUM(f.size), 0) AS total_size
FROM nodes d2
LEFT JOIN nodes f ON f.user_id = d2.user_id AND f.is_dir = 0 AND (
CASE WHEN d2.path = '/' THEN 1 ELSE f.path LIKE CONCAT(d2.path, '/%') END
)
WHERE d2.is_dir = 1
GROUP BY d2.id
) s ON d.id = s.id
SET d.size = s.total_size;`
_, err := db.Exec(query)
return err
},
},
}

for _, m := range migrations {
Expand Down
26 changes: 26 additions & 0 deletions internal/db/db_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package db

import (
"os"
"testing"
)

func TestMigrations(t *testing.T) {
dsn := os.Getenv("DB_DSN")
if dsn == "" {
dsn = "selfcloud:selfcloud_pass@tcp(127.0.0.1:3306)/selfcloud?parseTime=true"
}

sqlDB, err := Connect("mysql", dsn)
if err != nil {
t.Skipf("Skipping db migration test: MariaDB connection failed: %v", err)
return
}
defer sqlDB.Close()

// Reset migration 5 record so Migrate runs it
_, _ = sqlDB.Exec("DELETE FROM schema_migrations WHERE version = 5")
if err := Migrate(sqlDB); err != nil {
t.Fatalf("Migrate failed: %v", err)
}
}
1 change: 1 addition & 0 deletions internal/indexer/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ func (r *Reindexer) ReindexUser(ctx context.Context, user *domain.User, removeOr

if !r.dryRun && (stats.Created > 0 || stats.Updated > 0 || stats.Deleted > 0) {
_ = r.nodeRepo.TouchAncestors(ctx, user.ID, "/")
_ = r.nodeRepo.RecalculateDirectorySizes(ctx, user.ID)
}

return stats, nil
Expand Down
33 changes: 33 additions & 0 deletions internal/repository/node_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,24 @@ func (r *nodeRepo) TouchAncestors(ctx context.Context, userID int64, paths ...st
_, _ = rand.Read(b)
newETag := hex.EncodeToString(b)

var dirSize int64
if a == "/" {
const sizeQuery = `SELECT COALESCE(SUM(size), 0) FROM nodes WHERE user_id = ? AND is_dir = 0`
if err := r.db.QueryRowContext(ctx, sizeQuery, userID).Scan(&dirSize); err != nil {
return fmt.Errorf("failed to calculate size for root ancestor: %w", err)
}
} else {
const sizeQuery = `SELECT COALESCE(SUM(size), 0) FROM nodes WHERE user_id = ? AND is_dir = 0 AND path LIKE ?`
likePattern := escapeSQLLike(a) + "/%"
if err := r.db.QueryRowContext(ctx, sizeQuery, userID, likePattern).Scan(&dirSize); err != nil {
return fmt.Errorf("failed to calculate size for ancestor %s: %w", a, err)
}
}

query, args, err := dialect.Update("nodes").
Set(goqu.Record{
"etag": newETag,
"size": dirSize,
"mtime": now,
"updated_at": now,
}).
Expand All @@ -348,6 +363,24 @@ func (r *nodeRepo) TouchAncestors(ctx context.Context, userID int64, paths ...st
return nil
}

func (r *nodeRepo) RecalculateDirectorySizes(ctx context.Context, userID int64) error {
query := `UPDATE nodes d
JOIN (
SELECT d2.id, COALESCE(SUM(f.size), 0) AS total_size
FROM nodes d2
LEFT JOIN nodes f ON f.user_id = d2.user_id AND f.is_dir = 0 AND (
CASE WHEN d2.path = '/' THEN 1 ELSE f.path LIKE CONCAT(d2.path, '/%') END
)
WHERE d2.user_id = ? AND d2.is_dir = 1
GROUP BY d2.id
) s ON d.id = s.id
SET d.size = s.total_size;`
if _, err := r.db.ExecContext(ctx, query, userID); err != nil {
return fmt.Errorf("failed to recalculate directory sizes for user %d: %w", userID, err)
}
return nil
}

func normalizePath(p string) string {
return pathutil.NormalizePath(p)
}
Expand Down
1 change: 1 addition & 0 deletions internal/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type NodeRepository interface {
MovePrefix(ctx context.Context, userID int64, oldPrefix, newPrefix string) error
EnsureParentDirectoryExists(ctx context.Context, userID int64, parentPath string) error
TouchAncestors(ctx context.Context, userID int64, paths ...string) error
RecalculateDirectorySizes(ctx context.Context, userID int64) error
}

type ShareRepository interface {
Expand Down
21 changes: 21 additions & 0 deletions internal/repository/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,27 @@ func TestRepositoryOperations(t *testing.T) {
if newFolderB.ETag == oldFolderB.ETag {
t.Errorf("expected /folderB ETag to change after TouchAncestors, remained %s", newFolderB.ETag)
}
if newFolderB.Size != 999 {
t.Errorf("expected /folderB Size to be 999 after TouchAncestors, got %d", newFolderB.Size)
}
if newRoot.Size < 999 {
t.Errorf("expected root Size to be at least 999 after TouchAncestors, got %d", newRoot.Size)
}

// Test RecalculateDirectorySizes
canceledCtx2, cancel2 := context.WithCancel(ctx)
cancel2()
if err := repos.Nodes.RecalculateDirectorySizes(canceledCtx2, fetchedUser.ID); err == nil {
t.Errorf("expected error with canceled context in RecalculateDirectorySizes, got nil")
}

if err := repos.Nodes.RecalculateDirectorySizes(ctx, fetchedUser.ID); err != nil {
t.Errorf("RecalculateDirectorySizes failed: %v", err)
}
recalcFolderB, _ := repos.Nodes.GetByPath(ctx, fetchedUser.ID, "/folderB")
if recalcFolderB.Size != 999 {
t.Errorf("expected /folderB Size to be 999 after RecalculateDirectorySizes, got %d", recalcFolderB.Size)
}

// 3. Share Repository with ExpiresAt & ListByUserID
expiresAt := time.Now().Add(24 * time.Hour)
Expand Down
48 changes: 48 additions & 0 deletions internal/webdav/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -143,12 +144,38 @@ func (m *mockNodeRepo) TouchAncestors(ctx context.Context, userID int64, paths .
if node, ok := m.nodes[a]; ok && node.IsDir {
node.ETag = fmt.Sprintf("touched_%d", time.Now().UnixNano())
node.MTime = time.Now()
var totalSize int64
for _, child := range m.nodes {
if !child.IsDir {
if a == "/" || strings.HasPrefix(child.Path, a+"/") {
totalSize += child.Size
}
}
}
node.Size = totalSize
}
}
}
return nil
}

func (m *mockNodeRepo) RecalculateDirectorySizes(ctx context.Context, userID int64) error {
for _, dir := range m.nodes {
if dir.IsDir {
var totalSize int64
for _, child := range m.nodes {
if !child.IsDir {
if dir.Path == "/" || strings.HasPrefix(child.Path, dir.Path+"/") {
totalSize += child.Size
}
}
}
dir.Size = totalSize
}
}
return nil
}

func TestWebDAVHandlerGetAndRange(t *testing.T) {
tempDir, err := os.MkdirTemp("", "webdav_test_*")
if err != nil {
Expand Down Expand Up @@ -353,6 +380,27 @@ func TestWebDAVHandlerGetAndRange(t *testing.T) {
}
})

t.Run("PROPFIND on directory returns oc:size matching directory size", func(t *testing.T) {
repo.nodes["/sizedir"] = &domain.Node{
UserID: 1, Path: "/sizedir", Name: "sizedir", IsDir: true, Size: 12345,
}
req := withUserCtx(httptest.NewRequest("PROPFIND", "/remote.php/webdav/sizedir", nil))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)

if rec.Code != http.StatusMultiStatus {
t.Fatalf("expected status 207 Multi-Status, got %d", rec.Code)
}

body := rec.Body.String()
if !strings.Contains(body, "<oc:size>12345</oc:size>") {
t.Fatalf("expected oc:size to be 12345 for directory, got: %s", body)
}
if strings.Contains(body, "<d:getcontentlength>") {
t.Fatalf("expected d:getcontentlength to NOT be present on directory resource, got: %s", body)
}
})

t.Run("MKCOL creates directory successfully", func(t *testing.T) {
req := withUserCtx(httptest.NewRequest("MKCOL", "/remote.php/webdav/testdir", nil))
rec := httptest.NewRecorder()
Expand Down
5 changes: 3 additions & 2 deletions internal/webdav/xml.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,14 @@ func FormatMultiStatus(basePath string, nodes []*domain.Node) ([]byte, error) {
OCPermissions: permissions,
}

size := node.Size
prop.OCSize = &size

if node.IsDir {
prop.ResourceType = &ResourceType{Collection: &struct{}{}}
} else {
prop.ResourceType = &ResourceType{}
size := node.Size
prop.GetContentLength = &size
prop.OCSize = &size
if node.ContentType != "" {
prop.GetContentType = node.ContentType
} else {
Expand Down
5 changes: 5 additions & 0 deletions test/functional/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,7 @@ func TestOwnCloudSyncClientOAuthWorkflow(t *testing.T) {
PropStat struct {
Prop struct {
GetETag string `xml:"getetag"`
OCSize int64 `xml:"size"`
} `xml:"prop"`
} `xml:"propstat"`
} `xml:"response"`
Expand Down Expand Up @@ -869,6 +870,10 @@ func TestOwnCloudSyncClientOAuthWorkflow(t *testing.T) {
t.Fatalf("expected root ETag to change after PUT file (to notify sync client), before=%q, after=%q", rootETagBefore, rootETagAfter)
}

if rootSizeAfter := msAfter.Responses[0].PropStat.Prop.OCSize; rootSizeAfter < int64(len(putContent)) {
t.Fatalf("expected root directory oc:size to reflect uploaded file, got %d, expected at least %d", rootSizeAfter, len(putContent))
}

// 8d. GET /remote.php/webdav/client_sync_test.txt
getReq, _ := http.NewRequest("GET", server.URL+"/remote.php/webdav/client_sync_test.txt", nil)
getReq.Header.Set("Authorization", "Bearer "+tokenData.AccessToken)
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/FileList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export const FileList: React.FC<FileListProps> = ({

{/* Size */}
<td className="py-3 px-4 hidden md:table-cell text-slate-400 text-xs font-mono">
{item.is_dir ? '-' : formatBytes(item.size)}
{item.is_dir ? (item.size > 0 ? formatBytes(item.size) : '-') : formatBytes(item.size)}
</td>

{/* Actions */}
Expand Down
Loading