diff --git a/frontend/src/dialog/account/p-account-edit-dialog.vue b/frontend/src/dialog/account/p-account-edit-dialog.vue index d7b0dd9d8..839c63b31 100644 --- a/frontend/src/dialog/account/p-account-edit-dialog.vue +++ b/frontend/src/dialog/account/p-account-edit-dialog.vue @@ -80,8 +80,8 @@ :search-input.sync="search" :items="pathItems" :loading="loading" - item-text="text" - item-value="value" + item-text="abs" + item-value="abs" :label="label.SharePath" :disabled="!model.AccShare || loading" > @@ -145,8 +145,8 @@ :search-input.sync="search" :items="pathItems" :loading="loading" - item-text="text" - item-value="value" + item-text="abs" + item-value="abs" :label="label.SyncPath" :disabled="!model.AccSync || loading" > @@ -317,7 +317,7 @@ search: null, path: "/", paths: [ - {"text": "/", "value": "/"} + {"abs": "/"} ], pathItems: [], newPath: "", @@ -390,8 +390,7 @@ } } }, - computed: { - }, + computed: {}, methods: { cancel() { this.$emit('cancel'); @@ -437,12 +436,12 @@ return result; }, onChange() { - this.paths = [{"text": "/", "value": "/"}]; + this.paths = [{"abs": "/"}]; this.loading = true; - this.model.Ls().then(l => { - for (let i = 0; i < l.length; i++) { - this.paths.push({"text": l[i], "value": l[i]}); + this.model.Dirs().then(p => { + for (let i = 0; i < p.length; i++) { + this.paths.push(p[i]); } this.pathItems = [...this.paths]; @@ -461,11 +460,11 @@ this.newPath = ""; } else { this.newPath = q; - this.pathItems = this.paths.concat([{"text": q, "value": q}]); + this.pathItems = this.paths.concat([{"abs": q}]); } }, show: function (show) { - if(show) { + if (show) { this.onChange(); } } diff --git a/frontend/src/dialog/p-photo-share-dialog.vue b/frontend/src/dialog/p-photo-share-dialog.vue index cc5567c4a..fa48c5e3d 100644 --- a/frontend/src/dialog/p-photo-share-dialog.vue +++ b/frontend/src/dialog/p-photo-share-dialog.vue @@ -32,8 +32,8 @@ :items="pathItems" :loading="loading" :disabled="loading || noAccounts" - item-text="text" - item-value="value" + item-text="abs" + item-value="abs" :label="labels.path" > @@ -74,7 +74,7 @@ accounts: [], path: "/", paths: [ - {"text": "/", "value": "/"} + {"abs": "/"} ], pathItems: [], newPath: "", @@ -104,7 +104,7 @@ (files) => { this.loading = false; - if(files.length === 1) { + if (files.length === 1) { this.$notify.success("One photo shared"); } else { this.$notify.success(files.length + " photos shared"); @@ -115,13 +115,12 @@ ).catch(() => this.loading = false); }, onChange() { - this.paths = [{"text": "/", "value": "/"}]; + this.paths = [{"abs": "/"}]; this.loading = true; - this.account.Ls().then(l => { - console.log("result", l); - for (let i = 0; i < l.length; i++) { - this.paths.push({"text": l[i], "value": l[i]}); + this.account.Dirs().then(p => { + for (let i = 0; i < p.length; i++) { + this.paths.push(p[i]); } this.pathItems = [...this.paths]; @@ -160,7 +159,7 @@ this.newPath = ""; } else { this.newPath = q; - this.pathItems = this.paths.concat([{"text": q, "value": q}]); + this.pathItems = this.paths.concat([{"abs": q}]); } }, show: function (show) { diff --git a/frontend/src/model/account.js b/frontend/src/model/account.js index be048b1f1..cc205b9af 100644 --- a/frontend/src/model/account.js +++ b/frontend/src/model/account.js @@ -46,20 +46,8 @@ class Account extends Abstract { return this.ID; } - toggleShare() { - const values = { AccShare: !this.AccShare }; - - return Api.put(this.getEntityResource(), values).then((response) => Promise.resolve(this.setValues(response.data))); - } - - toggleSync() { - const values = { AccSync: !this.AccSync }; - - return Api.put(this.getEntityResource(), values).then((response) => Promise.resolve(this.setValues(response.data))); - } - - Ls() { - return Api.get(this.getEntityResource() + "/ls").then((response) => Promise.resolve(response.data)); + Dirs() { + return Api.get(this.getEntityResource() + "/dirs").then((response) => Promise.resolve(response.data)); } Share(UUIDs, dest) { diff --git a/internal/api/account.go b/internal/api/account.go index 0e2086eb7..d199f1d1c 100644 --- a/internal/api/account.go +++ b/internal/api/account.go @@ -69,12 +69,12 @@ func GetAccount(router *gin.RouterGroup, conf *config.Config) { }) } -// GET /api/v1/accounts/:id/ls +// GET /api/v1/accounts/:id/dirs // // Parameters: // id: string Account ID as returned by the API -func LsAccount(router *gin.RouterGroup, conf *config.Config) { - router.GET("/accounts/:id/ls", func(c *gin.Context) { +func GetAccountDirs(router *gin.RouterGroup, conf *config.Config) { + router.GET("/accounts/:id/dirs", func(c *gin.Context) { if Unauthorized(c, conf) { c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) return @@ -90,7 +90,7 @@ func LsAccount(router *gin.RouterGroup, conf *config.Config) { return } - list, err := m.Ls() + list, err := m.Directories() if err != nil { log.Errorf("account: %s", err.Error()) diff --git a/internal/entity/account.go b/internal/entity/account.go index d531aebbf..3c16f7bae 100644 --- a/internal/entity/account.go +++ b/internal/entity/account.go @@ -9,6 +9,7 @@ import ( "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/internal/service/webdav" + "github.com/photoprism/photoprism/pkg/fs" "github.com/ulule/deepcopier" ) @@ -83,14 +84,14 @@ func (m *Account) Delete(db *gorm.DB) error { return db.Delete(m).Error } -// Ls returns a list of directories or albums in an account. -func (m *Account) Ls() (result []string, err error) { +// Directories returns a list of directories or albums in an account. +func (m *Account) Directories() (result fs.FileInfos, err error) { if m.AccType == string(service.TypeWebDAV) { c := webdav.Connect(m.AccURL, m.AccUser, m.AccPass) result, err = c.Directories("/", true) } - sort.Strings(result) + sort.Sort(result) return result, err } diff --git a/internal/entity/file_sync.go b/internal/entity/file_sync.go index 266d3df19..bfcdddadd 100644 --- a/internal/entity/file_sync.go +++ b/internal/entity/file_sync.go @@ -13,6 +13,8 @@ type FileSync struct { FileID uint `gorm:"index;"` AccountID uint `gorm:"primary_key;auto_increment:false"` RemoteName string `gorm:"type:varbinary(256);primary_key;auto_increment:false"` + RemoteDate time.Time + RemoteSize int64 Status string `gorm:"type:varbinary(16);"` Error string `gorm:"type:varbinary(512);"` File *File @@ -32,6 +34,7 @@ func NewFileSync(accountID uint, remoteName string) *FileSync { result := &FileSync{ AccountID: accountID, RemoteName: remoteName, + Status: "new", } return result diff --git a/internal/server/routes.go b/internal/server/routes.go index c537230fc..44c6214cc 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -72,7 +72,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) { api.GetAccounts(v1, conf) api.GetAccount(v1, conf) - api.LsAccount(v1, conf) + api.GetAccountDirs(v1, conf) api.ShareWithAccount(v1, conf) api.CreateAccount(v1, conf) api.DeleteAccount(v1, conf) diff --git a/internal/service/webdav/webdav.go b/internal/service/webdav/webdav.go index a65c65857..d87ad7efe 100644 --- a/internal/service/webdav/webdav.go +++ b/internal/service/webdav/webdav.go @@ -14,6 +14,7 @@ import ( "path" "github.com/photoprism/photoprism/internal/event" + "github.com/photoprism/photoprism/pkg/fs" "github.com/studio-b12/gowebdav" ) @@ -41,7 +42,7 @@ func (c Client) readDir(path string) ([]os.FileInfo, error) { } // Files returns all files in path as string slice. -func (c Client) Files(dir string) (result []string, err error) { +func (c Client) Files(dir string) (result fs.FileInfos, err error) { files, err := c.readDir(dir) if err != nil { @@ -52,14 +53,17 @@ func (c Client) Files(dir string) (result []string, err error) { if !file.Mode().IsRegular() { continue } - result = append(result, fmt.Sprintf("%s/%s", dir, file.Name())) + + info := fs.NewFileInfo(file, dir) + + result = append(result, info) } return result, nil } // Directories returns all sub directories in path as string slice. -func (c Client) Directories(root string, recursive bool) (result []string, err error) { +func (c Client) Directories(root string, recursive bool) (result fs.FileInfos, err error) { files, err := c.readDir(root) if err != nil { @@ -75,12 +79,12 @@ func (c Client) Directories(root string, recursive bool) (result []string, err e continue } - dir := fmt.Sprintf("%s/%s", root, file.Name()) + info := fs.NewFileInfo(file, root) - result = append(result, dir) + result = append(result, info) if recursive { - subDirs, err := c.Directories(dir, true) + subDirs, err := c.Directories(info.Abs, true) if err != nil { return result, err @@ -125,7 +129,7 @@ func (c Client) DownloadDir(from, to string, recursive bool) (errs []error) { } for _, file := range files { - dest := to + string(os.PathSeparator) + file + dest := to + string(os.PathSeparator) + file.Abs if _, err := os.Stat(dest); err == nil { // File exists @@ -135,7 +139,7 @@ func (c Client) DownloadDir(from, to string, recursive bool) (errs []error) { continue } - if err := c.Download(file, dest); err != nil { + if err := c.Download(file.Abs, dest); err != nil { msg := fmt.Errorf("webdav: %s", err) errs = append(errs, msg) log.Error(msg) @@ -150,7 +154,7 @@ func (c Client) DownloadDir(from, to string, recursive bool) (errs []error) { dirs, err := c.Directories(from, false) for _, dir := range dirs { - errs = append(errs, c.DownloadDir(dir, to, true)...) + errs = append(errs, c.DownloadDir(dir.Abs, to, true)...) } return errs diff --git a/internal/service/webdav/webdav_test.go b/internal/service/webdav/webdav_test.go index 9d8c9d54f..b91068e17 100644 --- a/internal/service/webdav/webdav_test.go +++ b/internal/service/webdav/webdav_test.go @@ -53,7 +53,11 @@ func TestClient_Directories(t *testing.T) { t.Fatal("no directories found") } - assert.Equal(t, "/Photos", dirs[0]) + assert.IsType(t, fs.FileInfo{}, dirs[0]) + assert.Equal(t, "Photos", dirs[0].Name) + assert.Equal(t, "/Photos", dirs[0].Abs) + assert.Equal(t, true, dirs[0].Dir) + assert.Equal(t, int64(0), dirs[0].Size) }) t.Run("recursive", func(t *testing.T) { @@ -87,7 +91,7 @@ func TestClient_Download(t *testing.T) { t.Fatal("no files to download") } - if err := c.Download(files[0], tempFile); err != nil { + if err := c.Download(files[0].Abs, tempFile); err != nil { t.Fatal(err) } diff --git a/pkg/fs/fileinfo.go b/pkg/fs/fileinfo.go new file mode 100644 index 000000000..a492cfe45 --- /dev/null +++ b/pkg/fs/fileinfo.go @@ -0,0 +1,58 @@ +package fs + +import ( + "fmt" + "os" + "strings" + "time" +) + +type FileInfo struct { + Name string `json:"name"` + Abs string `json:"abs"` + Size int64 `json:"size"` + Date time.Time `json:"date"` + Dir bool `json:"dir"` +} + +func NewFileInfo(info os.FileInfo, dir string) FileInfo { + if dir == "/" { + dir = "" + } else if len(dir) > 0 { + if dir[len(dir)-1:] == "/" { + dir = dir[:len(dir)-1] + } + + if dir[0:1] != "/" { + dir = "/" + dir + } + } + + result := FileInfo{ + Name: info.Name(), + Abs: fmt.Sprintf("%s/%s", dir, info.Name()), + Size: info.Size(), + Date: info.ModTime(), + Dir: info.IsDir(), + } + + return result +} + +type FileInfos []FileInfo + +func (infos FileInfos) Len() int { return len(infos) } +func (infos FileInfos) Swap(i, j int) { infos[i], infos[j] = infos[j], infos[i] } +func (infos FileInfos) Less(i, j int) bool { + return strings.Compare(infos[i].Abs, infos[j].Abs) == -1 +} + +func NewFileInfos(infos []os.FileInfo, dir string) FileInfos { + var result FileInfos + + for _, info := range infos { + result = append(result, NewFileInfo(info, dir)) + } + + return result +} diff --git a/pkg/fs/fileinfo_test.go b/pkg/fs/fileinfo_test.go new file mode 100644 index 000000000..a2bbeae8e --- /dev/null +++ b/pkg/fs/fileinfo_test.go @@ -0,0 +1,46 @@ +package fs + +import ( + "io/ioutil" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestNewFileInfo(t *testing.T) { + info, err := os.Stat("testdata/test.jpg") + + if err != nil { + t.Fatal(err) + } + + result := NewFileInfo(info, "Photos/") + + assert.Equal(t, "test.jpg", result.Name) + assert.Equal(t, "/Photos/test.jpg", result.Abs) + assert.Equal(t, int64(10990), result.Size) + assert.IsType(t, time.Time{}, result.Date) + assert.Equal(t, false, result.Dir) +} + +func TestNewFileInfos(t *testing.T) { + infos, err := ioutil.ReadDir("testdata") + + if err != nil { + t.Fatal(err) + } + + result := NewFileInfos(infos, "/") + + if len(result) < 1 { + t.Fatal("empty result") + } + + assert.Equal(t, "test.jpg", result[0].Name) + assert.Equal(t, "/test.jpg", result[0].Abs) + assert.Equal(t, int64(10990), result[0].Size) + assert.IsType(t, time.Time{}, result[0].Date) + assert.Equal(t, false, result[0].Dir) +}