Add /rest/tree API call

This commit is contained in:
Audrius Butkevicius 2015-02-07 10:52:42 +00:00
parent 9876d93b60
commit 9d1e2d9f46
5 changed files with 516 additions and 6 deletions

View File

@ -130,6 +130,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
getRestMux.HandleFunc("/rest/system", restGetSystem)
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
getRestMux.HandleFunc("/rest/version", restGetVersion)
getRestMux.HandleFunc("/rest/tree", withModel(m, restGetTree))
getRestMux.HandleFunc("/rest/stats/device", withModel(m, restGetDeviceStats))
getRestMux.HandleFunc("/rest/stats/folder", withModel(m, restGetFolderStats))
@ -262,6 +263,24 @@ func restGetVersion(w http.ResponseWriter, r *http.Request) {
})
}
func restGetTree(m *model.Model, w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
folder := qs.Get("folder")
prefix := qs.Get("prefix")
dirsonly := qs.Get("dirsonly") != ""
levels, err := strconv.Atoi(qs.Get("levels"))
if err != nil {
levels = -1
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
tree := m.GlobalDirectoryTree(folder, prefix, levels, dirsonly)
json.NewEncoder(w).Encode(tree)
}
func restGetCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var folder = qs.Get("folder")

View File

@ -709,11 +709,9 @@ func ldbGetGlobal(db *leveldb.DB, folder, file []byte, truncate bool) (FileIntf,
return fi, true
}
func ldbWithGlobal(db *leveldb.DB, folder []byte, truncate bool, fn Iterator) {
func ldbWithGlobal(db *leveldb.DB, folder, prefix []byte, truncate bool, fn Iterator) {
runtime.GC()
start := globalKey(folder, nil)
limit := globalKey(folder, []byte{0xff, 0xff, 0xff, 0xff})
snap, err := db.GetSnapshot()
if err != nil {
panic(err)
@ -728,7 +726,7 @@ func ldbWithGlobal(db *leveldb.DB, folder []byte, truncate bool, fn Iterator) {
snap.Release()
}()
dbi := snap.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
dbi := snap.NewIterator(util.BytesPrefix(globalKey(folder, prefix)), nil)
defer dbi.Release()
for dbi.Next() {

View File

@ -172,14 +172,21 @@ func (s *FileSet) WithGlobal(fn Iterator) {
if debug {
l.Debugf("%s WithGlobal()", s.folder)
}
ldbWithGlobal(s.db, []byte(s.folder), false, nativeFileIterator(fn))
ldbWithGlobal(s.db, []byte(s.folder), nil, false, nativeFileIterator(fn))
}
func (s *FileSet) WithGlobalTruncated(fn Iterator) {
if debug {
l.Debugf("%s WithGlobalTruncated()", s.folder)
}
ldbWithGlobal(s.db, []byte(s.folder), true, nativeFileIterator(fn))
ldbWithGlobal(s.db, []byte(s.folder), nil, true, nativeFileIterator(fn))
}
func (s *FileSet) WithPrefixedGlobalTruncated(prefix string, fn Iterator) {
if debug {
l.Debugf("%s WithPrefixedGlobalTruncated()", s.folder, prefix)
}
ldbWithGlobal(s.db, []byte(s.folder), []byte(osutil.NormalizedFilename(prefix)), true, nativeFileIterator(fn))
}
func (s *FileSet) Get(device protocol.DeviceID, file string) (protocol.FileInfo, bool) {

View File

@ -1417,6 +1417,69 @@ func (m *Model) RemoteLocalVersion(folder string) int64 {
return ver
}
func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{} {
m.fmut.RLock()
files, ok := m.folderFiles[folder]
m.fmut.RUnlock()
if !ok {
return nil
}
output := make(map[string]interface{})
sep := string(filepath.Separator)
prefix = osutil.NativeFilename(prefix)
if prefix != "" && !strings.HasSuffix(prefix, sep) {
prefix = prefix + sep
}
files.WithPrefixedGlobalTruncated(prefix, func(fi db.FileIntf) bool {
f := fi.(db.FileInfoTruncated)
if f.IsInvalid() || f.IsDeleted() || f.Name == prefix {
return true
}
f.Name = strings.Replace(f.Name, prefix, "", 1)
var dir, base string
if f.IsDirectory() && !f.IsSymlink() {
dir = f.Name
} else {
dir = filepath.Dir(f.Name)
base = filepath.Base(f.Name)
}
if levels > -1 && strings.Count(f.Name, sep) > levels {
return true
}
last := output
if dir != "." {
for _, path := range strings.Split(dir, sep) {
directory, ok := last[path]
if !ok {
newdir := make(map[string]interface{})
last[path] = newdir
last = newdir
} else {
last = directory.(map[string]interface{})
}
}
}
if !dirsonly && base != "" {
last[base] = []int64{
f.Modified, f.Size(),
}
}
return true
})
return output
}
func (m *Model) availability(folder, file string) []protocol.DeviceID {
// Acquire this lock first, as the value returned from foldersFiles can
// get heavily modified on Close()

View File

@ -17,8 +17,11 @@ package model
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
"time"
@ -580,3 +583,423 @@ func TestRefuseUnknownBits(t *testing.T) {
t.Error("Valid file not found or name mismatch", ok, f)
}
}
func TestGlobalDirectoryTree(t *testing.T) {
fcfg := config.FolderConfiguration{
ID: "default",
Path: "testdata",
Devices: []config.FolderDeviceConfiguration{
{
DeviceID: device1,
},
},
}
cfg := config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
Devices: []config.DeviceConfiguration{
{
DeviceID: device1,
},
},
}
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
m := NewModel(config.Wrap("/tmp/test", cfg), "device", "syncthing", "dev", db)
m.AddFolder(fcfg)
b := func(isfile bool, path ...string) protocol.FileInfo {
var flags uint32 = protocol.FlagDirectory
blocks := []protocol.BlockInfo{}
if isfile {
flags = 0
blocks = []protocol.BlockInfo{{Offset: 0x0, Size: 0xa, Hash: []uint8{0x2f, 0x72, 0xcc, 0x11, 0xa6, 0xfc, 0xd0, 0x27, 0x1e, 0xce, 0xf8, 0xc6, 0x10, 0x56, 0xee, 0x1e, 0xb1, 0x24, 0x3b, 0xe3, 0x80, 0x5b, 0xf9, 0xa9, 0xdf, 0x98, 0xf9, 0x2f, 0x76, 0x36, 0xb0, 0x5c}}}
}
return protocol.FileInfo{
Name: filepath.Join(path...),
Flags: flags,
Modified: 0x666,
Blocks: blocks,
}
}
filedata := []int64{0x666, 0xa}
testdata := []protocol.FileInfo{
b(false, "another"),
b(false, "another", "directory"),
b(true, "another", "directory", "afile"),
b(false, "another", "directory", "with"),
b(false, "another", "directory", "with", "a"),
b(true, "another", "directory", "with", "a", "file"),
b(true, "another", "directory", "with", "file"),
b(true, "another", "file"),
b(false, "other"),
b(false, "other", "rand"),
b(false, "other", "random"),
b(false, "other", "random", "dir"),
b(false, "other", "random", "dirx"),
b(false, "other", "randomx"),
b(false, "some"),
b(false, "some", "directory"),
b(false, "some", "directory", "with"),
b(false, "some", "directory", "with", "a"),
b(true, "some", "directory", "with", "a", "file"),
b(true, "rootfile"),
}
expectedResult := map[string]interface{}{
"another": map[string]interface{}{
"directory": map[string]interface{}{
"afile": filedata,
"with": map[string]interface{}{
"a": map[string]interface{}{
"file": filedata,
},
"file": filedata,
},
},
"file": filedata,
},
"other": map[string]interface{}{
"rand": map[string]interface{}{},
"random": map[string]interface{}{
"dir": map[string]interface{}{},
"dirx": map[string]interface{}{},
},
"randomx": map[string]interface{}{},
},
"some": map[string]interface{}{
"directory": map[string]interface{}{
"with": map[string]interface{}{
"a": map[string]interface{}{
"file": filedata,
},
},
},
},
"rootfile": filedata,
}
mm := func(data interface{}) string {
bytes, err := json.Marshal(data)
if err != nil {
panic(err)
}
return string(bytes)
}
m.Index(device1, "default", testdata)
result := m.GlobalDirectoryTree("default", "", -1, false)
if !reflect.DeepEqual(result, expectedResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(expectedResult))
}
result = m.GlobalDirectoryTree("default", "another", -1, false)
if !reflect.DeepEqual(result, expectedResult["another"]) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(expectedResult["another"]))
}
result = m.GlobalDirectoryTree("default", "", 0, false)
currentResult := map[string]interface{}{
"another": map[string]interface{}{},
"other": map[string]interface{}{},
"some": map[string]interface{}{},
"rootfile": filedata,
}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
result = m.GlobalDirectoryTree("default", "", 1, false)
currentResult = map[string]interface{}{
"another": map[string]interface{}{
"directory": map[string]interface{}{},
"file": filedata,
},
"other": map[string]interface{}{
"rand": map[string]interface{}{},
"random": map[string]interface{}{},
"randomx": map[string]interface{}{},
},
"some": map[string]interface{}{
"directory": map[string]interface{}{},
},
"rootfile": filedata,
}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
result = m.GlobalDirectoryTree("default", "", -1, true)
currentResult = map[string]interface{}{
"another": map[string]interface{}{
"directory": map[string]interface{}{
"with": map[string]interface{}{
"a": map[string]interface{}{},
},
},
},
"other": map[string]interface{}{
"rand": map[string]interface{}{},
"random": map[string]interface{}{
"dir": map[string]interface{}{},
"dirx": map[string]interface{}{},
},
"randomx": map[string]interface{}{},
},
"some": map[string]interface{}{
"directory": map[string]interface{}{
"with": map[string]interface{}{
"a": map[string]interface{}{},
},
},
},
}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
result = m.GlobalDirectoryTree("default", "", 1, true)
currentResult = map[string]interface{}{
"another": map[string]interface{}{
"directory": map[string]interface{}{},
},
"other": map[string]interface{}{
"rand": map[string]interface{}{},
"random": map[string]interface{}{},
"randomx": map[string]interface{}{},
},
"some": map[string]interface{}{
"directory": map[string]interface{}{},
},
}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
result = m.GlobalDirectoryTree("default", "another", 0, false)
currentResult = map[string]interface{}{
"directory": map[string]interface{}{},
"file": filedata,
}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
result = m.GlobalDirectoryTree("default", "some/directory", 0, false)
currentResult = map[string]interface{}{
"with": map[string]interface{}{},
}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
result = m.GlobalDirectoryTree("default", "some/directory", 1, false)
currentResult = map[string]interface{}{
"with": map[string]interface{}{
"a": map[string]interface{}{},
},
}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
result = m.GlobalDirectoryTree("default", "some/directory", 2, false)
currentResult = map[string]interface{}{
"with": map[string]interface{}{
"a": map[string]interface{}{
"file": filedata,
},
},
}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
result = m.GlobalDirectoryTree("default", "another", -1, true)
currentResult = map[string]interface{}{
"directory": map[string]interface{}{
"with": map[string]interface{}{
"a": map[string]interface{}{},
},
},
}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
// No prefix matching!
result = m.GlobalDirectoryTree("default", "som", -1, false)
currentResult = map[string]interface{}{}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
}
func TestGlobalDirectorySelfFixing(t *testing.T) {
fcfg := config.FolderConfiguration{
ID: "default",
Path: "testdata",
Devices: []config.FolderDeviceConfiguration{
{
DeviceID: device1,
},
},
}
cfg := config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
Devices: []config.DeviceConfiguration{
{
DeviceID: device1,
},
},
}
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
m := NewModel(config.Wrap("/tmp/test", cfg), "device", "syncthing", "dev", db)
m.AddFolder(fcfg)
b := func(isfile bool, path ...string) protocol.FileInfo {
var flags uint32 = protocol.FlagDirectory
blocks := []protocol.BlockInfo{}
if isfile {
flags = 0
blocks = []protocol.BlockInfo{{Offset: 0x0, Size: 0xa, Hash: []uint8{0x2f, 0x72, 0xcc, 0x11, 0xa6, 0xfc, 0xd0, 0x27, 0x1e, 0xce, 0xf8, 0xc6, 0x10, 0x56, 0xee, 0x1e, 0xb1, 0x24, 0x3b, 0xe3, 0x80, 0x5b, 0xf9, 0xa9, 0xdf, 0x98, 0xf9, 0x2f, 0x76, 0x36, 0xb0, 0x5c}}}
}
return protocol.FileInfo{
Name: filepath.Join(path...),
Flags: flags,
Modified: 0x666,
Blocks: blocks,
}
}
filedata := []int64{0x666, 0xa}
testdata := []protocol.FileInfo{
b(true, "another", "directory", "afile"),
b(true, "another", "directory", "with", "a", "file"),
b(true, "another", "directory", "with", "file"),
b(false, "other", "random", "dirx"),
b(false, "other", "randomx"),
b(false, "some", "directory", "with", "x"),
b(true, "some", "directory", "with", "a", "file"),
b(false, "this", "is", "a", "deep", "invalid", "directory"),
b(true, "xthis", "is", "a", "deep", "invalid", "file"),
}
expectedResult := map[string]interface{}{
"another": map[string]interface{}{
"directory": map[string]interface{}{
"afile": filedata,
"with": map[string]interface{}{
"a": map[string]interface{}{
"file": filedata,
},
"file": filedata,
},
},
},
"other": map[string]interface{}{
"random": map[string]interface{}{
"dirx": map[string]interface{}{},
},
"randomx": map[string]interface{}{},
},
"some": map[string]interface{}{
"directory": map[string]interface{}{
"with": map[string]interface{}{
"a": map[string]interface{}{
"file": filedata,
},
"x": map[string]interface{}{},
},
},
},
"this": map[string]interface{}{
"is": map[string]interface{}{
"a": map[string]interface{}{
"deep": map[string]interface{}{
"invalid": map[string]interface{}{
"directory": map[string]interface{}{},
},
},
},
},
},
"xthis": map[string]interface{}{
"is": map[string]interface{}{
"a": map[string]interface{}{
"deep": map[string]interface{}{
"invalid": map[string]interface{}{
"file": filedata,
},
},
},
},
},
}
mm := func(data interface{}) string {
bytes, err := json.Marshal(data)
if err != nil {
panic(err)
}
return string(bytes)
}
m.Index(device1, "default", testdata)
result := m.GlobalDirectoryTree("default", "", -1, false)
if !reflect.DeepEqual(result, expectedResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(expectedResult))
}
result = m.GlobalDirectoryTree("default", "xthis/is/a/deep", -1, false)
currentResult := map[string]interface{}{
"invalid": map[string]interface{}{
"file": filedata,
},
}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
result = m.GlobalDirectoryTree("default", "xthis/is/a/deep", -1, true)
currentResult = map[string]interface{}{
"invalid": map[string]interface{}{},
}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
// !!! This is actually BAD, because we don't have enough level allowance
// to accept this file, hence the tree is left unbuilt !!!
result = m.GlobalDirectoryTree("default", "xthis", 1, false)
currentResult = map[string]interface{}{}
if !reflect.DeepEqual(result, currentResult) {
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
}
}