Working download photo method
Signed-off-by: Kris Nóva <kris@nivenly.com>
This commit is contained in:
parent
c1a45bf8e3
commit
b7fd487a2e
18 changed files with 918 additions and 170 deletions
|
@ -16,6 +16,7 @@ const (
|
|||
)
|
||||
|
||||
type V1Client struct {
|
||||
downloadToken string
|
||||
token string
|
||||
apihost *url.URL
|
||||
client http.Client
|
||||
|
@ -23,11 +24,12 @@ type V1Client struct {
|
|||
|
||||
// New will only accept a url.URL so that we know
|
||||
// all errors have been handled up until this point
|
||||
func New(connURL *url.URL, token string) *V1Client {
|
||||
func New(connURL *url.URL, token, downloadToken string) *V1Client {
|
||||
return &V1Client{
|
||||
client: http.Client{},
|
||||
apihost: connURL,
|
||||
token: token,
|
||||
downloadToken: downloadToken,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,6 +198,60 @@ func (v1 *V1Client) PUT(payload interface{}, endpointFormat string, a ...interfa
|
|||
return response
|
||||
}
|
||||
|
||||
// DELETE is the V1 POST function. By design it will check globally for all non 200
|
||||
// responses and return an error if a non 200 is encountered.
|
||||
// DELETE will accept a payload.
|
||||
//
|
||||
// Error Codes:
|
||||
// -1 Unable to create request
|
||||
// -2 Unable to write payload
|
||||
// -3 Unable to JSON Marshal
|
||||
func (v1 *V1Client) DELETE(payload interface{}, endpointFormat string, a ...interface{}) *V1Response {
|
||||
url := v1.Endpoint(fmt.Sprintf(endpointFormat, a...))
|
||||
//logger.Debug("POST [%s]", url)
|
||||
response := &V1Response{}
|
||||
jBytes, err := json.Marshal(&payload)
|
||||
if err != nil {
|
||||
response.StatusCode = -3
|
||||
response.Error = fmt.Errorf("unable to marshal JSON: %v", err)
|
||||
return response
|
||||
}
|
||||
buffer := &bytes.Buffer{}
|
||||
_, err = buffer.Write(jBytes)
|
||||
if err != nil {
|
||||
response.StatusCode = -2
|
||||
response.Error = fmt.Errorf("unable to write payload: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("DELETE", url, buffer)
|
||||
if err != nil {
|
||||
response.StatusCode = -1
|
||||
response.Error = fmt.Errorf("unable to create new request: %v", err)
|
||||
return response
|
||||
}
|
||||
req.Header.Set("Content-Type", DefaultContentType)
|
||||
req.Header.Set("X-Session-Id", v1.token)
|
||||
resp, err := v1.client.Do(req)
|
||||
if err != nil {
|
||||
response.Error = fmt.Errorf("error while executing request: %v", err)
|
||||
return response
|
||||
}
|
||||
response.StatusCode = resp.StatusCode
|
||||
response.HTTPResponse = resp
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
response.Error = fmt.Errorf("unable to read body: %v", err)
|
||||
return response
|
||||
}
|
||||
response.Body = body
|
||||
if resp.StatusCode != 200 {
|
||||
response.Error = fmt.Errorf("[%d]: %s", resp.StatusCode, body)
|
||||
return response
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// Endpoint supports "/api/v1" and "api/v1" like strings
|
||||
// to generate the string type of a given endpoint based on
|
||||
// a client
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -58,7 +57,7 @@ type Photo struct {
|
|||
//Place *Place `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Place" yaml:"-"`
|
||||
//Keywords []Keyword `json:"-" yaml:"-"`
|
||||
//Albums []Album `json:"-" yaml:"-"`
|
||||
//Files []File `yaml:"-"`
|
||||
Files []File `yaml:"-"`
|
||||
//Labels []PhotoLabel `yaml:"-"`
|
||||
CreatedAt time.Time `yaml:"CreatedAt,omitempty"`
|
||||
UpdatedAt time.Time `yaml:"UpdatedAt,omitempty"`
|
||||
|
@ -80,6 +79,9 @@ func (v1 *V1Client) GetPhoto(uuid string) (Photo, error) {
|
|||
}
|
||||
|
||||
// PUT /api/v1/photos/:uid
|
||||
//
|
||||
// Parameters:
|
||||
// uuid: string PhotoUUID as returned by the API
|
||||
func (v1 *V1Client) UpdatePhoto(object Photo) (Photo, error) {
|
||||
err := v1.PUT(&object, "/api/v1/photos/%s", object.UUID).JSON(&object)
|
||||
return object, err
|
||||
|
@ -89,36 +91,27 @@ func (v1 *V1Client) UpdatePhoto(object Photo) (Photo, error) {
|
|||
//
|
||||
// Parameters:
|
||||
// uuid: string PhotoUUID as returned by the API
|
||||
func (v1 *V1Client) GetPhotoDownload(uuid string) (*File, error) {
|
||||
if uuid == "" {
|
||||
return nil, fmt.Errorf("missing uuid for GetPhotoDownload [GET /api/v1/photos/:uuid/dl]")
|
||||
}
|
||||
file := &File{}
|
||||
return file, nil
|
||||
func (v1 *V1Client) GetPhotoDownload(uuid string) ([]byte, error) {
|
||||
resp := v1.GET("/api/v1/photos/%s/dl?t=%s", uuid, v1.downloadToken)
|
||||
return resp.Body, resp.Error
|
||||
}
|
||||
|
||||
// GET /api/v1/photos/:uuid/yaml
|
||||
//
|
||||
// Parameters:
|
||||
// uuid: string PhotoUUID as returned by the API
|
||||
func (v1 *V1Client) GetPhotoYaml(uuid string) (*Photo, error) {
|
||||
if uuid == "" {
|
||||
return nil, fmt.Errorf("missing uuid for GetPhotoYAML [GET /api/v1/photos/:uuid/yaml]")
|
||||
}
|
||||
photo := &Photo{}
|
||||
return photo, nil
|
||||
func (v1 *V1Client) GetPhotoYaml(uuid string) ([]byte, error) {
|
||||
resp := v1.GET("/api/v1/photos/%s/yaml", uuid)
|
||||
return resp.Body, resp.Error
|
||||
}
|
||||
|
||||
// POST /api/v1/photos/:uuid/approve
|
||||
//
|
||||
// Parameters:
|
||||
// uuid: string PhotoUUID as returned by the API
|
||||
func (v1 *V1Client) ApprovePhoto(uuid string) (*Photo, error) {
|
||||
if uuid == "" {
|
||||
return nil, fmt.Errorf("missing uuid for ApprovePhoto [POST /api/v1/photos/:uuid/approve]")
|
||||
}
|
||||
photo := &Photo{}
|
||||
return photo, nil
|
||||
func (v1 *V1Client) ApprovePhoto(uuid string) error {
|
||||
resp := v1.POST(nil, "/api/v1/photos/%s/approve", uuid)
|
||||
return resp.Error
|
||||
}
|
||||
|
||||
// POST /api/v1/photos/:uid/like
|
||||
|
@ -126,10 +119,8 @@ func (v1 *V1Client) ApprovePhoto(uuid string) (*Photo, error) {
|
|||
// Parameters:
|
||||
// uid: string PhotoUID as returned by the API
|
||||
func (v1 *V1Client) LikePhoto(uuid string) error {
|
||||
if uuid == "" {
|
||||
return fmt.Errorf("missing uuid for LikePhoto [POST /api/v1/photos/:uid/like]")
|
||||
}
|
||||
return nil
|
||||
resp := v1.POST(nil, "/api/v1/photos/%s/like", uuid)
|
||||
return resp.Error
|
||||
}
|
||||
|
||||
// DELETE /api/v1/photos/:uuid/like
|
||||
|
@ -137,10 +128,8 @@ func (v1 *V1Client) LikePhoto(uuid string) error {
|
|||
// Parameters:
|
||||
// uuid: string PhotoUUID as returned by the API
|
||||
func (v1 *V1Client) DislikePhoto(uuid string) error {
|
||||
if uuid == "" {
|
||||
return fmt.Errorf("missing uuid for DislikePhoto [DELETE /api/v1/photos/:uuid/like]")
|
||||
}
|
||||
return nil
|
||||
resp := v1.DELETE(nil, "/api/v1/photos/%s/approve", uuid)
|
||||
return resp.Error
|
||||
}
|
||||
|
||||
// POST /api/v1/photos/:uid/files/:file_uid/primary
|
||||
|
@ -149,48 +138,6 @@ func (v1 *V1Client) DislikePhoto(uuid string) error {
|
|||
// uid: string PhotoUID as returned by the API
|
||||
// file_uid: string File UID as returned by the API
|
||||
func (v1 *V1Client) PhotoPrimary(uuid, fileuuid string) error {
|
||||
if uuid == "" {
|
||||
return fmt.Errorf("missing uuid for PhotoPrimary [POST /api/v1/photos/:uid/files/:file_uid/primary]")
|
||||
resp := v1.POST(nil, "/api/v1/photos/%s/files/%s/primary", uuid, fileuuid)
|
||||
return resp.Error
|
||||
}
|
||||
if fileuuid == "" {
|
||||
return fmt.Errorf("missing fileuuid for PhotoPrimary [POST /api/v1/photos/:uid/files/:file_uid/primary]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// -----
|
||||
// Dump from Chrome
|
||||
//
|
||||
//Request URL: http://localhost:8080/api/v1/photos/pqnzigq156lndozm
|
||||
//Request Method: PUT
|
||||
//Status Code: 200 OK
|
||||
//Remote Address: 127.0.0.1:8080
|
||||
//Referrer Policy: strict-origin-when-cross-origin
|
||||
|
||||
// [RESPONSE HEADERS]
|
||||
//Content-Type: application/json; charset=utf-8
|
||||
//Date: Thu, 04 Feb 2021 04:27:16 GMT
|
||||
//Transfer-Encoding: chunked
|
||||
|
||||
// [REQUEST HEADERS]
|
||||
//Accept: application/json, text/plain, */*
|
||||
//Accept-Encoding: gzip, deflate, br
|
||||
//Accept-Language: en-US,en;q=0.9
|
||||
//Connection: keep-alive
|
||||
//Content-Length: 41
|
||||
//Content-Type: application/json;charset=UTF-8
|
||||
//Host: localhost:8080
|
||||
//Origin: http://localhost:8080
|
||||
//Referer: http://localhost:8080/albums/aqnzih81icziiyae/february-2021
|
||||
//Sec-Fetch-Dest: empty
|
||||
//Sec-Fetch-Mode: cors
|
||||
//Sec-Fetch-Site: same-origin
|
||||
//User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36
|
||||
//X-Client-Hash: 2607a5a5
|
||||
//X-Client-Version: 210121-07e559df-Linux-x86_64
|
||||
//X-Session-ID: d92837cb1c41e37b9993d25e282efb3b337b6ae609a687d9
|
||||
|
||||
// [REQUEST PAYLOAD]
|
||||
//{Title: "Test Nova", TitleSrc: "manual"}
|
||||
//Title: "Test Nova"
|
||||
//TitleSrc: "manual"
|
||||
|
|
16
client.go
16
client.go
|
@ -124,11 +124,25 @@ func (c *Client) LoginV1() error {
|
|||
}
|
||||
return fmt.Errorf("login error [%d] %s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
// --- JSON Auth Response on to Options ---
|
||||
cfg := &Config{
|
||||
Config: &Options{},
|
||||
}
|
||||
bytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse auth body: %v", err)
|
||||
}
|
||||
err = json.Unmarshal(bytes, &cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to json unmarshal auth body: %v", err)
|
||||
}
|
||||
|
||||
token := resp.Header.Get(APIAuthHeaderKey)
|
||||
if token == "" {
|
||||
return fmt.Errorf("missing auth token from successful login")
|
||||
}
|
||||
c.v1client = v1.New(c.connectionURL, token)
|
||||
c.v1client = v1.New(c.connectionURL, token, cfg.Config.DownloadToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
1
examples/.gitignore
vendored
Normal file
1
examples/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
ignore_*
|
|
@ -1,30 +1,60 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
|
||||
photoprism "github.com/kris-nova/client-go"
|
||||
"github.com/kris-nova/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// ---
|
||||
// Log Level 4 (Most)
|
||||
// Log Level 3
|
||||
// Log Level 2
|
||||
// Log Level 1
|
||||
// Log Level 0 (Least)
|
||||
//
|
||||
logger.Level = 4
|
||||
//
|
||||
// ---
|
||||
|
||||
uuid := "pqnzigq351j2fqgn" // This is a known ID
|
||||
client := photoprism.New("http://localhost:8080")
|
||||
err := client.Auth(photoprism.NewClientAuthLogin("admin", "missy"))
|
||||
if err != nil {
|
||||
halt(4, "Error logging into API: %v", err)
|
||||
}
|
||||
//logger.Always("Login Success!")
|
||||
|
||||
// ---
|
||||
// GetPhoto()
|
||||
//
|
||||
photo, err := client.V1().GetPhoto(uuid)
|
||||
if err != nil {
|
||||
halt(3, "Error fetching photo: %v", err)
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(photo)
|
||||
// ---
|
||||
// UpdatePhoto()
|
||||
photo.PhotoTitle = "A really great photo!"
|
||||
photo, err = client.V1().UpdatePhoto(photo)
|
||||
if err != nil {
|
||||
halt(5, "Error: %v", err)
|
||||
halt(2, "Error updating photo: %v", err)
|
||||
}
|
||||
fmt.Println(string(bytes))
|
||||
|
||||
// ---
|
||||
// GetPhotoDownload()
|
||||
file, err := client.V1().GetPhotoDownload(photo.UUID)
|
||||
if err != nil {
|
||||
halt(2, "Error getting photo download: %v", err)
|
||||
}
|
||||
|
||||
for _, f := range photo.Files {
|
||||
fileName := fmt.Sprintf("ignore_%s", path.Base(f.FileName))
|
||||
logger.Always(fileName)
|
||||
ioutil.WriteFile(fileName, file, 0666)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ func GetAccountFolders(router *gin.RouterGroup) {
|
|||
if cacheData, ok := cache.Get(cacheKey); ok {
|
||||
cached := cacheData.(fs.FileInfos)
|
||||
|
||||
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
c.JSON(http.StatusOK, cached)
|
||||
return
|
||||
|
|
|
@ -38,22 +38,32 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
log.Infof("archive: adding %s", f.String())
|
||||
log.Infof("photos: archiving %s", f.String())
|
||||
|
||||
// Soft delete by setting deleted_at to current date.
|
||||
err := entity.Db().Where("photo_uid IN (?)", f.Photos).Delete(&entity.Photo{}).Error
|
||||
if service.Config().BackupYaml() {
|
||||
photos, err := query.PhotoSelection(f)
|
||||
|
||||
if err != nil {
|
||||
AbortSaveFailed(c)
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove archived photos from albums.
|
||||
logError("archive", entity.Db().Model(&entity.PhotoAlbum{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("hidden", true).Error)
|
||||
|
||||
if err := entity.UpdatePhotoCounts(); err != nil {
|
||||
log.Errorf("photos: %s", err)
|
||||
for _, p := range photos {
|
||||
if err := p.Archive(); err != nil {
|
||||
log.Errorf("archive: %s", err)
|
||||
} else {
|
||||
SavePhotoAsYaml(p)
|
||||
}
|
||||
}
|
||||
} else if err := entity.Db().Where("photo_uid IN (?)", f.Photos).Delete(&entity.Photo{}).Error; err != nil {
|
||||
log.Errorf("archive: %s", err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
} else if err := entity.Db().Model(&entity.PhotoAlbum{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("hidden", true).Error; err != nil {
|
||||
log.Errorf("archive: %s", err)
|
||||
}
|
||||
|
||||
logError("photos", entity.UpdatePhotoCounts())
|
||||
|
||||
UpdateClientConfig()
|
||||
|
||||
|
@ -63,6 +73,62 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/batch/photos/restore
|
||||
func BatchPhotosRestore(router *gin.RouterGroup) {
|
||||
router.POST("/batch/photos/restore", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDelete)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
var f form.Selection
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
if len(f.Photos) == 0 {
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("photos: restoring %s", f.String())
|
||||
|
||||
if service.Config().BackupYaml() {
|
||||
photos, err := query.PhotoSelection(f)
|
||||
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range photos {
|
||||
if err := p.Restore(); err != nil {
|
||||
log.Errorf("restore: %s", err)
|
||||
} else {
|
||||
SavePhotoAsYaml(p)
|
||||
}
|
||||
}
|
||||
} else if err := entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uid IN (?)", f.Photos).
|
||||
UpdateColumn("deleted_at", gorm.Expr("NULL")).Error; err != nil {
|
||||
log.Errorf("restore: %s", err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
logError("photos", entity.UpdatePhotoCounts())
|
||||
|
||||
UpdateClientConfig()
|
||||
|
||||
event.EntitiesRestored("photos", f.Photos)
|
||||
|
||||
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgSelectionRestored))
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/batch/photos/approve
|
||||
func BatchPhotosApprove(router *gin.RouterGroup) {
|
||||
router.POST("batch/photos/approve", func(c *gin.Context) {
|
||||
|
@ -98,7 +164,7 @@ func BatchPhotosApprove(router *gin.RouterGroup) {
|
|||
|
||||
for _, p := range photos {
|
||||
if err := p.Approve(); err != nil {
|
||||
log.Errorf("photo: %s (approve)", err.Error())
|
||||
log.Errorf("approve: %s", err)
|
||||
} else {
|
||||
approved = append(approved, p)
|
||||
SavePhotoAsYaml(p)
|
||||
|
@ -113,50 +179,6 @@ func BatchPhotosApprove(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/batch/photos/restore
|
||||
func BatchPhotosRestore(router *gin.RouterGroup) {
|
||||
router.POST("/batch/photos/restore", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDelete)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
var f form.Selection
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
if len(f.Photos) == 0 {
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("archive: restoring %s", f.String())
|
||||
|
||||
err := entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uid IN (?)", f.Photos).
|
||||
UpdateColumn("deleted_at", gorm.Expr("NULL")).Error
|
||||
|
||||
if err != nil {
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := entity.UpdatePhotoCounts(); err != nil {
|
||||
log.Errorf("photos: %s", err)
|
||||
}
|
||||
|
||||
UpdateClientConfig()
|
||||
|
||||
event.EntitiesRestored("photos", f.Photos)
|
||||
|
||||
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgSelectionRestored))
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/batch/albums/delete
|
||||
func BatchAlbumsDelete(router *gin.RouterGroup) {
|
||||
router.POST("/batch/albums/delete", func(c *gin.Context) {
|
||||
|
@ -214,22 +236,23 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
log.Infof("photos: mark %s as private", f.String())
|
||||
log.Infof("photos: updating private flag for %s", f.String())
|
||||
|
||||
err := entity.Db().Model(entity.Photo{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("photo_private",
|
||||
gorm.Expr("CASE WHEN photo_private > 0 THEN 0 ELSE 1 END")).Error
|
||||
|
||||
if err != nil {
|
||||
if err := entity.Db().Model(entity.Photo{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("photo_private",
|
||||
gorm.Expr("CASE WHEN photo_private > 0 THEN 0 ELSE 1 END")).Error; err != nil {
|
||||
log.Errorf("private: %s", err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := entity.UpdatePhotoCounts(); err != nil {
|
||||
log.Errorf("photos: %s", err)
|
||||
logError("photos", entity.UpdatePhotoCounts())
|
||||
|
||||
if photos, err := query.PhotoSelection(f); err == nil {
|
||||
for _, p := range photos {
|
||||
SavePhotoAsYaml(p)
|
||||
}
|
||||
|
||||
if entities, err := query.PhotoSelection(f); err == nil {
|
||||
event.EntitiesUpdated("photos", entities)
|
||||
event.EntitiesUpdated("photos", photos)
|
||||
}
|
||||
|
||||
UpdateClientConfig()
|
||||
|
@ -313,7 +336,7 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
log.Infof("archive: permanently deleting %s", f.String())
|
||||
log.Infof("photos: deleting %s", f.String())
|
||||
|
||||
photos, err := query.PhotoSelection(f)
|
||||
|
||||
|
@ -327,7 +350,7 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
|
|||
// Delete photos.
|
||||
for _, p := range photos {
|
||||
if err := photoprism.Delete(p); err != nil {
|
||||
log.Errorf("photo: %s (delete)", err.Error())
|
||||
log.Errorf("delete: %s", err)
|
||||
} else {
|
||||
deleted = append(deleted, p)
|
||||
}
|
||||
|
@ -335,9 +358,7 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
|
|||
|
||||
// Update counts and views if needed.
|
||||
if len(deleted) > 0 {
|
||||
if err := entity.UpdatePhotoCounts(); err != nil {
|
||||
log.Errorf("photos: %s", err)
|
||||
}
|
||||
logError("photos", entity.UpdatePhotoCounts())
|
||||
|
||||
UpdateClientConfig()
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ func AlbumCover(router *gin.RouterGroup) {
|
|||
cacheKey := CacheKey(albumCover, uid, typeName)
|
||||
|
||||
if cacheData, ok := cache.Get(cacheKey); ok {
|
||||
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
cached := cacheData.(ThumbCache)
|
||||
|
||||
|
@ -108,8 +108,8 @@ func AlbumCover(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("album: %s", err)
|
||||
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
|
||||
log.Errorf("%s: %s", albumCover, err)
|
||||
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
|
||||
return
|
||||
} else if thumbnail == "" {
|
||||
log.Errorf("%s: %s has empty thumb name - bug?", albumCover, filepath.Base(fileName))
|
||||
|
@ -160,7 +160,7 @@ func LabelCover(router *gin.RouterGroup) {
|
|||
cacheKey := CacheKey(labelCover, uid, typeName)
|
||||
|
||||
if cacheData, ok := cache.Get(cacheKey); ok {
|
||||
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
cached := cacheData.(ThumbCache)
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ func GetFolders(router *gin.RouterGroup, urlPath, rootName, rootPath string) {
|
|||
if cacheData, ok := cache.Get(cacheKey); ok {
|
||||
cached := cacheData.(FoldersResponse)
|
||||
|
||||
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
c.JSON(http.StatusOK, cached)
|
||||
return
|
||||
|
|
140
internal/api/folder_cover.go
Normal file
140
internal/api/folder_cover.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
const (
|
||||
folderCover = "folder-cover"
|
||||
)
|
||||
|
||||
// GET /api/v1/folders/t/:hash/:token/:type
|
||||
//
|
||||
// Parameters:
|
||||
// uid: string folder uid
|
||||
// token: string url security token, see config
|
||||
// type: string thumb type, see thumb.Types
|
||||
func GetFolderCover(router *gin.RouterGroup) {
|
||||
router.GET("/folders/t/:uid/:token/:type", func(c *gin.Context) {
|
||||
if InvalidPreviewToken(c) {
|
||||
c.Data(http.StatusForbidden, "image/svg+xml", folderIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
conf := service.Config()
|
||||
uid := c.Param("uid")
|
||||
typeName := c.Param("type")
|
||||
download := c.Query("download") != ""
|
||||
|
||||
thumbType, ok := thumb.Types[typeName]
|
||||
|
||||
if !ok {
|
||||
log.Errorf("folder: invalid thumb type %s", txt.Quote(typeName))
|
||||
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
if thumbType.ExceedsSize() && !conf.ThumbUncached() {
|
||||
typeName, thumbType = thumb.Find(conf.ThumbSize())
|
||||
|
||||
if typeName == "" {
|
||||
log.Errorf("folder: invalid thumb size %d", conf.ThumbSize())
|
||||
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cache := service.CoverCache()
|
||||
cacheKey := CacheKey(folderCover, uid, typeName)
|
||||
|
||||
if cacheData, ok := cache.Get(cacheKey); ok {
|
||||
log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
cached := cacheData.(ThumbCache)
|
||||
|
||||
if !fs.FileExists(cached.FileName) {
|
||||
log.Errorf("%s: %s not found", folderCover, uid)
|
||||
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
AddCoverCacheHeader(c)
|
||||
|
||||
if download {
|
||||
c.FileAttachment(cached.FileName, cached.ShareName)
|
||||
} else {
|
||||
c.File(cached.FileName)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
f, err := query.FolderCoverByUID(uid)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("%s: no photos yet, using generic image for %s", folderCover, uid)
|
||||
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := photoprism.FileName(f.FileRoot, f.FileName)
|
||||
|
||||
if !fs.FileExists(fileName) {
|
||||
log.Errorf("%s: could not find original for %s", folderCover, fileName)
|
||||
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore.
|
||||
log.Warnf("%s: %s is missing", folderCover, txt.Quote(f.FileName))
|
||||
logError(folderCover, f.Update("FileMissing", true))
|
||||
return
|
||||
}
|
||||
|
||||
// Use original file if thumb size exceeds limit, see https://github.com/photoprism/photoprism/issues/157
|
||||
if thumbType.ExceedsSizeUncached() && !download {
|
||||
log.Debugf("%s: using original, size exceeds limit (width %d, height %d)", folderCover, thumbType.Width, thumbType.Height)
|
||||
AddCoverCacheHeader(c)
|
||||
c.File(fileName)
|
||||
return
|
||||
}
|
||||
|
||||
var thumbnail string
|
||||
|
||||
if conf.ThumbUncached() || thumbType.OnDemand() {
|
||||
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
|
||||
} else {
|
||||
thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("%s: %s", folderCover, err)
|
||||
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
||||
return
|
||||
} else if thumbnail == "" {
|
||||
log.Errorf("%s: %s has empty thumb name - bug?", folderCover, filepath.Base(fileName))
|
||||
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
cache.SetDefault(cacheKey, ThumbCache{thumbnail, f.ShareBase(0)})
|
||||
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
AddCoverCacheHeader(c)
|
||||
|
||||
if download {
|
||||
c.FileAttachment(thumbnail, f.DownloadName(DownloadName(c), 0))
|
||||
} else {
|
||||
c.File(thumbnail)
|
||||
}
|
||||
})
|
||||
}
|
332
internal/api/photo.go
Normal file
332
internal/api/photo.go
Normal file
|
@ -0,0 +1,332 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// SavePhotoAsYaml saves photo data as YAML file.
|
||||
func SavePhotoAsYaml(p entity.Photo) {
|
||||
c := service.Config()
|
||||
|
||||
// Write YAML sidecar file (optional).
|
||||
if !c.BackupYaml() {
|
||||
return
|
||||
}
|
||||
|
||||
fileName := p.YamlFileName(c.OriginalsPath(), c.SidecarPath())
|
||||
|
||||
if err := p.SaveAsYaml(fileName); err != nil {
|
||||
log.Errorf("photo: %s (update yaml)", err)
|
||||
} else {
|
||||
log.Debugf("photo: updated yaml file %s", txt.Quote(filepath.Base(fileName)))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/v1/photos/:uid
|
||||
//
|
||||
// Parameters:
|
||||
// uid: string PhotoUID as returned by the API
|
||||
func GetPhoto(router *gin.RouterGroup) {
|
||||
router.GET("/photos/:uid", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionRead)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
p, err := query.PhotoPreloadByUID(c.Param("uid"))
|
||||
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.IndentedJSON(http.StatusOK, p)
|
||||
})
|
||||
}
|
||||
|
||||
// PUT /api/v1/photos/:uid
|
||||
func UpdatePhoto(router *gin.RouterGroup) {
|
||||
router.PUT("/photos/:uid", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
uid := c.Param("uid")
|
||||
m, err := query.PhotoByUID(uid)
|
||||
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Proof-of-concept for form handling - might need refactoring
|
||||
// 1) Init form with model values
|
||||
f, err := form.NewPhoto(m)
|
||||
|
||||
if err != nil {
|
||||
Abort(c, http.StatusInternalServerError, i18n.ErrSaveFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// 2) Update form with values from request
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrBadRequest)
|
||||
return
|
||||
} else if f.PhotoPrivate {
|
||||
FlushCoverCache()
|
||||
}
|
||||
|
||||
// 3) Save model with values from form
|
||||
if err := entity.SavePhotoForm(m, f); err != nil {
|
||||
Abort(c, http.StatusInternalServerError, i18n.ErrSaveFailed)
|
||||
return
|
||||
}
|
||||
|
||||
PublishPhotoEvent(EntityUpdated, uid, c)
|
||||
|
||||
event.SuccessMsg(i18n.MsgChangesSaved)
|
||||
|
||||
p, err := query.PhotoPreloadByUID(uid)
|
||||
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
SavePhotoAsYaml(p)
|
||||
|
||||
UpdateClientConfig()
|
||||
|
||||
c.JSON(http.StatusOK, p)
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/photos/:uid/dl
|
||||
//
|
||||
// Parameters:
|
||||
// uid: string PhotoUID as returned by the API
|
||||
func GetPhotoDownload(router *gin.RouterGroup) {
|
||||
router.GET("/photos/:uid/dl", func(c *gin.Context) {
|
||||
if InvalidDownloadToken(c) {
|
||||
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
f, err := query.FileByPhotoUID(c.Param("uid"))
|
||||
|
||||
if err != nil {
|
||||
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := photoprism.FileName(f.FileRoot, f.FileName)
|
||||
|
||||
if !fs.FileExists(fileName) {
|
||||
log.Errorf("photo: file %s is missing", txt.Quote(f.FileName))
|
||||
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore.
|
||||
logError("photo", f.Update("FileMissing", true))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.FileAttachment(fileName, f.DownloadName(DownloadName(c), 0))
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/photos/:uid/yaml
|
||||
//
|
||||
// Parameters:
|
||||
// uid: string PhotoUID as returned by the API
|
||||
func GetPhotoYaml(router *gin.RouterGroup) {
|
||||
router.GET("/photos/:uid/yaml", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionExport)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
p, err := query.PhotoPreloadByUID(c.Param("uid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := p.Yaml()
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if c.Query("download") != "" {
|
||||
AddDownloadHeader(c, c.Param("uid")+fs.YamlExt)
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, "text/x-yaml; charset=utf-8", data)
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/photos/:uid/approve
|
||||
//
|
||||
// Parameters:
|
||||
// uid: string PhotoUID as returned by the API
|
||||
func ApprovePhoto(router *gin.RouterGroup) {
|
||||
router.POST("/photos/:uid/approve", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("uid")
|
||||
m, err := query.PhotoByUID(id)
|
||||
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.Approve(); err != nil {
|
||||
log.Errorf("photo: %s", err.Error())
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
SavePhotoAsYaml(m)
|
||||
|
||||
PublishPhotoEvent(EntityUpdated, id, c)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/photos/:uid/like
|
||||
//
|
||||
// Parameters:
|
||||
// uid: string PhotoUID as returned by the API
|
||||
func LikePhoto(router *gin.RouterGroup) {
|
||||
router.POST("/photos/:uid/like", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionLike)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("uid")
|
||||
m, err := query.PhotoByUID(id)
|
||||
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.SetFavorite(true); err != nil {
|
||||
log.Errorf("photo: %s", err.Error())
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
SavePhotoAsYaml(m)
|
||||
|
||||
PublishPhotoEvent(EntityUpdated, id, c)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE /api/v1/photos/:uid/like
|
||||
//
|
||||
// Parameters:
|
||||
// uid: string PhotoUID as returned by the API
|
||||
func DislikePhoto(router *gin.RouterGroup) {
|
||||
router.DELETE("/photos/:uid/like", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionLike)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("uid")
|
||||
m, err := query.PhotoByUID(id)
|
||||
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.SetFavorite(false); err != nil {
|
||||
log.Errorf("photo: %s", err.Error())
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
SavePhotoAsYaml(m)
|
||||
|
||||
PublishPhotoEvent(EntityUpdated, id, c)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/photos/:uid/files/:file_uid/primary
|
||||
//
|
||||
// Parameters:
|
||||
// uid: string PhotoUID as returned by the API
|
||||
// file_uid: string File UID as returned by the API
|
||||
func PhotoPrimary(router *gin.RouterGroup) {
|
||||
router.POST("/photos/:uid/files/:file_uid/primary", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
uid := c.Param("uid")
|
||||
fileUID := c.Param("file_uid")
|
||||
err := query.SetPhotoPrimary(uid, fileUID)
|
||||
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
PublishPhotoEvent(EntityUpdated, uid, c)
|
||||
|
||||
event.SuccessMsg(i18n.MsgChangesSaved)
|
||||
|
||||
p, err := query.PhotoPreloadByUID(uid)
|
||||
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, p)
|
||||
})
|
||||
}
|
|
@ -55,7 +55,7 @@ func GetThumb(router *gin.RouterGroup) {
|
|||
cacheKey := CacheKey("thumbs", fileHash, typeName)
|
||||
|
||||
if cacheData, ok := cache.Get(cacheKey); ok {
|
||||
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
cached := cacheData.(ThumbCache)
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
|
|||
stackPrimary, err := stackPhoto.PrimaryFile()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("photo: can't find primary file for existing photo (unstack %s)", txt.Quote(baseName))
|
||||
log.Errorf("photo: can't find primary file for %s (unstack)", txt.Quote(baseName))
|
||||
AbortUnexpected(c)
|
||||
return
|
||||
}
|
||||
|
@ -81,11 +81,11 @@ func PhotoUnstack(router *gin.RouterGroup) {
|
|||
AbortEntityNotFound(c)
|
||||
return
|
||||
} else if related.Len() == 0 {
|
||||
log.Errorf("photo: no files found (unstack %s)", txt.Quote(baseName))
|
||||
log.Errorf("photo: no files found for %s (unstack)", txt.Quote(baseName))
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
} else if related.Main == nil {
|
||||
log.Errorf("photo: no main file found (unstack %s)", txt.Quote(baseName))
|
||||
log.Errorf("photo: no main file found for %s (unstack)", txt.Quote(baseName))
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
|
110
sample-app/photoprism/storage/cache/sessions.json
vendored
110
sample-app/photoprism/storage/cache/sessions.json
vendored
|
@ -9,6 +9,16 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613514526199122543
|
||||
},
|
||||
"0957017ab9576154468a8a0df5fd74798be6965b3f5eef90": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613523028656083744
|
||||
},
|
||||
"0bc03c7776136e860865c7cd2c30d6678e896805d0e0bba8": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613527737294727784
|
||||
},
|
||||
"0bf52bb31c11c5ca6c56646496b184eb39f33b004a46b203": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
|
@ -34,6 +44,11 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613502823940492608
|
||||
},
|
||||
"163e08d6fc94c7c7c07f1af8638fd5db4aa929b0462d5548": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613522078076464014
|
||||
},
|
||||
"21a2853a53625574889c1b0d653170efaf3ba51489c3da85": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
|
@ -44,6 +59,11 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613513903738209742
|
||||
},
|
||||
"25847336ab7fa37815b6ba9fba455766bb797f81cdb70dc8": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613527778569130528
|
||||
},
|
||||
"27ecafdd4819a88cc523aa95a0698d353ceb85ac6371cc4e": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
|
@ -69,11 +89,26 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613500755747387098
|
||||
},
|
||||
"34dd871e663973ef4ae9f00ee7a2a10e264e3d12f60d36db": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613528491291754429
|
||||
},
|
||||
"370c05ea9a50f1e6befe3eba0e59e1a965daaa31da6f028a": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613520870803124902
|
||||
},
|
||||
"3bd19dc60e5a515d2624f0d712db8471f433416935c2a045": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613512395822771302
|
||||
},
|
||||
"3fac95f9ca118f4e3dfe3fc87a174ef845d608af2a301f31": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613526857034102326
|
||||
},
|
||||
"41a99f15500d1eca9818bc3e0cae7cca319cd6eb2b38bb8d": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
|
@ -94,6 +129,11 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613519956718633151
|
||||
},
|
||||
"447da9991224a43c09deb314d41a9f7ff718e1ce75d824cc": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613528419822486762
|
||||
},
|
||||
"469a249abd7cf70eae9e39abd41c7a311b40d8c1e31f0199": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
|
@ -104,11 +144,21 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613512362197250367
|
||||
},
|
||||
"55b19c28a5189350d53a979adb17a8335b683d5474a064d1": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613526231902816117
|
||||
},
|
||||
"589279988dbd4ad774ef7a59392f6bc44f416ff8d34e653e": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613502738172098175
|
||||
},
|
||||
"5973a6f141757700945f48564869ef350bfd0e6c7319aa35": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613527873142005750
|
||||
},
|
||||
"5a159bc84d1cac8cd025db03de8e1dbf414d6bdefb967885": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
|
@ -134,11 +184,21 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613519264711770335
|
||||
},
|
||||
"7ad01775ce800f5a065cb187bbdcfe8ff72e81774db5105f": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613526444602833378
|
||||
},
|
||||
"7d381eaaea551483d8d50aa39007d7fc9fdddd3b6a06359d": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613513949594487106
|
||||
},
|
||||
"8173f5cece02b1a41e9bc937bbbbc80960b838d93cef7f2d": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613526315102874135
|
||||
},
|
||||
"82b0a1bdd1ac266855b338513eff6631c000f7187f1e099d": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
|
@ -194,6 +254,16 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613516776661303208
|
||||
},
|
||||
"92f1ec16db743e3a112d6c889834c93e21dd015cba8d8917": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613526336803063121
|
||||
},
|
||||
"988de1401f8dfe14a3766ea883ac54fa14dfd3149cefff6d": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613520871647468457
|
||||
},
|
||||
"9d382dfa406c01501fc5cc88b025c22d09248834da7e2b8b": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
|
@ -204,6 +274,11 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613502624809588007
|
||||
},
|
||||
"9e8c7c0f010bbdf87c0274442c1b35cf98cd4aa521a8a94c": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613522128528856443
|
||||
},
|
||||
"a03f2d8d33fbb447c1c0573735d93632dc2b81e44923f4c1": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
|
@ -219,6 +294,11 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613502517447074204
|
||||
},
|
||||
"a83cc511e399c1dab37468d0c64302bf4b68f7e3df5a1e0b": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613527553683560802
|
||||
},
|
||||
"aa9951b3b6533deadac0376a66a51e63ff6b9c306c82f4c8": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
|
@ -229,6 +309,16 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613502660685792657
|
||||
},
|
||||
"c053cc843cd5a58b6e7e4dfb0b896180734df58c6cf6ca88": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613528519484379600
|
||||
},
|
||||
"c3440286c8cf0b619ec0a5883a836115de84752c519f625c": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613525841223849602
|
||||
},
|
||||
"c558cccdd25917056e8b7b72a2a3e5f40215d707a6fac1aa": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
|
@ -274,6 +364,11 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613511459136720858
|
||||
},
|
||||
"e90c5207fefb6712f0a261708de68ca5639dfc60a4dfe6f6": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613526269423740413
|
||||
},
|
||||
"ece6e3ed1d36a43bd26843ee5efa547c662cc23423959316": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
|
@ -289,9 +384,24 @@
|
|||
"tokens": null,
|
||||
"expiration": 1613512474632135880
|
||||
},
|
||||
"f27ef3af1a5139eb23ab2d2cd5adb54dd63e147c74f12208": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613522175097030333
|
||||
},
|
||||
"f5dd3851137b73d1e039ffa521d0c02e60504aa1f972fe13": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613515512912618531
|
||||
},
|
||||
"f733dc1a7213485c2d8c78a0839d2d2157d04c108ffd1789": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613525903142014501
|
||||
},
|
||||
"ff3f257a8a8d59834194117ec388228a6c2c4c799ffee9c6": {
|
||||
"user": "uqnzie01i1nypnt9",
|
||||
"tokens": null,
|
||||
"expiration": 1613526460305916347
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -1,9 +1,9 @@
|
|||
TakenAt: 2021-02-04T03:17:07Z
|
||||
UID: pqnzigq351j2fqgn
|
||||
Type: image
|
||||
Title: Tambourine Bitches!
|
||||
Title: A really great photo!
|
||||
TitleSrc: manual
|
||||
Description: 'Sample App Description: 2021-02-09 15:59:16.724684964 -0800 PST m=+5.911590611'
|
||||
Description: 'Sample App Description: 2021-02-09 16:14:30.809303693 -0800 PST m=+5.845330665'
|
||||
DescriptionSrc: manual
|
||||
OriginalName: IMG_3044
|
||||
Year: -1
|
||||
|
@ -14,5 +14,5 @@ Details:
|
|||
Keywords: green, mean, tambourine
|
||||
KeywordsSrc: manual
|
||||
CreatedAt: 2021-02-04T03:17:14.613092062Z
|
||||
UpdatedAt: 2021-02-09T23:59:16.737466872Z
|
||||
EditedAt: 2021-02-09T23:59:17Z
|
||||
UpdatedAt: 2021-02-10T02:21:59.491263544Z
|
||||
EditedAt: 2021-02-10T02:21:59Z
|
||||
|
|
|
@ -57,3 +57,21 @@ func TestSadUpdatePhoto(t *testing.T) {
|
|||
t.Errorf("expecting failure updaitng bad photo id: %s", photo.UUID)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
func TestHappyGetPhotoDownload(t *testing.T) {
|
||||
_, err := Client.V1().GetPhotoDownload(WellKnownPhotoID)
|
||||
if err != nil {
|
||||
t.Errorf("expected success getting well known photo: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestSadGetPhotoDownload(t *testing.T) {
|
||||
file, err := Client.V1().GetPhotoDownload("1234567890")
|
||||
if err != nil {
|
||||
t.Logf("success returning error for unknown photo: %v", err)
|
||||
return
|
||||
}
|
||||
t.Errorf("expected error for unknown file: %s", file.FileName)
|
||||
t.FailNow()
|
||||
}
|
||||
|
|
79
types.go
Normal file
79
types.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package photoprism
|
||||
|
||||
type Config struct {
|
||||
Config *Options `json:"config"`
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Copyright string `json:"copyright"`
|
||||
//Debug bool `yaml:"Debug" json:"Debug" flag:"debug"`
|
||||
//Test bool `yaml:"-" json:"Test,omitempty" flag:"test"`
|
||||
//Demo bool `yaml:"Demo" json:"-" flag:"demo"`
|
||||
//Sponsor bool `yaml:"-" json:"-" flag:"sponsor"`
|
||||
//Public bool `yaml:"Public" json:"-" flag:"public"`
|
||||
//ReadOnly bool `yaml:"ReadOnly" json:"ReadOnly" flag:"read-only"`
|
||||
//Experimental bool `yaml:"Experimental" json:"Experimental" flag:"experimental"`
|
||||
//ConfigPath string `yaml:"ConfigPath" json:"-" flag:"config-path"`
|
||||
//ConfigFile string `json:"-"`
|
||||
//AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
|
||||
//OriginalsPath string `yaml:"OriginalsPath" json:"-" flag:"originals-path"`
|
||||
//OriginalsLimit int64 `yaml:"OriginalsLimit" json:"OriginalsLimit" flag:"originals-limit"`
|
||||
//ImportPath string `yaml:"ImportPath" json:"-" flag:"import-path"`
|
||||
//StoragePath string `yaml:"StoragePath" json:"-" flag:"storage-path"`
|
||||
//SidecarPath string `yaml:"SidecarPath" json:"-" flag:"sidecar-path"`
|
||||
//TempPath string `yaml:"TempPath" json:"-" flag:"temp-path"`
|
||||
//BackupPath string `yaml:"BackupPath" json:"-" flag:"backup-path"`
|
||||
//AssetsPath string `yaml:"AssetsPath" json:"-" flag:"assets-path"`
|
||||
//CachePath string `yaml:"CachePath" json:"-" flag:"cache-path"`
|
||||
//Workers int `yaml:"Workers" json:"Workers" flag:"workers"`
|
||||
//WakeupInterval int `yaml:"WakeupInterval" json:"WakeupInterval" flag:"wakeup-interval"`
|
||||
//AutoIndex int `yaml:"AutoIndex" json:"AutoIndex" flag:"auto-index"`
|
||||
//AutoImport int `yaml:"AutoImport" json:"AutoImport" flag:"auto-import"`
|
||||
//DisableBackups bool `yaml:"DisableBackups" json:"DisableBackups" flag:"disable-backups"`
|
||||
//DisableWebDAV bool `yaml:"DisableWebDAV" json:"DisableWebDAV" flag:"disable-webdav"`
|
||||
//DisableSettings bool `yaml:"DisableSettings" json:"-" flag:"disable-settings"`
|
||||
//DisablePlaces bool `yaml:"DisablePlaces" json:"DisablePlaces" flag:"disable-places"`
|
||||
//DisableExifTool bool `yaml:"DisableExifTool" json:"DisableExifTool" flag:"disable-exiftool"`
|
||||
//DisableTensorFlow bool `yaml:"DisableTensorFlow" json:"DisableTensorFlow" flag:"disable-tensorflow"`
|
||||
//DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"`
|
||||
//UploadNSFW bool `yaml:"UploadNSFW" json:"-" flag:"upload-nsfw"`
|
||||
//LogLevel string `yaml:"LogLevel" json:"-" flag:"log-level"`
|
||||
//LogFilename string `yaml:"LogFilename" json:"-" flag:"log-filename"`
|
||||
//PIDFilename string `yaml:"PIDFilename" json:"-" flag:"pid-filename"`
|
||||
//SiteUrl string `yaml:"SiteUrl" json:"SiteUrl" flag:"site-url"`
|
||||
//SitePreview string `yaml:"SitePreview" json:"SitePreview" flag:"site-preview"`
|
||||
//SiteTitle string `yaml:"SiteTitle" json:"SiteTitle" flag:"site-title"`
|
||||
//SiteCaption string `yaml:"SiteCaption" json:"SiteCaption" flag:"site-caption"`
|
||||
//SiteDescription string `yaml:"SiteDescription" json:"SiteDescription" flag:"site-description"`
|
||||
//SiteAuthor string `yaml:"SiteAuthor" json:"SiteAuthor" flag:"site-author"`
|
||||
//DatabaseDriver string `yaml:"DatabaseDriver" json:"-" flag:"database-driver"`
|
||||
//DatabaseDsn string `yaml:"DatabaseDsn" json:"-" flag:"database-dsn"`
|
||||
//DatabaseServer string `yaml:"DatabaseServer" json:"-" flag:"database-server"`
|
||||
//DatabaseName string `yaml:"DatabaseName" json:"-" flag:"database-name"`
|
||||
//DatabaseUser string `yaml:"DatabaseUser" json:"-" flag:"database-user"`
|
||||
//DatabasePassword string `yaml:"DatabasePassword" json:"-" flag:"database-password"`
|
||||
//DatabaseConns int `yaml:"DatabaseConns" json:"-" flag:"database-conns"`
|
||||
//DatabaseConnsIdle int `yaml:"DatabaseConnsIdle" json:"-" flag:"database-conns-idle"`
|
||||
//HttpHost string `yaml:"HttpHost" json:"-" flag:"http-host"`
|
||||
//HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"`
|
||||
//HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
|
||||
//HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
|
||||
//SipsBin string `yaml:"SipsBin" json:"-" flag:"sips-bin"`
|
||||
//RawtherapeeBin string `yaml:"RawtherapeeBin" json:"-" flag:"rawtherapee-bin"`
|
||||
//DarktableBin string `yaml:"DarktableBin" json:"-" flag:"darktable-bin"`
|
||||
//DarktablePresets bool `yaml:"DarktablePresets" json:"DarktablePresets" flag:"darktable-presets"`
|
||||
//HeifConvertBin string `yaml:"HeifConvertBin" json:"-" flag:"heifconvert-bin"`
|
||||
//FFmpegBin string `yaml:"FFmpegBin" json:"-" flag:"ffmpeg-bin"`
|
||||
//ExifToolBin string `yaml:"ExifToolBin" json:"-" flag:"exiftool-bin"`
|
||||
//DetachServer bool `yaml:"DetachServer" json:"-" flag:"detach-server"`
|
||||
DownloadToken string `yaml:"DownloadToken" json:"downloadToken" flag:"download-token"`
|
||||
//PreviewToken string `yaml:"PreviewToken" json:"-" flag:"preview-token"`
|
||||
//ThumbFilter string `yaml:"ThumbFilter" json:"ThumbFilter" flag:"thumb-filter"`
|
||||
//ThumbUncached bool `yaml:"ThumbUncached" json:"ThumbUncached" flag:"thumb-uncached"`
|
||||
//ThumbSize int `yaml:"ThumbSize" json:"ThumbSize" flag:"thumb-size"`
|
||||
//ThumbSizeUncached int `yaml:"ThumbSizeUncached" json:"ThumbSizeUncached" flag:"thumb-size-uncached"`
|
||||
//JpegSize int `yaml:"JpegSize" json:"JpegSize" flag:"jpeg-size"`
|
||||
//JpegQuality int `yaml:"JpegQuality" json:"JpegQuality" flag:"jpeg-quality"`
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue