Initial API and entities for link sharing
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
f1e2d86e7c
commit
a8c48ab40e
24 changed files with 419 additions and 82 deletions
12
Makefile
12
Makefile
|
@ -88,23 +88,23 @@ acceptance-firefox:
|
||||||
(cd frontend && npm run acceptance-firefox)
|
(cd frontend && npm run acceptance-firefox)
|
||||||
test-go:
|
test-go:
|
||||||
$(info Running all Go unit tests...)
|
$(info Running all Go unit tests...)
|
||||||
$(GOTEST) -parallel=1 -count=1 -tags=slow -timeout 20m ./pkg/... ./internal/...
|
$(GOTEST) -parallel 1 -count 1 -cpu 1 -tags slow -timeout 20m ./pkg/... ./internal/...
|
||||||
test-verbose:
|
test-verbose:
|
||||||
$(info Running all Go unit tests in verbose mode...)
|
$(info Running all Go unit tests in verbose mode...)
|
||||||
$(GOTEST) -parallel=1 -tags=slow -timeout 20m -v ./pkg/... ./internal/...
|
$(GOTEST) -parallel 1 -count 1 -cpu 1 -tags slow -timeout 20m -v ./pkg/... ./internal/...
|
||||||
test-short:
|
test-short:
|
||||||
$(info Running short Go unit tests in verbose mode...)
|
$(info Running short Go unit tests in verbose mode...)
|
||||||
$(GOTEST) -parallel=1 -short -timeout 5m -v ./pkg/... ./internal/...
|
$(GOTEST) -parallel 1 -count 1 -cpu 1 -short -timeout 5m -v ./pkg/... ./internal/...
|
||||||
test-race:
|
test-race:
|
||||||
$(info Running all Go unit tests with race detection in verbose mode...)
|
$(info Running all Go unit tests with race detection in verbose mode...)
|
||||||
$(GOTEST) -parallel=1 -tags=slow -race -timeout 60m -v ./pkg/... ./internal/...
|
$(GOTEST) -tags slow -race -timeout 60m -v ./pkg/... ./internal/...
|
||||||
test-codecov:
|
test-codecov:
|
||||||
$(info Running all Go unit tests with code coverage report for codecov...)
|
$(info Running all Go unit tests with code coverage report for codecov...)
|
||||||
go test -parallel=1 -count=1 -tags=slow -timeout 30m -coverprofile=coverage.txt -covermode=atomic -v ./pkg/... ./internal/...
|
go test -parallel 1 -count 1 -cpu 1 -failfast -tags slow -timeout 30m -coverprofile coverage.txt -covermode atomic -v ./pkg/... ./internal/...
|
||||||
scripts/codecov.sh
|
scripts/codecov.sh
|
||||||
test-coverage:
|
test-coverage:
|
||||||
$(info Running all Go unit tests with code coverage report...)
|
$(info Running all Go unit tests with code coverage report...)
|
||||||
go test -parallel=1 -count=1 -tags=slow -timeout 30m -coverprofile=coverage.txt -covermode=atomic -v ./pkg/... ./internal/...
|
go test -parallel 1 -count 1 -cpu 1 -failfast -tags slow -timeout 30m -coverprofile coverage.txt -covermode atomic -v ./pkg/... ./internal/...
|
||||||
go tool cover -html=coverage.txt -o coverage.html
|
go tool cover -html=coverage.txt -o coverage.html
|
||||||
clean:
|
clean:
|
||||||
rm -f $(BINARY_NAME)
|
rm -f $(BINARY_NAME)
|
||||||
|
|
|
@ -6,22 +6,23 @@ INSERT INTO cameras (id, camera_slug, camera_model, camera_make, camera_type, ca
|
||||||
INSERT INTO cameras (id, camera_slug, camera_model, camera_make, camera_type, camera_description, camera_notes, created_at, updated_at, deleted_at) VALUES (6, 'apple-iphone-6', 'iPhone 6', 'Apple', '', '', '', '2020-01-06 02:06:42', '2020-01-06 02:06:42', null);
|
INSERT INTO cameras (id, camera_slug, camera_model, camera_make, camera_type, camera_description, camera_notes, created_at, updated_at, deleted_at) VALUES (6, 'apple-iphone-6', 'iPhone 6', 'Apple', '', '', '', '2020-01-06 02:06:42', '2020-01-06 02:06:42', null);
|
||||||
INSERT INTO cameras (id, camera_slug, camera_model, camera_make, camera_type, camera_description, camera_notes, created_at, updated_at, deleted_at) VALUES (7, 'apple-iphone-7', 'iPhone 7', 'Apple', '', '', '', '2020-01-06 02:06:51', '2020-01-06 02:06:51', null);
|
INSERT INTO cameras (id, camera_slug, camera_model, camera_make, camera_type, camera_description, camera_notes, created_at, updated_at, deleted_at) VALUES (7, 'apple-iphone-7', 'iPhone 7', 'Apple', '', '', '', '2020-01-06 02:06:51', '2020-01-06 02:06:51', null);
|
||||||
INSERT INTO countries (id, country_slug, country_name, country_description, country_notes, country_photo_id) VALUES ('de', 'germany', 'Germany', 'Country Description', 'Country Notes', 0);
|
INSERT INTO countries (id, country_slug, country_name, country_description, country_notes, country_photo_id) VALUES ('de', 'germany', 'Germany', 'Country Description', 'Country Notes', 0);
|
||||||
INSERT INTO albums (id, album_uuid, album_name, album_slug, album_favorite) VALUES ('2', '3', 'Christmas2030', 'christmas2030', 0);
|
INSERT INTO albums (id, album_uuid, album_name, album_slug, album_favorite) VALUES (2, '3', 'Christmas2030', 'christmas2030', 0);
|
||||||
INSERT INTO albums (id, album_uuid, cover_uuid, album_name, album_slug, album_favorite) VALUES ('1', '4', '654', 'Holiday2030', 'holiday-2030', 1);
|
INSERT INTO albums (id, album_uuid, cover_uuid, album_name, album_slug, album_favorite) VALUES (1, '4', '654', 'Holiday2030', 'holiday-2030', 1);
|
||||||
INSERT INTO albums (id, album_uuid, cover_uuid, album_name, album_slug, album_favorite) VALUES ('3', '5', '654', 'Berlin2019', 'berlin-2019', 0);
|
INSERT INTO albums (id, album_uuid, cover_uuid, album_name, album_slug, album_favorite) VALUES (3, '5', '654', 'Berlin2019', 'berlin-2019', 0);
|
||||||
|
INSERT INTO links (link_token, link_password, link_expires, share_uuid, can_comment, can_edit, created_at, updated_at) VALUES ('1jxf3jfn2k', 'somepassword', '2050-03-06 02:06:51', '4', 1, 0, '2020-03-06 02:06:51', '2020-03-28 14:06:00');
|
||||||
INSERT INTO photos_albums (album_uuid, photo_uuid) VALUES ('4', '654');
|
INSERT INTO photos_albums (album_uuid, photo_uuid) VALUES ('4', '654');
|
||||||
INSERT INTO photos_albums (album_uuid, photo_uuid) VALUES ('5', '658');
|
INSERT INTO photos_albums (album_uuid, photo_uuid) VALUES ('5', '658');
|
||||||
INSERT INTO files (id, photo_id, photo_uuid, file_name, file_primary, file_hash, file_missing) VALUES ('1', '1', '654', 'exampleFileName.jpg', 1, '123xxx', 0);
|
INSERT INTO files (id, photo_id, photo_uuid, file_uuid, file_name, file_primary, file_hash, file_missing) VALUES (1, '1', '654', 'fq8es39w45bnlqdw', 'exampleFileName.jpg', 1, '123xxx', 0);
|
||||||
INSERT INTO files (id, photo_id, photo_uuid, file_name, file_primary, file_hash, file_missing) VALUES ('2', '2', '655', 'exampleDNGFile.dng', 1, '124xxx', 0);
|
INSERT INTO files (id, photo_id, photo_uuid, file_uuid, file_name, file_primary, file_hash, file_missing) VALUES (2, '2', '655', 'fq8ev8o1tl6umi0s', 'exampleDNGFile.dng', 1, '124xxx', 0);
|
||||||
INSERT INTO files (id, photo_id, photo_uuid, file_name, file_primary, file_hash, file_missing) VALUES ('3', '2', '655', 'exampleXmpFile.xmp', 0, '125xxx', 0);
|
INSERT INTO files (id, photo_id, photo_uuid, file_uuid, file_name, file_primary, file_hash, file_missing) VALUES (3, '2', '655', 'fq8ev8t1x0bwje4e', 'exampleXmpFile.xmp', 0, '125xxx', 0);
|
||||||
INSERT INTO files (id, photo_id, photo_uuid, file_name, file_primary, file_hash, file_missing) VALUES ('4', '5', '658', 'bridge.jpg', 1, '126xxx', 0);
|
INSERT INTO files (id, photo_id, photo_uuid, file_uuid, file_name, file_primary, file_hash, file_missing) VALUES (4, '5', '658', 'fq8ev9c3sp88uwzq', 'bridge.jpg', 1, '126xxx', 0);
|
||||||
INSERT INTO files (id, photo_id, photo_uuid, file_name, file_primary, file_hash, file_missing) VALUES ('5', '6', '659', 'reunion.jpg', 1, '127xxx', 0);
|
INSERT INTO files (id, photo_id, photo_uuid, file_uuid, file_name, file_primary, file_hash, file_missing) VALUES (5, '6', '659', 'fq8evan3urz3i48d', 'reunion.jpg', 1, '127xxx', 0);
|
||||||
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES ('1', '654', 2790, 2, '48.519235', '9.057996666666666');
|
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES (1, '654', 2790, 2, '48.519235', '9.057996666666666');
|
||||||
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES ('2', '655', 2790, 2, '48.519235', '9.057996666666666');
|
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES (2, '655', 2790, 2, '48.519235', '9.057996666666666');
|
||||||
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES ('3', '656', 1990, 3, '48.519235', '9.057996666666666');
|
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES (3, '656', 1990, 3, '48.519235', '9.057996666666666');
|
||||||
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES ('4', '657', 1990, 4, '48.519235', '9.057996666666666');
|
INSERT INTO photos (id, photo_uuid, photo_year, photo_month, photo_lat, photo_lng) VALUES (4, '657', 1990, 4, '48.519235', '9.057996666666666');
|
||||||
INSERT INTO photos (id, photo_uuid, taken_at, photo_lat, photo_lng, photo_title) VALUES ('5', '658', '2014-07-17 15:42:12', '48.519235', '9.057996666666666', 'Neckarbrücke');
|
INSERT INTO photos (id, photo_uuid, taken_at, photo_lat, photo_lng, photo_title) VALUES (5, '658', '2014-07-17 15:42:12', '48.519235', '9.057996666666666', 'Neckarbrücke');
|
||||||
INSERT INTO photos (id, photo_uuid, taken_at, photo_lat, photo_lng, photo_title) VALUES ('6', '659', '2015-11-11 09:07:18', '-21.34263611111111', '55.466944444444444', 'Reunion');
|
INSERT INTO photos (id, photo_uuid, taken_at, photo_lat, photo_lng, photo_title) VALUES (6, '659', '2015-11-11 09:07:18', '-21.34263611111111', '55.466944444444444', 'Reunion');
|
||||||
INSERT INTO keywords (id, keyword, skip) VALUES (1, 'bridge', 0);
|
INSERT INTO keywords (id, keyword, skip) VALUES (1, 'bridge', 0);
|
||||||
INSERT INTO keywords (id, keyword, skip) VALUES (2, 'beach', 0);
|
INSERT INTO keywords (id, keyword, skip) VALUES (2, 'beach', 0);
|
||||||
INSERT INTO photos_keywords (photo_id, keyword_id) VALUES (5, 1);
|
INSERT INTO photos_keywords (photo_id, keyword_id) VALUES (5, 1);
|
||||||
|
|
|
@ -114,6 +114,14 @@ class Abstract {
|
||||||
return this.constructor.getModelName() + " " + this.getId();
|
return this.constructor.getModelName() + " " + this.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addLink(password, expires, comment, edit) {
|
||||||
|
expires = expires ? parseInt(expires) : 0;
|
||||||
|
comment = !!comment;
|
||||||
|
edit = !!edit;
|
||||||
|
const values = {password, expires, comment, edit};
|
||||||
|
return Api.post(this.getEntityResource() + "/link", values).then((response) => Promise.resolve(this.setValues(response.data)));
|
||||||
|
}
|
||||||
|
|
||||||
static getCollectionResource() {
|
static getCollectionResource() {
|
||||||
throw new Error("getCollectionResource() needs to be implemented");
|
throw new Error("getCollectionResource() needs to be implemented");
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ class Album extends Abstract {
|
||||||
AlbumRadius: 0,
|
AlbumRadius: 0,
|
||||||
AlbumOrder: "",
|
AlbumOrder: "",
|
||||||
AlbumTemplate: "",
|
AlbumTemplate: "",
|
||||||
|
Links: [],
|
||||||
CreatedAt: "",
|
CreatedAt: "",
|
||||||
UpdatedAt: "",
|
UpdatedAt: "",
|
||||||
DeletedAt: null,
|
DeletedAt: null,
|
||||||
|
|
|
@ -17,6 +17,7 @@ class Label extends Abstract {
|
||||||
LabelFavorite: false,
|
LabelFavorite: false,
|
||||||
LabelDescription: "",
|
LabelDescription: "",
|
||||||
LabelNotes: "",
|
LabelNotes: "",
|
||||||
|
Links: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
31
frontend/src/model/link.js
Normal file
31
frontend/src/model/link.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import Abstract from "model/abstract";
|
||||||
|
|
||||||
|
class Link extends Abstract {
|
||||||
|
getDefaults() {
|
||||||
|
return {
|
||||||
|
LinkToken: "",
|
||||||
|
LinkPassword: "",
|
||||||
|
LinkExpires: "",
|
||||||
|
ShareUUID: "",
|
||||||
|
CanComment: false,
|
||||||
|
CanEdit: false,
|
||||||
|
CreatedAt: "",
|
||||||
|
UpdatedAt: "",
|
||||||
|
Links: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.LinkToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getCollectionResource() {
|
||||||
|
return "links";
|
||||||
|
}
|
||||||
|
|
||||||
|
static getModelName() {
|
||||||
|
return "Link";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Link;
|
|
@ -56,6 +56,7 @@ class Photo extends Abstract {
|
||||||
Labels: [],
|
Labels: [],
|
||||||
Keywords: [],
|
Keywords: [],
|
||||||
Albums: [],
|
Albums: [],
|
||||||
|
Links: [],
|
||||||
CreatedAt: "",
|
CreatedAt: "",
|
||||||
UpdatedAt: "",
|
UpdatedAt: "",
|
||||||
DeletedAt: null,
|
DeletedAt: null,
|
||||||
|
|
|
@ -3,12 +3,13 @@ package api
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// API test helper
|
// NewApiTest returns new API test helper
|
||||||
func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, conf *config.Config) {
|
func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, conf *config.Config) {
|
||||||
conf = config.TestConfig()
|
conf = config.TestConfig()
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
@ -17,6 +18,7 @@ func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, conf *config.Config
|
||||||
return app, router, conf
|
return app, router, conf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performs API request with empty request body.
|
||||||
// See https://medium.com/@craigchilds94/testing-gin-json-responses-1f258ce3b0b1
|
// See https://medium.com/@craigchilds94/testing-gin-json-responses-1f258ce3b0b1
|
||||||
func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
||||||
req, _ := http.NewRequest(method, path, nil)
|
req, _ := http.NewRequest(method, path, nil)
|
||||||
|
@ -24,3 +26,12 @@ func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecor
|
||||||
r.ServeHTTP(w, req)
|
r.ServeHTTP(w, req)
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performs API request including request body as string.
|
||||||
|
func PerformRequestWithBody(r http.Handler, method, path, body string) *httptest.ResponseRecorder {
|
||||||
|
reader := strings.NewReader(body)
|
||||||
|
req, _ := http.NewRequest(method, path, reader)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
|
@ -5,13 +5,15 @@ import (
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /api/v1/files/:hash
|
// GET /api/v1/files/:hash
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// hash: string The sha1 hash of a file
|
// hash: string SHA-1 hash of the file
|
||||||
func GetFile(router *gin.RouterGroup, conf *config.Config) {
|
func GetFile(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.GET("/files/:hash", func(c *gin.Context) {
|
router.GET("/files/:hash", func(c *gin.Context) {
|
||||||
if Unauthorized(c, conf) {
|
if Unauthorized(c, conf) {
|
||||||
|
@ -30,3 +32,37 @@ func GetFile(router *gin.RouterGroup, conf *config.Config) {
|
||||||
c.JSON(http.StatusOK, p)
|
c.JSON(http.StatusOK, p)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /api/v1/files/:hash/link
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// hash: string SHA-1 hash of the file
|
||||||
|
func LinkFile(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
router.POST("/files/:hash/link", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := conf.Db()
|
||||||
|
q := query.New(db)
|
||||||
|
|
||||||
|
m, err := q.FileByUUID(c.Param("hash"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if link, err := newLink(c); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
db.Model(&m).Association("Links").Append(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
event.Success("created file share link")
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, m)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
125
internal/api/link.go
Normal file
125
internal/api/link.go
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newLink returns a new link entity initialized with request data
|
||||||
|
func newLink(c *gin.Context) (link entity.Link, err error) {
|
||||||
|
var f form.NewLink
|
||||||
|
|
||||||
|
if err := c.BindJSON(&f); err != nil {
|
||||||
|
return link, err
|
||||||
|
}
|
||||||
|
|
||||||
|
link = entity.NewLink(f.Password, f.CanComment, f.CanEdit)
|
||||||
|
|
||||||
|
if f.Expires > 0 {
|
||||||
|
expires := time.Now().Add(time.Duration(f.Expires) * time.Second)
|
||||||
|
link.LinkExpires = &expires
|
||||||
|
}
|
||||||
|
|
||||||
|
return link, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/v1/albums/:uuid/link
|
||||||
|
func LinkAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
router.POST("/albums/:uuid/link", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := conf.Db()
|
||||||
|
q := query.New(db)
|
||||||
|
|
||||||
|
m, err := q.AlbumByUUID(c.Param("uuid"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if link, err := newLink(c); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
db.Model(&m).Association("Links").Append(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
event.Success("created album share link")
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, m)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/v1/photos/:uuid/link
|
||||||
|
func LinkPhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
router.POST("/photos/:uuid/link", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := conf.Db()
|
||||||
|
q := query.New(db)
|
||||||
|
|
||||||
|
m, err := q.PhotoByUUID(c.Param("uuid"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if link, err := newLink(c); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
db.Model(&m).Association("Links").Append(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
event.Success("created photo share link")
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, m)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/v1/labels/:uuid/link
|
||||||
|
func LinkLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
router.POST("/labels/:uuid/link", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := conf.Db()
|
||||||
|
q := query.New(db)
|
||||||
|
|
||||||
|
m, err := q.LabelByUUID(c.Param("uuid"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if link, err := newLink(c); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
db.Model(&m).Association("Links").Append(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
event.Success("created label share link")
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, m)
|
||||||
|
})
|
||||||
|
}
|
54
internal/api/link_test.go
Normal file
54
internal/api/link_test.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLinkAlbum(t *testing.T) {
|
||||||
|
t.Run("create share link", func(t *testing.T) {
|
||||||
|
app, router, ctx := NewApiTest()
|
||||||
|
|
||||||
|
var album entity.Album
|
||||||
|
|
||||||
|
LinkAlbum(router, ctx)
|
||||||
|
|
||||||
|
result1 := PerformRequestWithBody(app, "POST", "/api/v1/albums/3/link", `{"password": "foobar", "expires": 0, "edit": true}`)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, result1.Code)
|
||||||
|
|
||||||
|
if err := json.Unmarshal(result1.Body.Bytes(), &album); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(album.Links) != 1 {
|
||||||
|
t.Fatalf("one link expected: %d, %+v", len(album.Links), album)
|
||||||
|
}
|
||||||
|
|
||||||
|
link := album.Links[0]
|
||||||
|
|
||||||
|
assert.Equal(t, "foobar", link.LinkPassword)
|
||||||
|
assert.Nil(t, link.LinkExpires)
|
||||||
|
assert.False(t, link.CanComment)
|
||||||
|
assert.True(t, link.CanEdit)
|
||||||
|
|
||||||
|
result2 := PerformRequestWithBody(app, "POST", "/api/v1/albums/3/link", `{"password": "", "expires": 3600}`)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, result2.Code)
|
||||||
|
|
||||||
|
// t.Logf("result1: %s", result1.Body.String())
|
||||||
|
// t.Logf("result2: %s", result2.Body.String())
|
||||||
|
|
||||||
|
if err := json.Unmarshal(result2.Body.Bytes(), &album); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(album.Links) != 2 {
|
||||||
|
t.Fatal("two links expected")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -73,7 +73,6 @@ func (c *Config) MigrateDb() {
|
||||||
&entity.Camera{},
|
&entity.Camera{},
|
||||||
&entity.Lens{},
|
&entity.Lens{},
|
||||||
&entity.Country{},
|
&entity.Country{},
|
||||||
|
|
||||||
&entity.Album{},
|
&entity.Album{},
|
||||||
&entity.PhotoAlbum{},
|
&entity.PhotoAlbum{},
|
||||||
&entity.Label{},
|
&entity.Label{},
|
||||||
|
@ -81,12 +80,49 @@ func (c *Config) MigrateDb() {
|
||||||
&entity.PhotoLabel{},
|
&entity.PhotoLabel{},
|
||||||
&entity.Keyword{},
|
&entity.Keyword{},
|
||||||
&entity.PhotoKeyword{},
|
&entity.PhotoKeyword{},
|
||||||
|
&entity.Link{},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity.CreateUnknownPlace(db)
|
entity.CreateUnknownPlace(db)
|
||||||
entity.CreateUnknownCountry(db)
|
entity.CreateUnknownCountry(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DropTables drops all tables in the currently configured database (be careful!).
|
||||||
|
func (c *Config) DropTables() {
|
||||||
|
db := c.Db()
|
||||||
|
|
||||||
|
logLevel := log.Level
|
||||||
|
|
||||||
|
log.SetLevel(logrus.FatalLevel)
|
||||||
|
db.SetLogger(log)
|
||||||
|
db.LogMode(false)
|
||||||
|
|
||||||
|
db.DropTableIfExists(
|
||||||
|
&entity.Account{},
|
||||||
|
&entity.File{},
|
||||||
|
&entity.FileShare{},
|
||||||
|
&entity.FileSync{},
|
||||||
|
&entity.Photo{},
|
||||||
|
&entity.Description{},
|
||||||
|
&entity.Event{},
|
||||||
|
&entity.Place{},
|
||||||
|
&entity.Location{},
|
||||||
|
&entity.Camera{},
|
||||||
|
&entity.Lens{},
|
||||||
|
&entity.Country{},
|
||||||
|
&entity.Album{},
|
||||||
|
&entity.PhotoAlbum{},
|
||||||
|
&entity.Label{},
|
||||||
|
&entity.Category{},
|
||||||
|
&entity.PhotoLabel{},
|
||||||
|
&entity.Keyword{},
|
||||||
|
&entity.PhotoKeyword{},
|
||||||
|
&entity.Link{},
|
||||||
|
)
|
||||||
|
|
||||||
|
log.SetLevel(logLevel)
|
||||||
|
}
|
||||||
|
|
||||||
// connectToDatabase establishes a database connection.
|
// connectToDatabase establishes a database connection.
|
||||||
// When used with the internal driver, it may create a new database server instance.
|
// When used with the internal driver, it may create a new database server instance.
|
||||||
// It tries to do this 12 times with a 5 second sleep interval in between.
|
// It tries to do this 12 times with a 5 second sleep interval in between.
|
||||||
|
@ -153,42 +189,6 @@ func (c *Config) connectToDatabase(ctx context.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DropTables drops all tables in the currently configured database (be careful!).
|
|
||||||
func (c *Config) DropTables() {
|
|
||||||
db := c.Db()
|
|
||||||
|
|
||||||
logLevel := log.Level
|
|
||||||
|
|
||||||
log.SetLevel(logrus.FatalLevel)
|
|
||||||
db.SetLogger(log)
|
|
||||||
db.LogMode(false)
|
|
||||||
|
|
||||||
db.DropTableIfExists(
|
|
||||||
&entity.Account{},
|
|
||||||
&entity.File{},
|
|
||||||
&entity.FileShare{},
|
|
||||||
&entity.FileSync{},
|
|
||||||
&entity.Photo{},
|
|
||||||
&entity.Description{},
|
|
||||||
&entity.Event{},
|
|
||||||
&entity.Place{},
|
|
||||||
&entity.Location{},
|
|
||||||
&entity.Camera{},
|
|
||||||
&entity.Lens{},
|
|
||||||
&entity.Country{},
|
|
||||||
|
|
||||||
&entity.Album{},
|
|
||||||
&entity.PhotoAlbum{},
|
|
||||||
&entity.Label{},
|
|
||||||
&entity.Category{},
|
|
||||||
&entity.PhotoLabel{},
|
|
||||||
&entity.Keyword{},
|
|
||||||
&entity.PhotoKeyword{},
|
|
||||||
)
|
|
||||||
|
|
||||||
log.SetLevel(logLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportSQL imports a file to the currently configured database.
|
// ImportSQL imports a file to the currently configured database.
|
||||||
func (c *Config) ImportSQL(filename string) {
|
func (c *Config) ImportSQL(filename string) {
|
||||||
contents, err := ioutil.ReadFile(filename)
|
contents, err := ioutil.ReadFile(filename)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -19,11 +18,10 @@ type Album struct {
|
||||||
AlbumName string `gorm:"type:varchar(128);"`
|
AlbumName string `gorm:"type:varchar(128);"`
|
||||||
AlbumDescription string `gorm:"type:text;"`
|
AlbumDescription string `gorm:"type:text;"`
|
||||||
AlbumNotes string `gorm:"type:text;"`
|
AlbumNotes string `gorm:"type:text;"`
|
||||||
AlbumFavorite bool
|
|
||||||
AlbumOrder string `gorm:"type:varbinary(32);"`
|
AlbumOrder string `gorm:"type:varbinary(32);"`
|
||||||
ShareTemplate string `gorm:"type:varbinary(256);"`
|
AlbumTemplate string `gorm:"type:varbinary(256);"`
|
||||||
SharePassword string `gorm:"type:varbinary(256);"`
|
AlbumFavorite bool
|
||||||
ShareExpires sql.NullTime
|
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:AlbumUUID"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt *time.Time `sql:"index"`
|
DeletedAt *time.Time `sql:"index"`
|
||||||
|
|
|
@ -40,8 +40,9 @@ type File struct {
|
||||||
FileChroma uint
|
FileChroma uint
|
||||||
FileNotes string `gorm:"type:text"`
|
FileNotes string `gorm:"type:text"`
|
||||||
FileError string `gorm:"type:varbinary(512)"`
|
FileError string `gorm:"type:varbinary(512)"`
|
||||||
FileShare []FileShare
|
Share []FileShare
|
||||||
FileSync []FileSync
|
Sync []FileSync
|
||||||
|
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:FileUUID"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
CreatedIn int64
|
CreatedIn int64
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
|
|
@ -22,6 +22,7 @@ type Label struct {
|
||||||
LabelDescription string `gorm:"type:text;"`
|
LabelDescription string `gorm:"type:text;"`
|
||||||
LabelNotes string `gorm:"type:text;"`
|
LabelNotes string `gorm:"type:text;"`
|
||||||
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id"`
|
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id"`
|
||||||
|
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:LabelUUID"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt *time.Time `sql:"index"`
|
DeletedAt *time.Time `sql:"index"`
|
||||||
|
|
42
internal/entity/link.go
Normal file
42
internal/entity/link.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"github.com/photoprism/photoprism/pkg/rnd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Link represents a sharing link.
|
||||||
|
type Link struct {
|
||||||
|
LinkToken string `gorm:"type:varbinary(256);primary_key;"`
|
||||||
|
LinkPassword string `gorm:"type:varbinary(256);"`
|
||||||
|
LinkExpires *time.Time `gorm:"type:datetime;"`
|
||||||
|
ShareUUID string `gorm:"type:varbinary(36);index;"`
|
||||||
|
CanComment bool
|
||||||
|
CanEdit bool
|
||||||
|
CreatedAt time.Time `deepcopier:"skip"`
|
||||||
|
UpdatedAt time.Time `deepcopier:"skip"`
|
||||||
|
DeletedAt *time.Time `deepcopier:"skip" sql:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate creates a new URL token when a new link is created.
|
||||||
|
func (m *Link) BeforeCreate(scope *gorm.Scope) error {
|
||||||
|
if err := scope.SetColumn("LinkToken", rnd.Token(10)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLink creates a sharing link.
|
||||||
|
func NewLink(password string, canComment, canEdit bool) Link {
|
||||||
|
result := Link{
|
||||||
|
LinkToken: rnd.Token(10),
|
||||||
|
LinkPassword: password,
|
||||||
|
CanComment: canComment,
|
||||||
|
CanEdit: canEdit,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -51,6 +51,7 @@ type Photo struct {
|
||||||
Lens *Lens `json:"Lens"`
|
Lens *Lens `json:"Lens"`
|
||||||
Location *Location `json:"-"`
|
Location *Location `json:"-"`
|
||||||
Place *Place `json:"-"`
|
Place *Place `json:"-"`
|
||||||
|
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:PhotoUUID"`
|
||||||
Keywords []Keyword `json:"-"`
|
Keywords []Keyword `json:"-"`
|
||||||
Albums []Album `json:"-"`
|
Albums []Album `json:"-"`
|
||||||
Files []File
|
Files []File
|
||||||
|
|
9
internal/form/new_link.go
Normal file
9
internal/form/new_link.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package form
|
||||||
|
|
||||||
|
// Link represents a sharing link form.
|
||||||
|
type NewLink struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
Expires int `json:"expires"`
|
||||||
|
CanComment bool `json:"comment"`
|
||||||
|
CanEdit bool `json:"edit"`
|
||||||
|
}
|
|
@ -23,11 +23,12 @@ type AlbumResult struct {
|
||||||
AlbumFavorite bool
|
AlbumFavorite bool
|
||||||
AlbumDescription string
|
AlbumDescription string
|
||||||
AlbumNotes string
|
AlbumNotes string
|
||||||
|
LinkCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// AlbumByUUID returns a Album based on the UUID.
|
// AlbumByUUID returns a Album based on the UUID.
|
||||||
func (q *Query) AlbumByUUID(albumUUID string) (album entity.Album, err error) {
|
func (q *Query) AlbumByUUID(albumUUID string) (album entity.Album, err error) {
|
||||||
if err := q.db.Where("album_uuid = ?", albumUUID).First(&album).Error; err != nil {
|
if err := q.db.Where("album_uuid = ?", albumUUID).Preload("Links").First(&album).Error; err != nil {
|
||||||
return album, err
|
return album, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,8 +60,11 @@ func (q *Query) Albums(f form.AlbumSearch) (results []AlbumResult, err error) {
|
||||||
s := q.db.NewScope(nil).DB()
|
s := q.db.NewScope(nil).DB()
|
||||||
|
|
||||||
s = s.Table("albums").
|
s = s.Table("albums").
|
||||||
Select(`albums.*, COUNT(photos_albums.album_uuid) AS album_count`).
|
Select(`albums.*,
|
||||||
|
COUNT(photos_albums.album_uuid) AS album_count,
|
||||||
|
COUNT(links.link_token) AS link_count`).
|
||||||
Joins("LEFT JOIN photos_albums ON photos_albums.album_uuid = albums.album_uuid").
|
Joins("LEFT JOIN photos_albums ON photos_albums.album_uuid = albums.album_uuid").
|
||||||
|
Joins("LEFT JOIN links ON links.share_uuid = albums.album_uuid").
|
||||||
Where("albums.deleted_at IS NULL").
|
Where("albums.deleted_at IS NULL").
|
||||||
Group("albums.id")
|
Group("albums.id")
|
||||||
|
|
||||||
|
|
|
@ -23,16 +23,16 @@ func (q *Query) FilesByUUID(u []string, limit int, offset int) (files []entity.F
|
||||||
|
|
||||||
// FileByPhotoUUID
|
// FileByPhotoUUID
|
||||||
func (q *Query) FileByPhotoUUID(u string) (file entity.File, err error) {
|
func (q *Query) FileByPhotoUUID(u string) (file entity.File, err error) {
|
||||||
if err := q.db.Where("photo_uuid = ? AND file_primary = 1", u).Preload("Photo").First(&file).Error; err != nil {
|
if err := q.db.Where("photo_uuid = ? AND file_primary = 1", u).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
|
||||||
return file, err
|
return file, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return file, nil
|
return file, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileByID returns a MediaFile given a certain ID.
|
// FileByUUID returns the file entity for a given UUID.
|
||||||
func (q *Query) FileByID(id string) (file entity.File, err error) {
|
func (q *Query) FileByUUID(uuid string) (file entity.File, err error) {
|
||||||
if err := q.db.Where("id = ?", id).Preload("Photo").First(&file).Error; err != nil {
|
if err := q.db.Where("file_uuid = ?", uuid).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
|
||||||
return file, err
|
return file, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ func (q *Query) FileByID(id string) (file entity.File, err error) {
|
||||||
|
|
||||||
// FirstFileByHash finds a file with a given hash string.
|
// FirstFileByHash finds a file with a given hash string.
|
||||||
func (q *Query) FileByHash(fileHash string) (file entity.File, err error) {
|
func (q *Query) FileByHash(fileHash string) (file entity.File, err error) {
|
||||||
if err := q.db.Where("file_hash = ?", fileHash).Preload("Photo").First(&file).Error; err != nil {
|
if err := q.db.Where("file_hash = ?", fileHash).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
|
||||||
return file, err
|
return file, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,20 +54,27 @@ func TestQuery_FileByPhotoUUID(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQuery_FileByID(t *testing.T) {
|
func TestQuery_FileByUUID(t *testing.T) {
|
||||||
conf := config.TestConfig()
|
conf := config.TestConfig()
|
||||||
|
|
||||||
search := New(conf.Db())
|
search := New(conf.Db())
|
||||||
|
|
||||||
t.Run("files found", func(t *testing.T) {
|
t.Run("files found", func(t *testing.T) {
|
||||||
file, err := search.FileByID("3")
|
file, err := search.FileByUUID("fq8es39w45bnlqdw")
|
||||||
|
|
||||||
assert.Nil(t, err)
|
if err != nil {
|
||||||
assert.Equal(t, "exampleXmpFile.xmp", file.FileName)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "exampleFileName.jpg", file.FileName)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("no files found", func(t *testing.T) {
|
t.Run("no files found", func(t *testing.T) {
|
||||||
file, err := search.FileByID("111")
|
file, err := search.FileByUUID("111")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("error expected")
|
||||||
|
}
|
||||||
|
|
||||||
assert.Error(t, err, "record not found")
|
assert.Error(t, err, "record not found")
|
||||||
t.Log(file)
|
t.Log(file)
|
||||||
|
|
|
@ -30,7 +30,7 @@ type LabelResult struct {
|
||||||
|
|
||||||
// LabelBySlug returns a Label based on the slug name.
|
// LabelBySlug returns a Label based on the slug name.
|
||||||
func (q *Query) LabelBySlug(labelSlug string) (label entity.Label, err error) {
|
func (q *Query) LabelBySlug(labelSlug string) (label entity.Label, err error) {
|
||||||
if err := q.db.Where("label_slug = ?", labelSlug).First(&label).Error; err != nil {
|
if err := q.db.Where("label_slug = ?", labelSlug).Preload("Links").First(&label).Error; err != nil {
|
||||||
return label, err
|
return label, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ func (q *Query) LabelBySlug(labelSlug string) (label entity.Label, err error) {
|
||||||
|
|
||||||
// LabelByUUID returns a Label based on the label UUID.
|
// LabelByUUID returns a Label based on the label UUID.
|
||||||
func (q *Query) LabelByUUID(labelUUID string) (label entity.Label, err error) {
|
func (q *Query) LabelByUUID(labelUUID string) (label entity.Label, err error) {
|
||||||
if err := q.db.Where("label_uuid = ?", labelUUID).First(&label).Error; err != nil {
|
if err := q.db.Where("label_uuid = ?", labelUUID).Preload("Links").First(&label).Error; err != nil {
|
||||||
return label, err
|
return label, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -337,7 +337,7 @@ func (q *Query) Photos(f form.PhotoSearch) (results []PhotoResult, err error) {
|
||||||
|
|
||||||
// PhotoByID returns a Photo based on the ID.
|
// PhotoByID returns a Photo based on the ID.
|
||||||
func (q *Query) PhotoByID(photoID uint64) (photo entity.Photo, err error) {
|
func (q *Query) PhotoByID(photoID uint64) (photo entity.Photo, err error) {
|
||||||
if err := q.db.Where("id = ?", photoID).Preload("Description").First(&photo).Error; err != nil {
|
if err := q.db.Where("id = ?", photoID).Preload("Links").Preload("Description").First(&photo).Error; err != nil {
|
||||||
return photo, err
|
return photo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,7 +346,7 @@ func (q *Query) PhotoByID(photoID uint64) (photo entity.Photo, err error) {
|
||||||
|
|
||||||
// PhotoByUUID returns a Photo based on the UUID.
|
// PhotoByUUID returns a Photo based on the UUID.
|
||||||
func (q *Query) PhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
|
func (q *Query) PhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
|
||||||
if err := q.db.Where("photo_uuid = ?", photoUUID).Preload("Description").First(&photo).Error; err != nil {
|
if err := q.db.Where("photo_uuid = ?", photoUUID).Preload("Links").Preload("Description").First(&photo).Error; err != nil {
|
||||||
return photo, err
|
return photo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,6 +362,7 @@ func (q *Query) PreloadPhotoByUUID(photoUUID string) (photo entity.Photo, err er
|
||||||
Preload("Labels.Label").
|
Preload("Labels.Label").
|
||||||
Preload("Camera").
|
Preload("Camera").
|
||||||
Preload("Lens").
|
Preload("Lens").
|
||||||
|
Preload("Links").
|
||||||
Preload("Description").
|
Preload("Description").
|
||||||
First(&photo).Error; err != nil {
|
First(&photo).Error; err != nil {
|
||||||
return photo, err
|
return photo, err
|
||||||
|
|
|
@ -32,15 +32,18 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
||||||
api.UpdatePhoto(v1, conf)
|
api.UpdatePhoto(v1, conf)
|
||||||
api.GetPhotos(v1, conf)
|
api.GetPhotos(v1, conf)
|
||||||
api.GetPhotoDownload(v1, conf)
|
api.GetPhotoDownload(v1, conf)
|
||||||
|
api.LinkPhoto(v1, conf)
|
||||||
api.LikePhoto(v1, conf)
|
api.LikePhoto(v1, conf)
|
||||||
api.DislikePhoto(v1, conf)
|
api.DislikePhoto(v1, conf)
|
||||||
api.AddPhotoLabel(v1, conf)
|
api.AddPhotoLabel(v1, conf)
|
||||||
api.RemovePhotoLabel(v1, conf)
|
api.RemovePhotoLabel(v1, conf)
|
||||||
api.GetMomentsTime(v1, conf)
|
api.GetMomentsTime(v1, conf)
|
||||||
api.GetFile(v1, conf)
|
api.GetFile(v1, conf)
|
||||||
|
api.LinkFile(v1, conf)
|
||||||
|
|
||||||
api.GetLabels(v1, conf)
|
api.GetLabels(v1, conf)
|
||||||
api.UpdateLabel(v1, conf)
|
api.UpdateLabel(v1, conf)
|
||||||
|
api.LinkLabel(v1, conf)
|
||||||
api.LikeLabel(v1, conf)
|
api.LikeLabel(v1, conf)
|
||||||
api.DislikeLabel(v1, conf)
|
api.DislikeLabel(v1, conf)
|
||||||
api.LabelThumbnail(v1, conf)
|
api.LabelThumbnail(v1, conf)
|
||||||
|
@ -64,6 +67,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
||||||
api.DeleteAlbum(v1, conf)
|
api.DeleteAlbum(v1, conf)
|
||||||
api.DownloadAlbum(v1, conf)
|
api.DownloadAlbum(v1, conf)
|
||||||
api.GetAlbums(v1, conf)
|
api.GetAlbums(v1, conf)
|
||||||
|
api.LinkAlbum(v1, conf)
|
||||||
api.LikeAlbum(v1, conf)
|
api.LikeAlbum(v1, conf)
|
||||||
api.DislikeAlbum(v1, conf)
|
api.DislikeAlbum(v1, conf)
|
||||||
api.AlbumThumbnail(v1, conf)
|
api.AlbumThumbnail(v1, conf)
|
||||||
|
|
Loading…
Reference in a new issue