Skip to content

Commit 340efcc

Browse files
authored
Merge pull request #11762 from owncloud/feat/ocisdev-439-v2
feat: [OCISDEV-439] CLI, shares move-stuck-upload-blobs
2 parents eb13cbe + 1e4ed4f commit 340efcc

File tree

2 files changed

+232
-10
lines changed

2 files changed

+232
-10
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Enhancement: Add Cli to move stuck uploads
2+
3+
In some cases of saturated disk usage ocis metadata may get stuck. This command relieves this case.
4+
5+
https://github.com/owncloud/ocis/pull/11762

ocis/pkg/command/shares.go

Lines changed: 227 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package command
22

33
import (
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

Comments
 (0)