feat(ocis): Add backup consistency command

Signed-off-by: jkoberg <jkoberg@owncloud.com>
Co-authored-by: dragonchaser <crichter@owncloud.com>
This commit is contained in:
jkoberg
2024-05-22 16:08:51 +02:00
parent 254c9d7c26
commit 8a08c9f9b8
3 changed files with 257 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
// Package backup contains ocis backup functionality.
package backup
import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node"
"github.com/shamaton/msgpack/v2"
)
// Inconsistency describes the type of incosistency
type Inconsistency string
var (
InconsistencyBlobMissing Inconsistency = "blob missing"
InconsistencyBlobOrphaned Inconsistency = "blob orphaned"
InconsistencyNodeMissing Inconsistency = "node missing"
InconsistencyMetadataMissing Inconsistency = "metadata missing"
InconsistencySymlinkMissing Inconsistency = "symlink missing"
)
// Consistency holds the node and blob data of a space
type Consistency struct {
Nodes map[string]Inconsistency
Links map[string]Inconsistency
Blobs map[string]Inconsistency
fsys fs.FS
discpath string
}
// ListBlobstore required to check blob consistency
type ListBlobstore interface {
Upload(node *node.Node, source string) error
Download(node *node.Node) (io.ReadCloser, error)
Delete(node *node.Node) error
List() ([]string, error)
}
// New creates a new Consistency object
func New(fsys fs.FS, discpath string) *Consistency {
return &Consistency{
Nodes: make(map[string]Inconsistency),
Links: make(map[string]Inconsistency),
Blobs: make(map[string]Inconsistency),
fsys: fsys,
discpath: discpath,
}
}
// CheckSpaceConsistency checks the consistency of a space
func CheckSpaceConsistency(pathToSpace string, lbs ListBlobstore) error {
fsys := os.DirFS(pathToSpace)
con := New(fsys, pathToSpace)
err := con.Initialize()
if err != nil {
return err
}
for n := range con.Nodes {
if _, ok := con.Links[n]; !ok {
// TODO: This is no inconsistency if
// * this is the spaceroot
// * this is a trashed node
con.Nodes[n] = InconsistencySymlinkMissing
continue
}
delete(con.Links, n)
delete(con.Nodes, n)
}
for l := range con.Links {
con.Links[l] = InconsistencyNodeMissing
}
blobs, err := lbs.List()
if err != nil {
return err
}
deadBlobs := make(map[string]Inconsistency)
for _, b := range blobs {
if _, ok := con.Blobs[b]; !ok {
deadBlobs[b] = InconsistencyBlobOrphaned
continue
}
delete(con.Blobs, b)
delete(deadBlobs, b)
}
for b := range con.Blobs {
con.Blobs[b] = InconsistencyBlobMissing
}
// get blobs from blobstore.
// initialize blobstore (s3, local)
// compare con.Blobs with blobstore.GetBlobs()
for n := range con.Nodes {
fmt.Println("Inconsistency", n, con.Nodes[n])
}
for l := range con.Links {
fmt.Println("Inconsistency", l, con.Links[l])
}
for b := range con.Blobs {
fmt.Println("Inconsistency", b, con.Blobs[b])
}
for b := range deadBlobs {
fmt.Println("Inconsistency", b, deadBlobs[b])
}
return nil
}
func (c *Consistency) Initialize() error {
dirs, err := fs.Glob(c.fsys, "nodes/*/*/*/*")
if err != nil {
return err
}
for _, d := range dirs {
entries, err := fs.ReadDir(c.fsys, d)
if err != nil {
return err
}
for _, e := range entries {
switch {
case e.IsDir():
ls, err := fs.ReadDir(c.fsys, filepath.Join(d, e.Name()))
if err != nil {
fmt.Println("error reading dir", err)
continue
}
for _, l := range ls {
p, err := filepath.EvalSymlinks(filepath.Join(c.discpath, d, e.Name(), l.Name()))
if err != nil {
fmt.Println("error evaluating symlink", filepath.Join(d, e.Name(), l.Name()), err)
continue
}
c.Links[p] = ""
}
case filepath.Ext(e.Name()) == ".mpk":
inc, err := c.checkNode(filepath.Join(d, e.Name()))
if err != nil {
fmt.Println("error checking node", err)
continue
}
c.Nodes[filepath.Join(c.discpath, d, strings.TrimSuffix(e.Name(), ".mpk"))] = inc
default:
// fmt.Println("unknown", e.Name())
}
}
}
return nil
}
func (c *Consistency) checkNode(path string) (Inconsistency, error) {
b, err := fs.ReadFile(c.fsys, path)
if err != nil {
return "", err
}
m := map[string][]byte{}
if err := msgpack.Unmarshal(b, &m); err != nil {
return "", err
}
if bid := m["user.ocis.blobid"]; string(bid) != "" {
c.Blobs[string(bid)] = ""
}
return "", nil
}
func iterate(fsys fs.FS, path string, d *Consistency) ([]string, error) {
// open symlink -> NodeMissing
// remove node from data.Nodes
// check blob -> BlobMissing
// remove blob from data.Blobs
// list children (symlinks!)
// return children (symlinks!)
return nil, nil
}
+63
View File
@@ -0,0 +1,63 @@
package command
import (
"fmt"
"path/filepath"
"github.com/cs3org/reva/v2/pkg/storage/fs/ocis/blobstore"
"github.com/owncloud/ocis/v2/ocis-pkg/config"
"github.com/owncloud/ocis/v2/ocis/pkg/backup"
"github.com/owncloud/ocis/v2/ocis/pkg/register"
"github.com/urfave/cli/v2"
)
// BackupCommand is the entrypoint for the backup command
func BackupCommand(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "backup",
Usage: "ocis backup functionality",
Subcommands: []*cli.Command{
ConsistencyCommand(cfg),
},
Action: func(c *cli.Context) error {
fmt.Println("Read the docs")
return nil
},
}
}
// ConsistencyCommand is the entrypoint for the consistency Command
func ConsistencyCommand(_ *config.Config) *cli.Command {
return &cli.Command{
Name: "consistency",
Usage: "check backup consistency",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "blobstore",
Aliases: []string{"b"},
Usage: "the blobstore type. Can be (ocis, s3ng). Default ocis",
},
},
Action: func(c *cli.Context) error {
basePath := "/home/jkoberg/.ocis/storage/users"
// TODO: switch for s3ng blobstore
bs, err := blobstore.New(basePath)
if err != nil {
fmt.Println(err)
return err
}
if err := backup.CheckSpaceConsistency(filepath.Join(basePath, "spaces/23/ebf113-76d4-43c0-8594-df974b02cd74"), bs); err != nil {
fmt.Println(err)
return err
}
return nil
},
}
}
func init() {
register.AddCommand(BackupCommand)
}