@@ -17,11 +17,12 @@ import (
1717// newAlistServer starts a fake alist whose login endpoint issues sequential
1818// tokens and whose PUT endpoint rejects the given token (simulating an expired
1919// credential) while accepting refreshed ones.
20- func newAlistServer (t * testing.T , rejectedToken string ) (* httptest.Server , * sync.Mutex , * int , * []putRecord ) {
20+ func newAlistServer (t * testing.T , rejectedToken string ) (* httptest.Server , * sync.Mutex , * int , * []putRecord , map [ string ] bool ) {
2121 t .Helper ()
2222 var mu sync.Mutex
2323 loginCount := 0
2424 tokenSeq := 0
25+ createdDirs := map [string ]bool {}
2526 var puts []putRecord
2627
2728 mux := http .NewServeMux ()
@@ -51,9 +52,41 @@ func newAlistServer(t *testing.T, rejectedToken string) (*httptest.Server, *sync
5152 }
5253 json .NewEncoder (w ).Encode (map [string ]any {"code" : 200 , "message" : "ok" })
5354 })
55+ mux .HandleFunc ("/api/fs/get" , func (w http.ResponseWriter , r * http.Request ) {
56+ var body struct {
57+ Path string `json:"path"`
58+ }
59+ json .NewDecoder (r .Body ).Decode (& body )
60+ mu .Lock ()
61+ defer mu .Unlock ()
62+ exists := body .Path == "/probe" || createdDirs [body .Path ]
63+ w .Header ().Set ("Content-Type" , "application/json" )
64+ if ! exists {
65+ json .NewEncoder (w ).Encode (map [string ]any {"code" : 500 , "message" : "object not found" })
66+ return
67+ }
68+ json .NewEncoder (w ).Encode (map [string ]any {"code" : 200 , "message" : "ok" , "data" : map [string ]any {"is_dir" : true }})
69+ })
70+ mux .HandleFunc ("/api/fs/mkdir" , func (w http.ResponseWriter , r * http.Request ) {
71+ var body struct {
72+ Path string `json:"path"`
73+ }
74+ json .NewDecoder (r .Body ).Decode (& body )
75+ mu .Lock ()
76+ defer mu .Unlock ()
77+ if r .Header .Get ("Authorization" ) == rejectedToken {
78+ w .Header ().Set ("Content-Type" , "application/json" )
79+ w .WriteHeader (http .StatusUnauthorized )
80+ json .NewEncoder (w ).Encode (map [string ]any {"code" : 401 , "message" : "unauthorized" })
81+ return
82+ }
83+ createdDirs [body .Path ] = true
84+ w .Header ().Set ("Content-Type" , "application/json" )
85+ json .NewEncoder (w ).Encode (map [string ]any {"code" : 200 , "message" : "ok" })
86+ })
5487 srv := httptest .NewServer (mux )
5588 t .Cleanup (srv .Close )
56- return srv , & mu , & loginCount , & puts
89+ return srv , & mu , & loginCount , & puts , createdDirs
5790}
5891
5992type putRecord struct {
@@ -66,7 +99,7 @@ type putRecord struct {
6699// (token-1); the server rejects it, so the concurrent uploads must trigger a
67100// second, merged login (token-2).
68101func TestConcurrent401RetrySingleLogin (t * testing.T ) {
69- srv , mu , loginCount , putAuths := newAlistServer (t , "token-1" )
102+ srv , mu , loginCount , putAuths , _ := newAlistServer (t , "token-1" )
70103
71104 cfg := & storconfig.AlistStorageConfig {}
72105 cfg .Name = "probe"
@@ -123,7 +156,7 @@ func TestConcurrent401RetrySingleLogin(t *testing.T) {
123156// A token-only storage receives 401 and must return the auth error without
124157// attempting a login (it has no credentials to refresh with).
125158func TestTokenOnlyNoLoginOn401 (t * testing.T ) {
126- srv , mu , loginCount , _ := newAlistServer (t , "token-0" )
159+ srv , mu , loginCount , _ , _ := newAlistServer (t , "token-0" )
127160
128161 cfg := & storconfig.AlistStorageConfig {}
129162 cfg .Name = "probe"
@@ -150,3 +183,37 @@ func TestTokenOnlyNoLoginOn401(t *testing.T) {
150183 t .Fatalf ("expected no login attempts for token-only storage, got %d" , * loginCount )
151184 }
152185}
186+
187+ // Regression for issue #239: uploading when the parent directory does not
188+ // exist made alist return FileNotFound (code 500) and the task layer retried
189+ // until exhaustion. Save must create missing parent directories first.
190+ func TestSaveCreatesMissingParentDirs (t * testing.T ) {
191+ srv , mu , loginCount , _ , createdDirs := newAlistServer (t , "unused" )
192+
193+ cfg := & storconfig.AlistStorageConfig {}
194+ cfg .Name = "probe"
195+ cfg .URL = srv .URL
196+ cfg .Username = "user"
197+ cfg .Password = "pass"
198+ cfg .BasePath = "/probe"
199+
200+ stor := & alist.Alist {}
201+ if err := stor .Init (t .Context (), cfg ); err != nil {
202+ t .Fatalf ("Init failed: %v" , err )
203+ }
204+
205+ if err := stor .Save (t .Context (), bytes .NewReader ([]byte ("data" )), "a/b/file.txt" ); err != nil {
206+ t .Fatalf ("Save failed: %v" , err )
207+ }
208+
209+ mu .Lock ()
210+ defer mu .Unlock ()
211+ for _ , dir := range []string {"/probe/a" , "/probe/a/b" } {
212+ if ! createdDirs [dir ] {
213+ t .Fatalf ("expected directory %s to be created" , dir )
214+ }
215+ }
216+ if * loginCount != 1 {
217+ t .Fatalf ("expected only the init login, got %d" , * loginCount )
218+ }
219+ }
0 commit comments