Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/restore: restore files in trash #3657

Merged
merged 9 commits into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/gc.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func gc(ctx *cli.Context) error {
edge := time.Now().Add(-time.Duration(format.TrashDays) * 24 * time.Hour)
if delete {
cleanTrashSpin := progress.AddCountSpinner("Cleaned trash")
m.CleanupTrashBefore(c, edge, cleanTrashSpin.Increment)
m.CleanupTrashBefore(c, edge, cleanTrashSpin.IncrBy)
cleanTrashSpin.Done()

cleanDetachedNodeSpin := progress.AddCountSpinner("Cleaned detached nodes")
Expand Down
1 change: 1 addition & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func Main(args []string) error {
cmdDestroy(),
cmdGC(),
cmdFsck(),
cmdRestore(),
cmdDump(),
cmdLoad(),
cmdVersion(),
Expand Down
129 changes: 129 additions & 0 deletions cmd/restore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package cmd

import (
"bytes"
"fmt"
"math/rand"
"os"
"strconv"
"sync"

"github.com/juicedata/juicefs/pkg/meta"
"github.com/juicedata/juicefs/pkg/utils"
"github.com/urfave/cli/v2"
)

func cmdRestore() *cli.Command {
return &cli.Command{
Name: "restore",
Action: restore,
Category: "ADMIN",
Usage: "restore files from trash",
ArgsUsage: "META HOUR ...",
Description: `
Rebuild the tree structure for trash files, and put them back to original directories.

Examples:
$ juicefs restore redis://localhost/1 2023-05-10-01`,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "put-back",
Usage: "move the recovered files into original directory",
},
&cli.IntFlag{
Name: "threads",
Value: 10,
Usage: "number of threads",
},
},
}
}

func restore(ctx *cli.Context) error {
setup(ctx, 2)
if os.Getuid() != 0 {
return fmt.Errorf("only root can restore files from trash")
}
removePassword(ctx.Args().Get(0))
m := meta.NewClient(ctx.Args().Get(0), nil)
_, err := m.Load(true)
if err != nil {
return err
}
for i := 1; i < ctx.NArg(); i++ {
hour := ctx.Args().Get(i)
doRestore(m, hour, ctx.Bool("put-back"), ctx.Int("threads"))
}
return nil
}

func doRestore(m meta.Meta, hour string, putBack bool, threads int) {
logger.Infof("restore files in %s ...", hour)
ctx := meta.Background
var parent meta.Ino
var attr meta.Attr
err := m.Lookup(ctx, meta.TrashInode, hour, &parent, &attr, false)
if err != 0 {
logger.Errorf("lookup %s: %s", hour, err)
return
}
var entries []*meta.Entry
err = m.Readdir(meta.Background, parent, 0, &entries)
if err != 0 {
logger.Errorf("list %s: %s", hour, err)
return
}
entries = entries[2:]
// to avoid conflict
rand.Shuffle(len(entries), func(i, j int) {
entries[i], entries[j] = entries[j], entries[i]
})

var parents = make(map[meta.Ino]bool)
if !putBack {
for _, e := range entries {
if e.Attr.Typ == meta.TypeDirectory {
parents[e.Inode] = true
}
}
}

todo := make(chan *meta.Entry, 1000)
p := utils.NewProgress(false)
restored := p.AddCountBar("restored", int64(len(entries)))
skipped := p.AddCountSpinner("skipped")
failed := p.AddCountSpinner("failed")
var wg sync.WaitGroup
for i := 0; i < threads; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for e := range todo {
ps := bytes.SplitN(e.Name, []byte("-"), 3)
dst, _ := strconv.Atoi(string(ps[0]))
if putBack || parents[meta.Ino(dst)] {
err = m.Rename(ctx, parent, string(e.Name), meta.Ino(dst), string(ps[2]), meta.RenameNoReplace, nil, nil)
if err != 0 {
logger.Warnf("restore %s: %s", string(e.Name), err)
failed.Increment()
} else {
restored.Increment()
}
} else {
skipped.Increment()
}
}
}()
}

for _, e := range entries {
todo <- e
}
close(todo)
wg.Wait()
failed.Done()
skipped.Done()
restored.Done()
p.Done()
logger.Infof("restored %d files in %s", restored.Current(), hour)
}
2 changes: 2 additions & 0 deletions docs/en/security/trash.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ When JuiceFS Client is started by a non-root user, add the `-o allow_root` optio

Recover/Purge files in trash are only available for root users, simply use `mv` command to recover a file, or use `rm` to permanently delete a file. Normal users, however, can only recover a file by reading its content and write it to a new file.

With JuiceFS 1.1, you can rebuild the tree structure for all files under given hour in trash, then restore a single directory using `mv` or all of them by using `--put-back`, which move all the files and directories to the place they were deleted (will not overwrite new created files).

JuiceFS Client is in charge of periodically checking trash and expire old entries (run every hour by default), so you need at least one active client mounted (without [`--no-bgjob`](../reference/command_reference.md#mount)). If you wish to quickly free up object storage, you can manually delete files in the `.trash` directory using the `rm` command.

Furthermore, garbage blocks created by file overwrites are not visible to users, if you must force delete them, you'll have to temporarily disable trash (setting [`--trash-days 0`](#configure)), and then manually run garbage collection using [`juicefs gc`](../reference/command_reference.md#gc). Remember to re-enable trash after done.
2 changes: 2 additions & 0 deletions docs/zh_cn/security/trash.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ mv .trash/2022-11-30-10/[parent inode]-[file inode]-[file name] .

只有 root 用户具有回收站目录的写权限,在 root 用户下,可以用 `mv` 的命令将文件移出回收站来恢复删掉的文件,或者用 `rm` 将文件从回收站彻底删除。普通用户则没有回收站的写权限,无法方便地使用 `mv`、`rm` 等命令,如果要从回收站恢复文件,只能读取(有访问权限的)文件,再写入到新文件。

JuiceFS 1.1 版本提供了 restore 子命令来快速恢复大量误删的文件,它会把指定的某个小时的回收站中的文件按照被删除前的目录结构组织起来,方便手动按照目录恢复,或者使用 `--put-back` 参数将所有文件和目录恢复到删除前的位置(不会覆盖新创建的文件)。

回收站的清理由 JuiceFS 客户端定期运行后台任务执行(默认每小时清理一次),因此需要至少有 1 个在线的挂载点(不能开启 [`--no-bgjob`](../reference/command_reference.md#mount))。如果你希望尽快释放对象存储空间,也可以手动强制清理,以 root 身份在 `.trash` 目录执行 `rm` 命令即可。

值得一提,覆写产生的文件碎片由于对用户不可见,所以无法轻易强制删除。如果你确实想要主动清理它们,可以临时禁用回收站(设置 [`--trash-days 0`](#configure)),再通过 [`juicefs gc`](../reference/command_reference.md#gc) 命令将这些数据块标为泄漏并删除。操作完成以后,记得重新开启回收站。
13 changes: 5 additions & 8 deletions pkg/meta/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -2175,7 +2175,7 @@ func (m *baseMeta) CleanupDetachedNodesBefore(ctx Context, edge time.Time, incre
}
}

func (m *baseMeta) CleanupTrashBefore(ctx Context, edge time.Time, increProgress func()) {
func (m *baseMeta) CleanupTrashBefore(ctx Context, edge time.Time, increProgress func(int)) {
logger.Debugf("cleanup trash: started")
now := time.Now()
var st syscall.Errno
Expand Down Expand Up @@ -2214,15 +2214,12 @@ func (m *baseMeta) CleanupTrashBefore(ctx Context, edge time.Time, increProgress
entries = entries[1:]
}
for _, se := range subEntries {
if se.Attr.Typ == TypeDirectory {
st = m.en.doRmdir(ctx, e.Inode, string(se.Name), nil)
} else {
st = m.en.doUnlink(ctx, e.Inode, string(se.Name), nil)
}
var c uint64
st = m.Remove(ctx, e.Inode, string(se.Name), &c)
if st == 0 {
count++
count += int(c)
if increProgress != nil {
increProgress()
increProgress(int(c))
}
} else {
logger.Warnf("delete from trash %s/%s: %s", e.Name, se.Name, st)
Expand Down
2 changes: 1 addition & 1 deletion pkg/meta/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ type Meta interface {
// CleanStaleSessions cleans up sessions not active for more than 5 minutes
CleanStaleSessions()
// CleanupTrashBefore deletes all files in trash before the given time.
CleanupTrashBefore(ctx Context, edge time.Time, increProgress func())
CleanupTrashBefore(ctx Context, edge time.Time, increProgress func(int))
// CleanupDetachedNodesBefore deletes all detached nodes before the given time.
CleanupDetachedNodesBefore(ctx Context, edge time.Time, increProgress func())

Expand Down
7 changes: 6 additions & 1 deletion pkg/meta/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -1583,6 +1583,11 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
var dtyp uint8
var tattr Attr
var newSpace, newInode int64
keys := []string{m.inodeKey(parentSrc), m.entryKey(parentSrc), m.inodeKey(parentDst), m.entryKey(parentDst)}
if isTrash(parentSrc) {
// lock the parentDst
keys[0], keys[2] = keys[2], keys[0]
}
err := m.txn(ctx, func(tx *redis.Tx) error {
opened = false
dino, dtyp = 0, 0
Expand Down Expand Up @@ -1844,7 +1849,7 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
return nil
})
return err
}, m.inodeKey(parentSrc), m.entryKey(parentSrc), m.inodeKey(parentDst), m.entryKey(parentDst))
}, keys...)
if err == nil && !exchange && trash == 0 {
if dino > 0 && dtyp == TypeFile && tattr.Nlink == 0 {
m.fileDeleted(opened, false, dino, tattr.Length)
Expand Down
6 changes: 5 additions & 1 deletion pkg/meta/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,10 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
var dino Ino
var dn node
var newSpace, newInode int64
lockParent := parentSrc
if isTrash(lockParent) {
lockParent = parentDst
}
err := m.txn(func(s *xorm.Session) error {
opened = false
dino = 0
Expand Down Expand Up @@ -1879,7 +1883,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
}
}
return err
}, parentSrc)
}, lockParent)
if err == nil && !exchange && trash == 0 {
if dino > 0 && dn.Type == TypeFile && dn.Nlink == 0 {
m.fileDeleted(opened, false, dino, dn.Length)
Expand Down
6 changes: 5 additions & 1 deletion pkg/meta/tkv.go
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,10 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
var dtyp uint8
var tattr Attr
var newSpace, newInode int64
lockParent := parentSrc
if isTrash(lockParent) {
lockParent = parentDst
}
err := m.txn(func(tx *kvTxn) error {
opened = false
dino, dtyp = 0, 0
Expand Down Expand Up @@ -1667,7 +1671,7 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
tx.set(m.inodeKey(parentDst), m.marshal(&dattr))
}
return nil
}, parentSrc)
}, lockParent)
if err == nil && !exchange && trash == 0 {
if dino > 0 && dtyp == TypeFile && tattr.Nlink == 0 {
m.fileDeleted(opened, false, dino, tattr.Length)
Expand Down