Compare commits

..

3 Commits

Author SHA1 Message Date
renovate[bot] 579ceff18f fix(deps): update module github.com/go-webauthn/webauthn to v0.18.2 2026-09-19 16:39:32 +00:00
Fighting 4580c4db33 fix(alist_v3): set child paths so nested directories resolve (#3019)
* fix(alist_v3): set child paths so nested directories resolve

- Set `Path` on every object returned by `List`, matching the OpenList
  driver. `op.Get` hands a child object straight back to `List`, so a
  child without a path made the driver request `""` from the upstream
  server, which answered with its own root: every directory below the
  mount point served the same listing back, endlessly.
- Add tests covering the child paths and a three-level descent.

Co-authored-by: Claude <81847+claude@users.noreply.github.com>

* test(alist_v3): trim the child-path test to a single case

Collapse the two tests into one two-level descent, drop the recorder
type and the helper funcs, and inline the driver setup. 173 -> 65 lines.

Co-authored-by: Claude <81847+claude@users.noreply.github.com>

---------

Co-authored-by: Fighting <3899648+Elity@users.noreply.github.com>
Co-authored-by: Claude <81847+claude@users.noreply.github.com>
2026-09-15 23:13:10 +08:00
Nostalgia 5447ecb072 fix(s3): encode multipart fixture paths (#3074)
test(s3): encode multipart fixture paths

- Serialize the Local storage addition instead of interpolating filesystem paths into JSON.
- Keep multipart fixture behavior portable across Windows and Linux.

Co-authored-by: nostalume <nostalucent@gmail.com>
Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
2026-09-14 20:30:17 +08:00
12 changed files with 137 additions and 249 deletions
+1
View File
@@ -95,6 +95,7 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
file := model.ObjThumb{
Object: model.Object{
Name: f.Name,
Path: path.Join(dir.GetPath(), f.Name),
Modified: f.Modified,
Ctime: f.Created,
Size: f.Size,
+65
View File
@@ -0,0 +1,65 @@
package alist_v3
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/go-resty/resty/v2"
)
// TestListSetsChildPaths descends two levels the way op.Get does, feeding an
// object from one listing back into List as dir. Without a Path on that object
// the driver asks upstream for "", which a real server answers with its own
// root -- hence the fake upstream's fallback, and the endless self-similar tree.
func TestListSetsChildPaths(t *testing.T) {
tree := map[string][]ObjResp{
"/": {{Name: "root-marker", IsDir: true}},
"/drive": {{Name: "concerts", IsDir: true}},
"/drive/concerts": {{Name: "show.mkv"}},
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req ListReq
_ = json.NewDecoder(r.Body).Decode(&req)
content, ok := tree[req.Path]
if !ok {
content = tree["/"]
}
w.Header().Set("Content-Type", "application/json") // resty only unmarshals JSON
_ = json.NewEncoder(w).Encode(map[string]any{
"code": 200, "message": "success",
"data": map[string]any{"content": content, "total": len(content)},
})
}))
t.Cleanup(srv.Close)
// conf.Conf is nil outside a booted server, so base.InitClient() is unusable.
prev := base.RestyClient
base.RestyClient = resty.New().SetTimeout(5 * time.Second)
t.Cleanup(func() { base.RestyClient = prev })
d := &AListV3{Addition: Addition{
RootPath: driver.RootPath{RootFolderPath: "/drive"},
Address: srv.URL,
}}
dir := model.Obj(&model.Object{Path: "/drive", IsFolder: true})
for _, want := range []string{"/drive/concerts", "/drive/concerts/show.mkv"} {
objs, err := d.List(context.Background(), dir, model.ListArgs{})
if err != nil {
t.Fatalf("List(%q): %v", dir.GetPath(), err)
}
if len(objs) != 1 {
t.Fatalf("List(%q) returned %d objects, want 1", dir.GetPath(), len(objs))
}
if got := objs[0].GetPath(); got != want {
t.Fatalf("child of %q has path %q, want %q", dir.GetPath(), got, want)
}
dir = objs[0]
}
}
+10 -10
View File
@@ -10,7 +10,7 @@ require (
github.com/KarpelesLab/reflink v1.0.2
github.com/KirCute/zip v1.0.1
github.com/OpenListTeam/go-cache v0.1.0
github.com/OpenListTeam/gofakes3 v0.8.2-0.20260911144636-404e34e0f5af
github.com/OpenListTeam/gofakes3 v0.8.1
github.com/OpenListTeam/sftpd-openlist v1.0.1
github.com/OpenListTeam/tache v0.2.2
github.com/OpenListTeam/times v0.1.0
@@ -44,7 +44,7 @@ require (
github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0
github.com/go-resty/resty/v2 v2.17.2
github.com/go-webauthn/webauthn v0.18.0
github.com/go-webauthn/webauthn v0.18.2
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
@@ -75,7 +75,7 @@ require (
github.com/u2takey/ffmpeg-go v0.5.0
github.com/upyun/go-sdk/v3 v3.0.4
github.com/zzzhr1990/go-common-entity v0.0.0-20250202070650-1a200048f0d3
golang.org/x/crypto v0.56.0
golang.org/x/crypto v0.57.0
golang.org/x/image v0.45.0
golang.org/x/net v0.58.0
golang.org/x/oauth2 v0.36.0
@@ -222,7 +222,7 @@ require (
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.3 // indirect
github.com/fxamacker/cbor/v2 v2.9.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-chi/chi/v5 v5.3.1 // indirect
@@ -231,7 +231,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.3 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-webauthn/x v0.3.0 // indirect
github.com/go-webauthn/x v0.3.1 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
@@ -309,11 +309,11 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.5.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.41.0
golang.org/x/tools v0.48.0 // indirect
golang.org/x/sync v0.23.0
golang.org/x/sys v0.48.0
golang.org/x/term v0.46.0 // indirect
golang.org/x/text v0.42.0
golang.org/x/tools v0.49.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260715232425-e75dac1f907d // indirect
google.golang.org/grpc v1.85.0-dev
google.golang.org/protobuf v1.36.11 // indirect
+20 -2
View File
@@ -51,8 +51,8 @@ github.com/OpenListTeam/115-sdk-go v0.2.6 h1:ehXyStvncvn4qRBuknor3kyGZtUmHc0+stj
github.com/OpenListTeam/115-sdk-go v0.2.6/go.mod h1:cfvitk2lwe6036iNi2h+iNxwxWDifKZsSvNtrur5BqU=
github.com/OpenListTeam/go-cache v0.1.0 h1:eV2+FCP+rt+E4OCJqLUW7wGccWZNJMV0NNkh+uChbAI=
github.com/OpenListTeam/go-cache v0.1.0/go.mod h1:AHWjKhNK3LE4rorVdKyEALDHoeMnP8SjiNyfVlB+Pz4=
github.com/OpenListTeam/gofakes3 v0.8.2-0.20260911144636-404e34e0f5af h1:pcORNNaOgbc28g9YliwPc5mpEmGf/dKyZjkXhHZgvVo=
github.com/OpenListTeam/gofakes3 v0.8.2-0.20260911144636-404e34e0f5af/go.mod h1:mS9Ywbo6aId6BrRzeYjOIOpK0QDVnMoKOIb0hpaQZ3U=
github.com/OpenListTeam/gofakes3 v0.8.1 h1:uihJ7Zgb4qIafFcXhcm71BzxCyGRIqBVJYg4YOUa6uY=
github.com/OpenListTeam/gofakes3 v0.8.1/go.mod h1:mS9Ywbo6aId6BrRzeYjOIOpK0QDVnMoKOIb0hpaQZ3U=
github.com/OpenListTeam/gsync v0.1.0 h1:ywzGybOvA3lW8K1BUjKZ2IUlT2FSlzPO4DOazfYXjcs=
github.com/OpenListTeam/gsync v0.1.0/go.mod h1:h/Rvv9aX/6CdW/7B8di3xK3xNV8dUg45Fehrd/ksZ9s=
github.com/OpenListTeam/reflink v0.0.0-20260701021214-78760eaeafef h1:67uGHancMF/abMrnkc8abVUWQiG73Wk5d8CKt3RzkFo=
@@ -375,6 +375,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fxamacker/cbor/v2 v2.9.3 h1:oQBnFATpNdY8gJHTndDDv5Xl4QqNaz51G5LLEPhng3Q=
github.com/fxamacker/cbor/v2 v2.9.3/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fxamacker/cbor/v2 v2.9.4 h1:xwjVlxEMR3S605oUlgBjKLTTeGFciYPGYCtF/35LKGo=
github.com/fxamacker/cbor/v2 v2.9.4/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
@@ -433,10 +435,14 @@ github.com/go-webauthn/webauthn v0.13.4 h1:q68qusWPcqHbg9STSxBLBHnsKaLxNO0RnVKaA
github.com/go-webauthn/webauthn v0.13.4/go.mod h1:MglN6OH9ECxvhDqoq1wMoF6P6JRYDiQpC9nc5OomQmI=
github.com/go-webauthn/webauthn v0.18.0 h1:PC8R3PNLEmjZf++WwcQlo1Z39S9rf8ma69rlwkypZhA=
github.com/go-webauthn/webauthn v0.18.0/go.mod h1:ymzZQhx3D/PrDjznemBdQJ23gHTaSDxUchM7sH1lUCg=
github.com/go-webauthn/webauthn v0.18.2 h1:0BeftmEHU7i3Dv0VFwBtidy/ba37Vcdjvqst9EYu8Sk=
github.com/go-webauthn/webauthn v0.18.2/go.mod h1:hEXaOuLxvZ3zG9miZe3ehlyeVso9AtklXG+kTn36k+A=
github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI=
github.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E=
github.com/go-webauthn/x v0.3.0 h1:Q2X9vbrlP0Ed+QGEzixh1hthGZlDnzVT0XH/9IIQ0kE=
github.com/go-webauthn/x v0.3.0/go.mod h1:5OkdSQdOy7taRXWqvNHggtaPffmW94ybu3rZEER4I+I=
github.com/go-webauthn/x v0.3.1 h1:1ff37z3XfmTTomkhlURgGizLIDyOvPgTt2t9nlzKLRo=
github.com/go-webauthn/x v0.3.1/go.mod h1:ZInxAynYXfBPvvm5gzKZ7geBlL23K71xASMgohHl/Rg=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
@@ -950,6 +956,8 @@ golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
@@ -1001,6 +1009,8 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1026,6 +1036,8 @@ golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1040,6 +1052,8 @@ golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -1056,6 +1070,8 @@ golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
@@ -1073,6 +1089,8 @@ golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
+24 -65
View File
File diff suppressed because it is too large Load Diff
-17
View File
@@ -1,17 +0,0 @@
package s3
import (
"context"
"testing"
"github.com/OpenListTeam/gofakes3"
)
func TestCopyObjectMissingSourceReturnsNoSuchKey(t *testing.T) {
b, _ := setupMultipartBackend(t)
_, err := b.CopyObject(context.Background(), "mp", "missing.txt", "mp", "copy.txt", nil)
if code := s3ErrorCode(err); code != gofakes3.ErrNoSuchKey {
t.Fatalf("CopyObject() error = %v, want NoSuchKey", code)
}
}
-73
View File
@@ -1,73 +0,0 @@
package s3
import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"github.com/OpenListTeam/OpenList/v4/internal/fs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
)
type objectMetadata struct {
headers map[string]string
hash []byte
etag string
}
// Only reuse upload metadata when it still describes the current content.
func (b *s3Backend) loadMetadata(name string, hash []byte) (map[string]string, string) {
if value, ok := b.meta.Load(name); ok {
metadata := value.(objectMetadata)
if bytes.Equal(metadata.hash, hash) {
return metadata.headers, metadata.etag
}
}
return nil, ""
}
// Metadata alone cannot identify content: clients can preserve both file size
// and modification time when overwriting an object. Use the driver's MD5 when
// available, otherwise hash the complete content, including for HEAD and ranges.
func getObjectHash(ctx context.Context, name string, obj model.Obj) ([]byte, error) {
if value := obj.GetHash().GetHash(utils.MD5); value != "" {
hash, err := hex.DecodeString(value)
if err != nil || len(hash) != md5.Size {
return nil, fmt.Errorf("invalid object MD5: %q", value)
}
return hash, nil
}
link, file, err := fs.Link(ctx, name, model.LinkArgs{})
if err != nil {
return nil, err
}
defer link.Close()
size := link.ContentLength
if size <= 0 {
size = file.GetSize()
}
ranges, err := stream.GetRangeReaderFromLink(size, link)
if err != nil {
return nil, err
}
reader, err := ranges.RangeRead(ctx, http_range.Range{Length: -1})
if err != nil {
return nil, err
}
defer reader.Close()
hash := md5.New()
n, err := utils.CopyWithBuffer(hash, reader)
if err != nil {
return nil, err
}
if n != size {
return nil, io.ErrUnexpectedEOF
}
return hash.Sum(nil), nil
}
-64
View File
@@ -1,64 +0,0 @@
package s3
import (
"context"
"crypto/md5"
"encoding/hex"
"net/http/httptest"
"sync"
"testing"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
)
func TestObjectHashFromDriver(t *testing.T) {
sum := md5.Sum([]byte("content"))
obj := &model.Object{HashInfo: utils.NewHashInfo(utils.MD5, hex.EncodeToString(sum[:]))}
hash, err := getObjectHash(context.Background(), "object", obj)
if err != nil || hex.EncodeToString(hash) != hex.EncodeToString(sum[:]) {
t.Fatalf("hash = %x, err = %v", hash, err)
}
for _, value := range []string{"invalid", "ab"} {
obj.HashInfo = utils.NewHashInfo(utils.MD5, value)
if _, err := getObjectHash(context.Background(), "object", obj); err == nil {
t.Fatalf("accepted invalid MD5 %q", value)
}
}
}
func TestMultipartMetadataMatchesCurrentContent(t *testing.T) {
b := &s3Backend{meta: new(sync.Map)}
oldHash := md5.Sum([]byte("old"))
newHash := md5.Sum([]byte("new"))
b.meta.Store("object", objectMetadata{
headers: map[string]string{"Content-Type": "text/plain"},
hash: oldHash[:],
etag: `"multipart-2"`,
})
meta, etag := b.loadMetadata("object", oldHash[:])
if etag != `"multipart-2"` || meta["Content-Type"] != "text/plain" {
t.Fatal("lost metadata for unchanged multipart content")
}
meta, etag = b.loadMetadata("object", newHash[:])
if meta != nil || etag != "" {
t.Fatal("reused a multipart validator after a same-size content change")
}
}
func TestConditionalRequestsDoNotRedirect(t *testing.T) {
for _, method := range []string{"GET", "PUT"} {
for _, name := range []string{"If-Match", "If-None-Match", "If-Modified-Since", "If-Unmodified-Since", "If-Range"} {
for _, value := range []string{"*", ""} {
r := httptest.NewRequest(method, "/bucket/object", nil)
r.Header.Set(name, value)
if url, ok := directObjectURL(r, nil); ok || url != "" {
t.Fatalf("redirected %s with %s", method, name)
}
if url, ok := directUploadURL(r, nil); ok || url != "" {
t.Fatalf("redirected %s with %s", method, name)
}
}
}
}
}
+3 -3
View File
@@ -234,9 +234,7 @@ func (b *s3Backend) CompleteMultipartUpload(ctx context.Context, bucket, object
defer combined.Close()
sum := md5.Sum(concat)
etag := fmt.Sprintf("%q", fmt.Sprintf("%s-%d", hex.EncodeToString(sum[:]), len(ordered)))
err := b.putStream(ctx, bucket, object, state.meta, combined, total, etag)
err := b.putStream(ctx, bucket, object, state.meta, combined, total)
if err != nil {
// Leave the upload in place so the client may retry completion, per
// the gofakes3 MultipartBackend contract.
@@ -246,6 +244,8 @@ func (b *s3Backend) CompleteMultipartUpload(ctx context.Context, bucket, object
// Success: drop bookkeeping and clean up part files.
b.removeUpload(uploadID)
sum := md5.Sum(concat)
etag := fmt.Sprintf("%q", fmt.Sprintf("%s-%d", hex.EncodeToString(sum[:]), len(ordered)))
log.Debugf("s3 multipart: completed upload %s -> %s/%s (%d bytes)", uploadID, bucket, object, total)
return "", etag, nil
}
+12 -1
View File
@@ -3,6 +3,7 @@ package s3
import (
"bytes"
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
@@ -67,11 +68,21 @@ func setupMultipartBackend(t *testing.T) (*s3Backend, string) {
t.Fatalf("mkdir local root: %v", err)
}
t.Cleanup(func() { _ = os.RemoveAll(localRoot) })
addition, err := json.Marshal(struct {
RootFolderPath string `json:"root_folder_path"`
Thumbnail bool `json:"thumbnail"`
}{
RootFolderPath: localRoot,
Thumbnail: false,
})
if err != nil {
t.Fatalf("marshal local storage addition: %v", err)
}
_, err = op.CreateStorage(ctx, model.Storage{
Driver: "Local",
MountPath: mount,
Addition: `{"root_folder_path":"` + localRoot + `","thumbnail":false}`,
Addition: string(addition),
})
if err != nil {
t.Fatalf("create local storage: %+v", err)
-12
View File
@@ -1,12 +0,0 @@
package s3
import "net/http"
func hasPreconditions(r *http.Request) bool {
for _, name := range []string{"If-Match", "If-None-Match", "If-Modified-Since", "If-Unmodified-Since", "If-Range"} {
if _, ok := r.Header[name]; ok {
return true
}
}
return false
}
+2 -2
View File
@@ -38,7 +38,7 @@ func redirectHandler(next http.Handler, authPairs map[string]string) http.Handle
}
func directObjectURL(r *http.Request, authPairs map[string]string) (string, bool) {
if r.Method != http.MethodGet || hasPreconditions(r) {
if r.Method != http.MethodGet {
return "", false
}
if hasNonObjectQuery(r) || !s3RequestAuthorized(r, authPairs) {
@@ -75,7 +75,7 @@ func directObjectURL(r *http.Request, authPairs map[string]string) (string, bool
}
func directUploadURL(r *http.Request, authPairs map[string]string) (string, bool) {
if r.Method != http.MethodPut || r.ContentLength < 0 || hasPreconditions(r) {
if r.Method != http.MethodPut || r.ContentLength < 0 {
return "", false
}
if hasNonObjectQuery(r) || !s3RequestAuthorized(r, authPairs) {