11package command
22
33import (
4+ "encoding/json"
45 "errors"
6+ "fmt"
7+ "os"
8+ "path/filepath"
9+ "strings"
510
611 "github.com/rs/zerolog"
12+ "github.com/shamaton/msgpack/v2"
713 "github.com/urfave/cli/v2"
814
915 "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool"
@@ -25,18 +31,9 @@ func SharesCommand(cfg *config.Config) *cli.Command {
2531 Name : "shares" ,
2632 Usage : `cli tools to manage entries in the share manager.` ,
2733 Category : "maintenance" ,
28- Before : func (c * cli.Context ) error {
29- // Parse base config
30- if err := parser .ParseConfig (cfg , true ); err != nil {
31- return configlog .ReturnError (err )
32- }
33-
34- // Parse sharing config
35- cfg .Sharing .Commons = cfg .Commons
36- return configlog .ReturnError (sharingparser .ParseConfig (cfg .Sharing ))
37- },
3834 Subcommands : []* cli.Command {
3935 cleanupCmd (cfg ),
36+ moveStuckUploadBlobsCmd (cfg ),
4037 },
4138 }
4239}
@@ -126,3 +123,223 @@ func cleanup(c *cli.Context, cfg *config.Config) error {
126123
127124 return nil
128125}
126+
127+ // oCIS directory structure for share-manager metadata and user spaces:
128+ //
129+ // ocisHome/storage/
130+ // ...
131+ // └── metadata/
132+ // ├── spaces/js/oncs3-share-manager-metadata/ (rootMetadata - Phase 1,3,4)
133+ // │ ├── blobs/
134+ // │ │ ├── 9c/a3/b2/f5/-42a1-4b8e-9123-456789abcdef (Phase 4: MISSING received.json blob - reconstructed here)
135+ // │ │ │ {"Spaces": {"215fee7a-...:480049db-...": {"States": {"...:...:84652da9-...": {State: 2, MountPoint: {path: "file.txt"}}}}}}
136+ // │ │ └── d7/02/d7/e1/-37b0-4d41-b8dc-4b90c1d1f907 (Phase 1: read <spaceID>.json blob for Shares data)
137+ // │ │ {"Shares": {"215fee7a-...:480049db-...:84652da9-...": {resource_id: {...}, grantee: {...}, creator: {...}}}}
138+ // │ └── nodes/
139+ // │ ├── 3a/5f/c2/d8/-1234-5678-abcd-ef0123456789.mpk (Phase 4: received.json MPK → points to MISSING blob)
140+ // │ │ {"user.ocis.name": "received.json", "user.ocis.blobid": "9ca3b2f5-42a1-4b8e-9123-456789abcdef", "user.ocis.parentid": "a9a54ce7-..."}
141+ // │ ├── 99/98/b8/bf/-6871-49cc-aca9-dab4984dc1e4.mpk (Phase 1: <spaceID>.json MPK → points to Shares blob)
142+ // │ │ {"user.ocis.name": "480049db-...-...-....json", "user.ocis.blobid": "d702d7e1-37b0-4d41-b8dc-4b90c1d1f907"}
143+ // │ └── a9/a5/4c/e7/-de30-4d27-94f8-10e4612c66c2.mpk (Phase 3: parent node for ancestry lookup)
144+ // │ {"user.ocis.name": "einstein", "user.ocis.id": "a9a54ce7-...", "user.ocis.parentid": "...users-node-id..."}
145+ // └── uploads/ (rootMetadataUploads)
146+ // ├── d702d7e1-37b0-4d41-b8dc-4b90c1d1f907 (Phase 1: read <spaceID>.json blob for Shares data; blobUploadsPath = filepath.Join(rootMetadataUploads, blobID))
147+ // │ {"Shares": {"215fee7a-...:480049db-...:84652da9-...": {resource_id: {...}, grantee: {...}, creator: {...}}}}
148+ // └── 1c93b82b-d22d-41e0-8038-5a706e9b409e.info
149+ // {"MetaData": {"dir": "/users/4c510ada-c86b-4815-8820-42cdf82c3d51", "filename": "received.json", ...}, "Storage": {"NodeName": "received.json", "SpaceRoot": "jsoncs3-share-manager-metadata", ...}}
150+
151+ func moveStuckUploadBlobsCmd (cfg * config.Config ) * cli.Command {
152+ return & cli.Command {
153+ Name : "move-stuck-upload-blobs" ,
154+ Usage : `move stuck upload blobs to the jsoncs3 share-manager metadata` ,
155+ Flags : []cli.Flag {
156+ & cli.BoolFlag {
157+ Name : "dry-run" ,
158+ Value : false ,
159+ Usage : "Dry run mode enabled" ,
160+ },
161+ & cli.StringFlag {
162+ Name : "ocis-home" ,
163+ Value : "~/.ocis" ,
164+ Usage : "oCIS home directory" ,
165+ },
166+ & cli.StringFlag {
167+ Name : "filename" ,
168+ Value : "received.json" ,
169+ Usage : "file to move from uploads/ to share manager metadata blobs/" ,
170+ },
171+ & cli.BoolFlag {
172+ Name : "verbose" ,
173+ Value : false ,
174+ Usage : "Verbose logging enabled" ,
175+ },
176+ },
177+ Before : func (c * cli.Context ) error {
178+ // Parse base config to align with other shares subcommands; no config fields are required here
179+ if err := parser .ParseConfig (cfg , true ); err != nil {
180+ return configlog .ReturnError (err )
181+ }
182+ return nil
183+ },
184+ Action : func (c * cli.Context ) error {
185+ filename := c .String ("filename" )
186+ verbose := c .Bool ("verbose" )
187+
188+ dryRun := true
189+ if c .IsSet ("dry-run" ) {
190+ dryRun = c .Bool ("dry-run" )
191+ }
192+ if dryRun {
193+ fmt .Print ("Dry run mode enabled\n \n " )
194+ }
195+
196+ home , err := os .UserHomeDir ()
197+ if err != nil {
198+ return configlog .ReturnError (err )
199+ }
200+
201+ ocisHome := filepath .Join (home , ".ocis" )
202+ if c .IsSet ("ocis-home" ) {
203+ ocisHome = c .String ("ocis-home" )
204+ }
205+
206+ rootMetadata := filepath .Join (ocisHome , "storage" , "metadata" )
207+ rootMetadataBlobs := filepath .Join (rootMetadata , "spaces" , "js" , "oncs3-share-manager-metadata" )
208+
209+ fmt .Printf ("Scanning for missing blobs in: %s \n \n " , rootMetadataBlobs )
210+ missingBlobs , err := scanMissingBlobs (rootMetadataBlobs , filename )
211+ if err != nil {
212+ return err
213+ }
214+ if verbose {
215+ printJSON (missingBlobs , "missingBlobs" )
216+ }
217+
218+ if len (missingBlobs ) == 0 {
219+ fmt .Println ("No missing blobs found" )
220+ return nil
221+ }
222+
223+ rootMetadataUploads := filepath .Join (rootMetadata , "uploads" )
224+ fmt .Printf ("Found %d missing blobs. Restoring from %s\n " , len (missingBlobs ), rootMetadataUploads )
225+ remainingBlobIDs := restoreFromUploads (rootMetadataUploads , missingBlobs , dryRun )
226+
227+ if verbose {
228+ printJSON (remainingBlobIDs , "remainingBlobIDs" )
229+ }
230+
231+ return nil
232+ },
233+ }
234+ }
235+
236+ func printJSON (v any , label string ) {
237+ jbs , err := json .MarshalIndent (v , "" , " " )
238+ if err != nil {
239+ fmt .Printf ("Error marshalling JSON: %v\n " , err )
240+ return
241+ }
242+ fmt .Println (label , string (jbs ))
243+ }
244+
245+ // Scan for missing received.json blobs
246+ func scanMissingBlobs (rootMetadata , filename string ) (map [string ]string , error ) {
247+ missingBlobs := make (map [string ]string ) // blobID -> blobPathAbs
248+ nodesRoot := filepath .Join (rootMetadata , "nodes" )
249+
250+ _ = filepath .WalkDir (nodesRoot , func (path string , dir os.DirEntry , err error ) error {
251+ if err != nil || dir .IsDir () || filepath .Ext (path ) != ".mpk" {
252+ return nil
253+ }
254+ mpkBin , rerr := os .ReadFile (path )
255+ if rerr != nil {
256+ return nil
257+ }
258+ mpk := unmarshalMPK (mpkBin )
259+ if mpk ["user.ocis.name" ] != filename {
260+ return nil
261+ }
262+ blobID := mpk ["user.ocis.blobid" ]
263+ blobPathRel , ok := computeBlobPathRelative (blobID )
264+ if ! ok {
265+ return nil
266+ }
267+ blobPathAbs := filepath .Join (rootMetadata , blobPathRel )
268+ if _ , statErr := os .Stat (blobPathAbs ); statErr == nil {
269+ return nil
270+ }
271+ missingBlobs [blobID ] = blobPathAbs
272+ return nil
273+ })
274+
275+ return missingBlobs , nil
276+ }
277+
278+ // Attempt fast path restoration from uploads/ folder
279+ func restoreFromUploads (rootMetadataUploads string , missing map [string ]string , dryRun bool ) map [string ]bool {
280+ remainingBlobIDs := make (map [string ]bool )
281+
282+ for blobID , blobPathAbs := range missing {
283+ remainingBlobIDs [blobID ] = true
284+
285+ blobUploadsPath := filepath .Join (rootMetadataUploads , blobID )
286+ if dryRun {
287+ fmt .Printf (" DRY RUN: move %s to %s\n " , blobUploadsPath , blobPathAbs )
288+ continue
289+ }
290+
291+ // Check if the blob exists in the uploads folder and move it to the share manager metadata blobs/ folder
292+ if _ , err := os .Stat (blobUploadsPath ); err != nil {
293+ fmt .Printf (" Blob %s: not found in %s\n " , blobID , blobUploadsPath )
294+ continue
295+ }
296+ fmt .Printf (" Move %s to %s\n " , blobUploadsPath , blobPathAbs )
297+ if err := os .MkdirAll (filepath .Dir (blobPathAbs ), 0755 ); err != nil {
298+ fmt .Printf (" Warning: Failed to create dir: %v\n " , err )
299+ continue
300+ }
301+ if err := os .Rename (blobUploadsPath , blobPathAbs ); err != nil {
302+ fmt .Printf (" Warning: Failed to move blob: %v\n " , err )
303+ continue
304+ }
305+
306+ // Remove the info file after the blob is moved
307+ infoPath := blobUploadsPath + ".info"
308+ if _ , err := os .Stat (infoPath ); err != nil {
309+ fmt .Printf (" Info file %s: not found\n " , infoPath )
310+ continue
311+ }
312+ if err := os .Remove (infoPath ); err != nil {
313+ fmt .Printf (" Warning: Failed to remove info file: %v\n " , err )
314+ continue
315+ }
316+
317+ remainingBlobIDs [blobID ] = false
318+ }
319+
320+ return remainingBlobIDs
321+ }
322+
323+ func computeBlobPathRelative (bid string ) (string , bool ) {
324+ hyphen := strings .Index (bid , "-" )
325+ if hyphen < 0 || hyphen < 8 {
326+ return "" , false
327+ }
328+ prefix8 := bid [:hyphen ]
329+ if len (prefix8 ) < 8 {
330+ return "" , false
331+ }
332+ d1 , d2 , d3 , d4 := prefix8 [0 :2 ], prefix8 [2 :4 ], prefix8 [4 :6 ], prefix8 [6 :8 ]
333+ suffix := bid [hyphen :]
334+ return filepath .Join ("blobs" , d1 , d2 , d3 , d4 , suffix ), true
335+ }
336+
337+ func unmarshalMPK (bin []byte ) map [string ]string {
338+ keyValue := map [string ][]byte {}
339+ _ = msgpack .Unmarshal (bin , & keyValue )
340+ out := make (map [string ]string , len (keyValue ))
341+ for k , v := range keyValue {
342+ out [k ] = string (v )
343+ }
344+ return out
345+ }
0 commit comments