WebDAV sharing proof-of-concept #225

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-04-01 12:00:45 +02:00
parent 2d5fede6dd
commit 91b1d7a198
18 changed files with 104 additions and 77 deletions

View file

@ -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',

View file

@ -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": "/"}];

View file

@ -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>

View file

@ -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";
}

View file

@ -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) {

View file

@ -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)

View file

@ -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))

View file

@ -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))

View file

@ -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)

View file

@ -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 {

View file

@ -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)
}

View file

@ -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)
})

View file

@ -0,0 +1,6 @@
package form
type AccountShare struct {
Photos []string `json:"photos"`
Destination string `json:"destination"`
}

View file

@ -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)

View file

@ -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)