WebDAV sharing proof-of-concept #225
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
2d5fede6dd
commit
91b1d7a198
|
@ -59,9 +59,9 @@
|
|||
</template>
|
||||
<script>
|
||||
import Photo from "../model/photo";
|
||||
import PhotoEditDetails from "./photo-edit/details.vue";
|
||||
import PhotoEditLabels from "./photo-edit/labels.vue";
|
||||
import PhotoEditFiles from "./photo-edit/files.vue";
|
||||
import PhotoEditDetails from "./photo/details.vue";
|
||||
import PhotoEditLabels from "./photo/labels.vue";
|
||||
import PhotoEditFiles from "./photo/files.vue";
|
||||
|
||||
export default {
|
||||
name: 'p-photo-edit-dialog',
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
name: 'p-photo-share-dialog',
|
||||
props: {
|
||||
show: Boolean,
|
||||
selection: Array,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -98,7 +99,14 @@
|
|||
return;
|
||||
}
|
||||
|
||||
this.$emit('confirm', this.account);
|
||||
this.loading = true;
|
||||
this.account.Share(this.selection, this.path).then(
|
||||
(files) => {
|
||||
this.loading = false;
|
||||
this.$notify.success(files.length + " files uploaded");
|
||||
this.$emit('confirm', this.account);
|
||||
}
|
||||
).catch(() => this.loading = false);
|
||||
},
|
||||
onChange() {
|
||||
this.paths = [{"text": "/", "value": "/"}];
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
<template>
|
||||
<div class="p-tab p-tab-photo-share-todo">
|
||||
<v-container fluid>
|
||||
<p class="subheading pb-3">
|
||||
This is work in progress. Feedback and contributions welcome.
|
||||
</p>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import options from "resources/options.json";
|
||||
|
||||
export default {
|
||||
name: 'p-tab-photo-edit-todo',
|
||||
props: {
|
||||
model: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
config: this.$config.values,
|
||||
all: {
|
||||
countries: [{code: "", name: this.$gettext("Unknown")}],
|
||||
cameras: [{ID: 0, CameraModel: this.$gettext("Unknown")}],
|
||||
lenses: [{ID: 0, LensModel: "Unknown"}],
|
||||
colors: [{label: "Unknown", name: ""}],
|
||||
},
|
||||
readonly: this.$config.getValue("readonly"),
|
||||
options: options,
|
||||
labels: {
|
||||
search: this.$gettext("Search"),
|
||||
view: this.$gettext("View"),
|
||||
country: this.$gettext("Country"),
|
||||
camera: this.$gettext("Camera"),
|
||||
lens: this.$gettext("Lens"),
|
||||
year: this.$gettext("Year"),
|
||||
color: this.$gettext("Color"),
|
||||
category: this.$gettext("Category"),
|
||||
sort: this.$gettext("Sort By"),
|
||||
before: this.$gettext("Taken before"),
|
||||
after: this.$gettext("Taken after"),
|
||||
language: this.$gettext("Language"),
|
||||
theme: this.$gettext("Theme"),
|
||||
},
|
||||
showDatepicker: false,
|
||||
showTimepicker: false,
|
||||
date: "",
|
||||
time: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
openPhoto() {
|
||||
this.$viewer.show([this.model], 0)
|
||||
},
|
||||
refresh() {
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -62,6 +62,12 @@ class Account extends Abstract {
|
|||
return Api.get(this.getEntityResource() + "/ls").then((response) => Promise.resolve(response.data));
|
||||
}
|
||||
|
||||
Share(UUIDs, dest) {
|
||||
const values = { Photos: UUIDs, Destination: dest };
|
||||
|
||||
return Api.post(this.getEntityResource() + "/share", values).then((response) => Promise.resolve(response.data));
|
||||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
return "accounts";
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -11,6 +12,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service/webdav"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
|
@ -72,7 +74,7 @@ func GetAccount(router *gin.RouterGroup, conf *config.Config) {
|
|||
//
|
||||
// Parameters:
|
||||
// id: string Account ID as returned by the API
|
||||
func GetAccountLs(router *gin.RouterGroup, conf *config.Config) {
|
||||
func LsAccount(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/accounts/:id/ls", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
|
@ -101,6 +103,62 @@ func GetAccountLs(router *gin.RouterGroup, conf *config.Config) {
|
|||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/accounts/:id/share
|
||||
//
|
||||
// Parameters:
|
||||
// id: string Account ID as returned by the API
|
||||
func ShareWithAccount(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/accounts/:id/share", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
q := query.New(conf.Db())
|
||||
id := ParseUint(c.Param("id"))
|
||||
|
||||
m, err := q.AccountByID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var f form.AccountShare
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
dst := f.Destination
|
||||
files, err := q.FilesByUUID(f.Photos, 1000, 0)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
w := webdav.Connect(m.AccURL, m.AccUser, m.AccPass)
|
||||
|
||||
if err := w.CreateDir(dst); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
srcFileName := conf.OriginalsPath() + string(os.PathSeparator) + file.FileName
|
||||
dstFileName := dst + "/" + file.ShareFileName()
|
||||
|
||||
if err := w.Upload(srcFileName, dstFileName); err != nil {
|
||||
log.Error("upload failed: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, files)
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/accounts
|
||||
func CreateAccount(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/accounts", func(c *gin.Context) {
|
||||
|
|
|
@ -476,7 +476,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
|||
|
||||
if thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil {
|
||||
if c.Query("download") != "" {
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", f.DownloadFileName()))
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", f.ShareFileName()))
|
||||
}
|
||||
|
||||
thumbData, err := ioutil.ReadFile(thumbnail)
|
||||
|
|
|
@ -43,7 +43,7 @@ func GetDownload(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
downloadFileName := f.DownloadFileName()
|
||||
downloadFileName := f.ShareFileName()
|
||||
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
downloadFileName := f.DownloadFileName()
|
||||
downloadFileName := f.ShareFileName()
|
||||
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
|||
|
||||
if thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil {
|
||||
if c.Query("download") != "" {
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", f.DownloadFileName()))
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", f.ShareFileName()))
|
||||
}
|
||||
|
||||
c.File(thumbnail)
|
||||
|
|
|
@ -77,7 +77,7 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
|
|||
|
||||
for _, f := range files {
|
||||
fileName := path.Join(conf.OriginalsPath(), f.FileName)
|
||||
fileAlias := f.DownloadFileName()
|
||||
fileAlias := f.ShareFileName()
|
||||
|
||||
if fs.FileExists(fileName) {
|
||||
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
|
||||
|
|
|
@ -63,8 +63,8 @@ func (m *File) BeforeCreate(scope *gorm.Scope) error {
|
|||
return scope.SetColumn("FileUUID", rnd.PPID('f'))
|
||||
}
|
||||
|
||||
// DownloadFileName returns a name useful for download links
|
||||
func (m *File) DownloadFileName() string {
|
||||
// ShareFileName returns a meaningful file name useful for sharing.
|
||||
func (m *File) ShareFileName() string {
|
||||
if m.Photo == nil {
|
||||
return fmt.Sprintf("%s.%s", m.FileHash, m.FileType)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ func TestFile_DownloadFileName(t *testing.T) {
|
|||
photo := &Photo{TakenAt: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: "Berlin / Morning Mood"}
|
||||
file := &File{Photo: photo, FileType: "jpg"}
|
||||
|
||||
filename := file.DownloadFileName()
|
||||
filename := file.ShareFileName()
|
||||
|
||||
assert.Equal(t, "20190115-000000-Berlin-Morning-Mood.jpg", filename)
|
||||
})
|
||||
|
@ -20,14 +20,14 @@ func TestFile_DownloadFileName(t *testing.T) {
|
|||
photo := &Photo{TakenAt: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: ""}
|
||||
file := &File{Photo: photo, FileType: "jpg", PhotoUUID: "123"}
|
||||
|
||||
filename := file.DownloadFileName()
|
||||
filename := file.ShareFileName()
|
||||
|
||||
assert.Equal(t, "20190115-000000-123.jpg", filename)
|
||||
})
|
||||
t.Run("photo without photo", func(t *testing.T) {
|
||||
file := &File{Photo: nil, FileType: "jpg", FileHash: "123Hash"}
|
||||
|
||||
filename := file.DownloadFileName()
|
||||
filename := file.ShareFileName()
|
||||
|
||||
assert.Equal(t, "123Hash.jpg", filename)
|
||||
})
|
||||
|
|
6
internal/form/account_share.go
Normal file
6
internal/form/account_share.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package form
|
||||
|
||||
type AccountShare struct {
|
||||
Photos []string `json:"photos"`
|
||||
Destination string `json:"destination"`
|
||||
}
|
|
@ -72,7 +72,8 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
|
||||
api.GetAccounts(v1, conf)
|
||||
api.GetAccount(v1, conf)
|
||||
api.GetAccountLs(v1, conf)
|
||||
api.LsAccount(v1, conf)
|
||||
api.ShareWithAccount(v1, conf)
|
||||
api.CreateAccount(v1, conf)
|
||||
api.DeleteAccount(v1, conf)
|
||||
api.UpdateAccount(v1, conf)
|
||||
|
|
|
@ -156,6 +156,15 @@ func (c Client) DownloadDir(from, to string, recursive bool) (errs []error) {
|
|||
return errs
|
||||
}
|
||||
|
||||
// CreateDir recursively creates directories if they don't exist.
|
||||
func (c Client) CreateDir(dir string) error {
|
||||
if dir == "" || dir == "/" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.client.MkdirAll(dir, os.ModePerm)
|
||||
}
|
||||
|
||||
// Upload uploads a single file to the remote server.
|
||||
func (c Client) Upload(from, to string) error {
|
||||
file, err := os.Open(from)
|
||||
|
|
Loading…
Reference in a new issue