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)
|
||||
test-go:
|
||||
$(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:
|
||||
$(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:
|
||||
$(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:
|
||||
$(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:
|
||||
$(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
|
||||
test-coverage:
|
||||
$(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
|
||||
clean:
|
||||
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 (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 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 ('3', '5', '654', 'Berlin2019', 'berlin-2019', 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 (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 ('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_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_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_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_name, file_primary, file_hash, file_missing) VALUES ('5', '6', '659', '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 ('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 ('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 ('6', '659', '2015-11-11 09:07:18', '-21.34263611111111', '55.466944444444444', 'Reunion');
|
||||
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_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_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_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_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 (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 (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 (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 (2, 'beach', 0);
|
||||
INSERT INTO photos_keywords (photo_id, keyword_id) VALUES (5, 1);
|
||||
|
|
|
@ -114,6 +114,14 @@ class Abstract {
|
|||
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() {
|
||||
throw new Error("getCollectionResource() needs to be implemented");
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ class Album extends Abstract {
|
|||
AlbumRadius: 0,
|
||||
AlbumOrder: "",
|
||||
AlbumTemplate: "",
|
||||
Links: [],
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
DeletedAt: null,
|
||||
|
|
|
@ -17,6 +17,7 @@ class Label extends Abstract {
|
|||
LabelFavorite: false,
|
||||
LabelDescription: "",
|
||||
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: [],
|
||||
Keywords: [],
|
||||
Albums: [],
|
||||
Links: [],
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
DeletedAt: null,
|
||||
|
|
|
@ -3,12 +3,13 @@ package api
|
|||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"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) {
|
||||
conf = config.TestConfig()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
@ -17,6 +18,7 @@ func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, conf *config.Config
|
|||
return app, router, conf
|
||||
}
|
||||
|
||||
// Performs API request with empty request body.
|
||||
// See https://medium.com/@craigchilds94/testing-gin-json-responses-1f258ce3b0b1
|
||||
func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
||||
req, _ := http.NewRequest(method, path, nil)
|
||||
|
@ -24,3 +26,12 @@ func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecor
|
|||
r.ServeHTTP(w, req)
|
||||
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/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// GET /api/v1/files/:hash
|
||||
//
|
||||
// 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) {
|
||||
router.GET("/files/:hash", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
|
@ -30,3 +32,37 @@ func GetFile(router *gin.RouterGroup, conf *config.Config) {
|
|||
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.Lens{},
|
||||
&entity.Country{},
|
||||
|
||||
&entity.Album{},
|
||||
&entity.PhotoAlbum{},
|
||||
&entity.Label{},
|
||||
|
@ -81,12 +80,49 @@ func (c *Config) MigrateDb() {
|
|||
&entity.PhotoLabel{},
|
||||
&entity.Keyword{},
|
||||
&entity.PhotoKeyword{},
|
||||
&entity.Link{},
|
||||
)
|
||||
|
||||
entity.CreateUnknownPlace(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.
|
||||
// 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.
|
||||
|
@ -153,42 +189,6 @@ func (c *Config) connectToDatabase(ctx context.Context) error {
|
|||
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.
|
||||
func (c *Config) ImportSQL(filename string) {
|
||||
contents, err := ioutil.ReadFile(filename)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -19,11 +18,10 @@ type Album struct {
|
|||
AlbumName string `gorm:"type:varchar(128);"`
|
||||
AlbumDescription string `gorm:"type:text;"`
|
||||
AlbumNotes string `gorm:"type:text;"`
|
||||
AlbumFavorite bool
|
||||
AlbumOrder string `gorm:"type:varbinary(32);"`
|
||||
ShareTemplate string `gorm:"type:varbinary(256);"`
|
||||
SharePassword string `gorm:"type:varbinary(256);"`
|
||||
ShareExpires sql.NullTime
|
||||
AlbumTemplate string `gorm:"type:varbinary(256);"`
|
||||
AlbumFavorite bool
|
||||
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:AlbumUUID"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `sql:"index"`
|
||||
|
|
|
@ -40,8 +40,9 @@ type File struct {
|
|||
FileChroma uint
|
||||
FileNotes string `gorm:"type:text"`
|
||||
FileError string `gorm:"type:varbinary(512)"`
|
||||
FileShare []FileShare
|
||||
FileSync []FileSync
|
||||
Share []FileShare
|
||||
Sync []FileSync
|
||||
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:FileUUID"`
|
||||
CreatedAt time.Time
|
||||
CreatedIn int64
|
||||
UpdatedAt time.Time
|
||||
|
|
|
@ -22,6 +22,7 @@ type Label struct {
|
|||
LabelDescription string `gorm:"type:text;"`
|
||||
LabelNotes string `gorm:"type:text;"`
|
||||
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id"`
|
||||
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:LabelUUID"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
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"`
|
||||
Location *Location `json:"-"`
|
||||
Place *Place `json:"-"`
|
||||
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:PhotoUUID"`
|
||||
Keywords []Keyword `json:"-"`
|
||||
Albums []Album `json:"-"`
|
||||
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
|
||||
AlbumDescription string
|
||||
AlbumNotes string
|
||||
LinkCount int
|
||||
}
|
||||
|
||||
// AlbumByUUID returns a Album based on the UUID.
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -59,8 +60,11 @@ func (q *Query) Albums(f form.AlbumSearch) (results []AlbumResult, err error) {
|
|||
s := q.db.NewScope(nil).DB()
|
||||
|
||||
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 links ON links.share_uuid = albums.album_uuid").
|
||||
Where("albums.deleted_at IS NULL").
|
||||
Group("albums.id")
|
||||
|
||||
|
|
|
@ -23,16 +23,16 @@ func (q *Query) FilesByUUID(u []string, limit int, offset int) (files []entity.F
|
|||
|
||||
// FileByPhotoUUID
|
||||
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, nil
|
||||
}
|
||||
|
||||
// FileByID returns a MediaFile given a certain ID.
|
||||
func (q *Query) FileByID(id string) (file entity.File, err error) {
|
||||
if err := q.db.Where("id = ?", id).Preload("Photo").First(&file).Error; err != nil {
|
||||
// FileByUUID returns the file entity for a given UUID.
|
||||
func (q *Query) FileByUUID(uuid string) (file entity.File, err error) {
|
||||
if err := q.db.Where("file_uuid = ?", uuid).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
t.Run("files found", func(t *testing.T) {
|
||||
file, err := search.FileByID("3")
|
||||
file, err := search.FileByUUID("fq8es39w45bnlqdw")
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "exampleXmpFile.xmp", file.FileName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "exampleFileName.jpg", file.FileName)
|
||||
})
|
||||
|
||||
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")
|
||||
t.Log(file)
|
||||
|
|
|
@ -30,7 +30,7 @@ type LabelResult struct {
|
|||
|
||||
// LabelBySlug returns a Label based on the slug name.
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ func (q *Query) LabelBySlug(labelSlug string) (label entity.Label, err error) {
|
|||
|
||||
// LabelByUUID returns a Label based on the label UUID.
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -337,7 +337,7 @@ func (q *Query) Photos(f form.PhotoSearch) (results []PhotoResult, err error) {
|
|||
|
||||
// PhotoByID returns a Photo based on the ID.
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -346,7 +346,7 @@ func (q *Query) PhotoByID(photoID uint64) (photo entity.Photo, err error) {
|
|||
|
||||
// PhotoByUUID returns a Photo based on the UUID.
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -362,6 +362,7 @@ func (q *Query) PreloadPhotoByUUID(photoUUID string) (photo entity.Photo, err er
|
|||
Preload("Labels.Label").
|
||||
Preload("Camera").
|
||||
Preload("Lens").
|
||||
Preload("Links").
|
||||
Preload("Description").
|
||||
First(&photo).Error; err != nil {
|
||||
return photo, err
|
||||
|
|
|
@ -32,15 +32,18 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.UpdatePhoto(v1, conf)
|
||||
api.GetPhotos(v1, conf)
|
||||
api.GetPhotoDownload(v1, conf)
|
||||
api.LinkPhoto(v1, conf)
|
||||
api.LikePhoto(v1, conf)
|
||||
api.DislikePhoto(v1, conf)
|
||||
api.AddPhotoLabel(v1, conf)
|
||||
api.RemovePhotoLabel(v1, conf)
|
||||
api.GetMomentsTime(v1, conf)
|
||||
api.GetFile(v1, conf)
|
||||
api.LinkFile(v1, conf)
|
||||
|
||||
api.GetLabels(v1, conf)
|
||||
api.UpdateLabel(v1, conf)
|
||||
api.LinkLabel(v1, conf)
|
||||
api.LikeLabel(v1, conf)
|
||||
api.DislikeLabel(v1, conf)
|
||||
api.LabelThumbnail(v1, conf)
|
||||
|
@ -64,6 +67,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.DeleteAlbum(v1, conf)
|
||||
api.DownloadAlbum(v1, conf)
|
||||
api.GetAlbums(v1, conf)
|
||||
api.LinkAlbum(v1, conf)
|
||||
api.LikeAlbum(v1, conf)
|
||||
api.DislikeAlbum(v1, conf)
|
||||
api.AlbumThumbnail(v1, conf)
|
||||
|
|
Loading…
Reference in a new issue