4949 errPathNotFound = errors .New ("path not found" )
5050 errInvalidRequest = errors .New ("invalid request" )
5151 errUnauthenticated = errors .New ("unauthenticated" )
52+ errAccessDenied = errors .New ("access denied" )
5253)
5354
5455// ResolveGraphPath returns middleware that detects MS Graph colon-syntax path
@@ -91,7 +92,7 @@ func ResolveGraphPath(gws pool.Selectable[gateway.GatewayAPIClient], logger log.
9192
9293 driveID := chi .URLParam (r , "driveID" )
9394 original := r .URL .Path
94- rewritten , err := rewriteColonPath (r .Context (), gws , l , driveID , rctx .RoutePath )
95+ rewritten , err := rewriteColonPath (r .Context (), gws , l , driveID , rctx .RoutePath , r )
9596 switch {
9697 case errors .Is (err , errPathNotFound ):
9798 l .Debug ().Str ("original" , original ).Msg ("colon-path resolution: not found" )
@@ -105,6 +106,10 @@ func ResolveGraphPath(gws pool.Selectable[gateway.GatewayAPIClient], logger log.
105106 l .Debug ().Str ("original" , original ).Msg ("colon-path resolution: unauthenticated" )
106107 errorcode .Unauthenticated .Render (w , r , http .StatusUnauthorized , "unauthenticated" )
107108 return
109+ case errors .Is (err , errAccessDenied ):
110+ l .Debug ().Str ("original" , original ).Msg ("colon-path resolution: access denied" )
111+ errorcode .AccessDenied .Render (w , r , http .StatusForbidden , "access denied" )
112+ return
108113 case err != nil :
109114 l .Error ().Err (err ).Str ("original" , original ).Msg ("colon-path resolution: internal error" )
110115 errorcode .GeneralException .Render (
@@ -154,19 +159,38 @@ type colonMatch struct {
154159// - "" + other error - operational / internal failure (5xx)
155160//
156161// driveIDParam is the {driveID} route param (raw chi.URLParam value); routePath
157- // is chi.RouteContext().RoutePath (the part below /drives/{driveID}).
162+ // is chi.RouteContext().RoutePath (the part below /drives/{driveID}). r is only
163+ // consulted for the method and the @libre.graph.missingParentsBehavior query
164+ // parameter, which is meaningful for POST .../children requests only and
165+ // ignored otherwise.
158166func rewriteColonPath (
159167 ctx context.Context ,
160168 gws pool.Selectable [gateway.GatewayAPIClient ],
161169 logger zerolog.Logger ,
162170 driveIDParam string ,
163171 routePath string ,
172+ r * http.Request ,
164173) (string , error ) {
165174 match , ok := parseColonPath (routePath )
166175 if ! ok {
167176 return "" , nil
168177 }
169178
179+ // The colon path addresses the parent of the item a POST .../children
180+ // creates. With missingParentsBehavior=create the missing folders along
181+ // that path are created instead of returning 404.
182+ createParents := false
183+ if r .Method == http .MethodPost && match .suffix == "/children" {
184+ switch r .URL .Query ().Get ("@libre.graph.missingParentsBehavior" ) {
185+ case "" , "fail" :
186+ case "create" :
187+ createParents = true
188+ default :
189+ logger .Debug ().Msg ("invalid @libre.graph.missingParentsBehavior in colon path" )
190+ return "" , errInvalidRequest
191+ }
192+ }
193+
170194 // RoutePath follows chi's RawPath, i.e. the percent-encoded wire form
171195 // (e.g. "/Documents/My%20File"). A single PathUnescape reproduces exactly
172196 // what net/http put in r.URL.Path; it is NOT a double-decode (a crafted
@@ -218,7 +242,11 @@ func rewriteColonPath(
218242 return "" , errInvalidRequest
219243 }
220244
221- itemID , err := resolvePath (ctx , gws , & anchor , relPath )
245+ resolve := resolvePath
246+ if createParents {
247+ resolve = resolveOrCreatePath
248+ }
249+ itemID , err := resolve (ctx , gws , & anchor , relPath )
222250 if err != nil {
223251 return "" , err
224252 }
@@ -320,6 +348,79 @@ func resolvePath(
320348 return "" , fmt .Errorf ("gateway selector: %w" , err )
321349 }
322350
351+ return statPath (ctx , gw , anchor , relPath )
352+ }
353+
354+ // resolveOrCreatePath is resolvePath for POST .../children requests with
355+ // @libre.graph.missingParentsBehavior=create: it walks relPath segment by
356+ // segment and creates the missing folders along the way.
357+ func resolveOrCreatePath (
358+ ctx context.Context ,
359+ gws pool.Selectable [gateway.GatewayAPIClient ],
360+ anchor * storageprovider.ResourceId ,
361+ relPath string ,
362+ ) (string , error ) {
363+ gw , err := gws .Next ()
364+ if err != nil {
365+ return "" , fmt .Errorf ("gateway selector: %w" , err )
366+ }
367+
368+ var id , walked string
369+ for _ , segment := range strings .Split (strings .Trim (relPath , "/" ), "/" ) {
370+ walked += "/" + segment
371+ id , err = statPath (ctx , gw , anchor , walked )
372+ if err == nil {
373+ continue
374+ }
375+ if ! errors .Is (err , errPathNotFound ) {
376+ return "" , err
377+ }
378+
379+ res , err := gw .CreateContainer (ctx , & storageprovider.CreateContainerRequest {
380+ Ref : & storageprovider.Reference {
381+ ResourceId : anchor ,
382+ Path : utils .MakeRelativePath (walked ),
383+ },
384+ })
385+ if err != nil {
386+ return "" , fmt .Errorf ("CS3 CreateContainer: %w" , err )
387+ }
388+ switch res .GetStatus ().GetCode () {
389+ case cs3rpc .Code_CODE_OK :
390+ // fall through
391+ case cs3rpc .Code_CODE_ALREADY_EXISTS :
392+ // lost a creation race, the folder is there
393+ case cs3rpc .Code_CODE_NOT_FOUND :
394+ return "" , errPathNotFound
395+ case cs3rpc .Code_CODE_PERMISSION_DENIED :
396+ return "" , errAccessDenied
397+ case cs3rpc .Code_CODE_UNAUTHENTICATED :
398+ return "" , errUnauthenticated
399+ default :
400+ return "" , fmt .Errorf (
401+ "CS3 CreateContainer returned %s: %s" ,
402+ res .GetStatus ().GetCode (),
403+ res .GetStatus ().GetMessage (),
404+ )
405+ }
406+
407+ id , err = statPath (ctx , gw , anchor , walked )
408+ if err != nil {
409+ return "" , err
410+ }
411+ }
412+ return id , nil
413+ }
414+
415+ // statPath stats a relative filesystem path (anchored at the given CS3
416+ // resource id) and returns the item's id, running with the request user's
417+ // permissions.
418+ func statPath (
419+ ctx context.Context ,
420+ gw gateway.GatewayAPIClient ,
421+ anchor * storageprovider.ResourceId ,
422+ relPath string ,
423+ ) (string , error ) {
323424 // relPath is already decoded (PathUnescape'd once by the caller), matching
324425 // the form a normal handler would receive from r.URL.Path.
325426 statRes , err := gw .Stat (ctx , & storageprovider.StatRequest {
0 commit comments