Skip to content
Closed
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
37 changes: 37 additions & 0 deletions .github/workflows/quark-mkdir-hardening-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: quark-mkdir-hardening-validation

on:
pull_request:
branches:
- main

permissions:
contents: read

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Check formatting
run: |
files="drivers/quark_uc/driver.go drivers/quark_uc/driver_test.go"
out="$(gofmt -l $files)"
if [ -n "$out" ]; then
printf '%s\n' "$out"
exit 1
fi
- name: Vet Quark driver
run: go vet ./drivers/quark_uc
- name: Test Quark driver
run: go test ./drivers/quark_uc
- name: Race test Quark driver
run: go test -race ./drivers/quark_uc
- name: Shuffled repeated race test
run: go test -race -shuffle=on -count=2 ./drivers/quark_uc
- name: Test op and WebDAV
run: go test ./internal/op ./server/webdav
130 changes: 125 additions & 5 deletions drivers/quark_uc/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import (
"bytes"
"context"
"encoding/hex"
"fmt"
"hash"
"html"
"io"
"net/http"
"strconv"
"strings"
"sync"
"time"
Expand All @@ -22,6 +25,14 @@ import (
log "github.com/sirupsen/logrus"
)

const (
mkdirConflictMessage = "file is doloading[同名冲突]"
mkdirVisibilityAttempts = 6
mkdirVisibilityInitialBackoff = 200 * time.Millisecond
mkdirVisibilityMaxBackoff = 800 * time.Millisecond
mkdirVisibilityMaxPages = 1000
)

type QuarkOrUC struct {
model.Storage
Addition
Expand Down Expand Up @@ -100,20 +111,129 @@ func (d *QuarkOrUC) Link(ctx context.Context, file model.Obj, args model.LinkArg
return d.getDownloadLink(file)
}

func (d *QuarkOrUC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
func (d *QuarkOrUC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
data := base.Json{
"dir_init_lock": false,
"dir_path": "",
"file_name": dirName,
"pdir_fid": parentDir.GetID(),
}
_, err := d.request("/file", http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
req.SetContext(ctx).SetBody(data)
}, nil)
if err == nil {
time.Sleep(time.Second)
if err != nil && err.Error() != mkdirConflictMessage {
return nil, err
}

// Quark may acknowledge a mkdir before /file/sort reflects the new
// directory. Confirm visibility and return the verified object so op.MakeDir
// can update an existing parent cache directly instead of immediately
// re-listing the eventually-consistent backend.
obj, _, visibilityErr := d.waitForDirVisible(ctx, parentDir.GetID(), dirName)
if visibilityErr == nil {
return obj, nil
}
if ctxErr := ctx.Err(); ctxErr != nil {
return nil, ctxErr
}
if err != nil {
// Preserve the existing Quark conflict error when the directory cannot
// be confirmed. A same-name conflict is only treated as success after
// the matching directory is visible in the parent listing.
return nil, err
}

// The create request itself succeeded. A bounded visibility miss (whether
// caused by clean-but-stale listings or listing errors) must not turn that
// successful create into a false failure. Returning nil,nil makes
// op.MakeDir clear the parent cache, so later reads cannot remain pinned to
// a stale listing.
return nil, nil
}

func (d *QuarkOrUC) waitForDirVisible(ctx context.Context, parentID, dirName string) (model.Obj, bool, error) {
backoff := mkdirVisibilityInitialBackoff
var lastErr error
listed := false
for attempt := 0; attempt < mkdirVisibilityAttempts; attempt++ {
if err := ctx.Err(); err != nil {
return nil, listed, err
}

obj, err := d.findDir(ctx, parentID, dirName)
if err == nil {
lastErr = nil
listed = true
if obj != nil {
return obj, true, nil
}
} else {
lastErr = err
}
if attempt == mkdirVisibilityAttempts-1 {
break
}

select {
case <-ctx.Done():
return nil, listed, ctx.Err()
case <-time.After(backoff):
}
if backoff < mkdirVisibilityMaxBackoff {
backoff *= 2
if backoff > mkdirVisibilityMaxBackoff {
backoff = mkdirVisibilityMaxBackoff
}
}
}

if lastErr != nil {
return nil, listed, fmt.Errorf("failed to confirm created directory %q: %w", dirName, lastErr)
}
return nil, listed, fmt.Errorf("created directory %q did not become visible after %d attempts", dirName, mkdirVisibilityAttempts)
}

func (d *QuarkOrUC) findDir(ctx context.Context, parentID, dirName string) (model.Obj, error) {
const size = 100
query := map[string]string{
"pdir_fid": parentID,
"_size": strconv.Itoa(size),
"_fetch_total": "1",
"fetch_all_file": "1",
"fetch_risk_file_name": "1",
}
for page := 1; ; page++ {
if err := ctx.Err(); err != nil {
return nil, err
}
if page > mkdirVisibilityMaxPages {
return nil, fmt.Errorf("quark mkdir visibility listing exceeded %d pages for parent %s", mkdirVisibilityMaxPages, parentID)
}

query["_page"] = strconv.Itoa(page)
var resp SortResp
_, err := d.request("/file/sort", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx).SetQueryParams(query)
}, &resp)
if err != nil {
return nil, err
}
for i := range resp.Data.List {
file := &resp.Data.List[i]
file.FileName = html.UnescapeString(file.FileName)
if !file.File && file.FileName == dirName {
return file, nil
}
}

if resp.Metadata.Total > 0 {
if page*size >= resp.Metadata.Total {
return nil, nil
}
} else if len(resp.Data.List) < size {
return nil, nil
}
}
return err
}

func (d *QuarkOrUC) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
Expand Down
Loading
Loading