Image Saving & Serving
This commit is contained in:
parent
547bb4a8b9
commit
997806b7f0
@ -24,7 +24,7 @@ func NewServer() *Server {
|
|||||||
c := config.Load()
|
c := config.Load()
|
||||||
db := db.NewMgr(c)
|
db := db.NewMgr(c)
|
||||||
auth := auth.NewMgr(db, c)
|
auth := auth.NewMgr(db, c)
|
||||||
api := api.NewApi(db, auth)
|
api := api.NewApi(db, c, auth)
|
||||||
|
|
||||||
return &Server{
|
return &Server{
|
||||||
API: api,
|
API: api,
|
||||||
|
1
go.mod
1
go.mod
@ -6,6 +6,7 @@ require (
|
|||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
github.com/disintegration/imaging v1.6.2 // indirect
|
github.com/disintegration/imaging v1.6.2 // indirect
|
||||||
github.com/dsoprea/go-exif/v3 v3.0.0-20201216222538-db167117f483
|
github.com/dsoprea/go-exif/v3 v3.0.0-20201216222538-db167117f483
|
||||||
|
github.com/gabriel-vasile/mimetype v1.1.2
|
||||||
github.com/google/uuid v1.1.5
|
github.com/google/uuid v1.1.5
|
||||||
github.com/lestrrat-go/jwx v1.0.8
|
github.com/lestrrat-go/jwx v1.0.8
|
||||||
github.com/mattn/go-sqlite3 v1.14.6
|
github.com/mattn/go-sqlite3 v1.14.6
|
||||||
|
2
go.sum
2
go.sum
@ -37,6 +37,8 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uA
|
|||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.1.2 h1:gaPnPcNor5aZSVCJVSGipcpbgMWiAAj9z182ocSGbHU=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.1.2/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
|
||||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||||
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
|
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
|
||||||
|
@ -5,17 +5,20 @@ import (
|
|||||||
|
|
||||||
"reichard.io/imagini/internal/db"
|
"reichard.io/imagini/internal/db"
|
||||||
"reichard.io/imagini/internal/auth"
|
"reichard.io/imagini/internal/auth"
|
||||||
|
"reichard.io/imagini/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
Router *http.ServeMux
|
Router *http.ServeMux
|
||||||
|
Config *config.Config
|
||||||
Auth *auth.AuthManager
|
Auth *auth.AuthManager
|
||||||
DB *db.DBManager
|
DB *db.DBManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApi(db *db.DBManager, auth *auth.AuthManager) *API {
|
func NewApi(db *db.DBManager, c *config.Config, auth *auth.AuthManager) *API {
|
||||||
api := &API{
|
api := &API{
|
||||||
Router: http.NewServeMux(),
|
Router: http.NewServeMux(),
|
||||||
|
Config: c,
|
||||||
Auth: auth,
|
Auth: auth,
|
||||||
DB: db,
|
DB: db,
|
||||||
}
|
}
|
||||||
|
@ -53,8 +53,8 @@ func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
refreshToken, err := api.Auth.CreateJWTRefreshToken(user, device)
|
refreshToken, err := api.Auth.CreateJWTRefreshToken(user, device)
|
||||||
|
|
||||||
// Set appropriate cookies
|
// Set appropriate cookies
|
||||||
accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken, HttpOnly: true}
|
accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken, Path: "/", HttpOnly: true}
|
||||||
refreshCookie := http.Cookie{Name: "RefreshToken", Value: refreshToken, HttpOnly: true}
|
refreshCookie := http.Cookie{Name: "RefreshToken", Value: refreshToken, Path: "/", HttpOnly: true}
|
||||||
http.SetCookie(w, &accessCookie)
|
http.SetCookie(w, &accessCookie)
|
||||||
http.SetCookie(w, &refreshCookie)
|
http.SetCookie(w, &refreshCookie)
|
||||||
|
|
||||||
|
30
internal/api/media.go
Normal file
30
internal/api/media.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Responsible for serving up static images / videos
|
||||||
|
func (api *API) mediaHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.Dir(r.URL.Path) != "/media" {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull out UUIDs
|
||||||
|
reqInfo := r.Context().Value("uuids").(map[string]string)
|
||||||
|
uid := reqInfo["uid"]
|
||||||
|
|
||||||
|
// Derive Path
|
||||||
|
fileName := path.Base(r.URL.Path)
|
||||||
|
folderPath := path.Join("/" + api.Config.DataPath + "/media/" + uid)
|
||||||
|
mediaPath := path.Join(folderPath + "/" + fileName)
|
||||||
|
|
||||||
|
http.ServeFile(w, r, mediaPath)
|
||||||
|
}
|
@ -3,21 +3,29 @@ package api
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"fmt"
|
"time"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"net/http"
|
"net/http"
|
||||||
"encoding/json"
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/dsoprea/go-exif/v3"
|
"github.com/dsoprea/go-exif/v3"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
|
"github.com/dsoprea/go-exif/v3/common"
|
||||||
|
|
||||||
"reichard.io/imagini/internal/models"
|
"reichard.io/imagini/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// GET
|
||||||
|
// - /api/v1/MediaItems/<GUID>
|
||||||
|
// - JSON Struct
|
||||||
|
// - /api/v1/MediaItems/<GUID>/content
|
||||||
|
// - The raw file
|
||||||
func (api *API) mediaItemsHandler(w http.ResponseWriter, r *http.Request) {
|
func (api *API) mediaItemsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
// CREATE
|
// CREATE
|
||||||
api.createMediaItem(w, r)
|
api.mediaItemPOSTHandler(w, r)
|
||||||
} else if r.Method == http.MethodPut {
|
} else if r.Method == http.MethodPut {
|
||||||
// UPDATE / REPLACE
|
// UPDATE / REPLACE
|
||||||
} else if r.Method == http.MethodPatch {
|
} else if r.Method == http.MethodPatch {
|
||||||
@ -32,24 +40,10 @@ func (api *API) mediaItemsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) createMediaItem(w http.ResponseWriter, r *http.Request) {
|
func (api *API) mediaItemPOSTHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 64MB limit (TODO: Change this)
|
||||||
r.ParseMultipartForm(64 << 20)
|
r.ParseMultipartForm(64 << 20)
|
||||||
|
|
||||||
// Parse metadata
|
|
||||||
metadata := r.FormValue("metadata")
|
|
||||||
var mediaItem models.MediaItem
|
|
||||||
err := json.Unmarshal([]byte(metadata), &mediaItem)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("[api] createMediaItem - Invalid metadata: ", err)
|
|
||||||
errorJSON(w, "Metadata invalid.", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Verify mediaItem contains appropriate values
|
|
||||||
|
|
||||||
|
|
||||||
// fmt.Printf("Media Item: %+v\n", mediaItem)
|
|
||||||
|
|
||||||
// Open form file
|
// Open form file
|
||||||
formFile, multipartFileHeader, err := r.FormFile("file")
|
formFile, multipartFileHeader, err := r.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -59,7 +53,7 @@ func (api *API) createMediaItem(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer formFile.Close()
|
defer formFile.Close()
|
||||||
|
|
||||||
// File header placeholder
|
// File header placeholder
|
||||||
fileHeader := make([]byte, 512)
|
fileHeader := make([]byte, 64)
|
||||||
|
|
||||||
// Copy headers into the buffer
|
// Copy headers into the buffer
|
||||||
if _, err := formFile.Read(fileHeader); err != nil {
|
if _, err := formFile.Read(fileHeader); err != nil {
|
||||||
@ -74,8 +68,9 @@ func (api *API) createMediaItem(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine media type
|
// Determine media type
|
||||||
|
fileMime := mimetype.Detect(fileHeader)
|
||||||
|
contentType := fileMime.String()
|
||||||
var mediaType string
|
var mediaType string
|
||||||
contentType := http.DetectContentType(fileHeader)
|
|
||||||
if strings.HasPrefix(contentType, "image/") {
|
if strings.HasPrefix(contentType, "image/") {
|
||||||
mediaType = "Image"
|
mediaType = "Image"
|
||||||
} else if strings.HasPrefix(contentType, "video/") {
|
} else if strings.HasPrefix(contentType, "video/") {
|
||||||
@ -85,16 +80,18 @@ func (api *API) createMediaItem(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = mediaType
|
// Pull out UUIDs
|
||||||
_ = multipartFileHeader
|
reqInfo := r.Context().Value("uuids").(map[string]string)
|
||||||
|
uid := reqInfo["uid"]
|
||||||
|
|
||||||
// Print details
|
// Derive Folder & File Path
|
||||||
// Filename: multipartFileHeader.Filename
|
mediaItemUUID := uuid.New()
|
||||||
// Size: multipartFileHeader.Size
|
fileName := mediaItemUUID.String() + fileMime.Extension()
|
||||||
// ContentType: http.DetectContentType(fileHeader)
|
folderPath := path.Join("/" + api.Config.DataPath + "/media/" + uid)
|
||||||
|
os.MkdirAll(folderPath, 0700)
|
||||||
|
filePath := path.Join(folderPath + "/" + fileName)
|
||||||
|
|
||||||
// Open file to store
|
// Create File
|
||||||
filePath := "./test.png" // TODO: Change dynamic
|
|
||||||
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0600)
|
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("[api] createMediaItem - Unable to open file: ", filePath)
|
log.Warn("[api] createMediaItem - Unable to open file: ", filePath)
|
||||||
@ -110,23 +107,69 @@ func (api *API) createMediaItem(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather EXIF Data
|
// Create MediaItem From EXIF Data
|
||||||
mediaItem = deriveEXIFData(filePath, mediaItem)
|
mediaItem, err := mediaItemFromEXIFData(filePath)
|
||||||
|
if err != nil {
|
||||||
|
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Additional MediaItem Fields
|
||||||
|
mediaItem.Base.UUID = mediaItemUUID
|
||||||
|
mediaItem.User.UUID, err = uuid.Parse(uid)
|
||||||
|
mediaItem.MediaType = mediaType
|
||||||
|
mediaItem.FileName = fileName
|
||||||
|
mediaItem.OrigName = multipartFileHeader.Filename
|
||||||
|
|
||||||
|
// Create MediaItem in DB
|
||||||
|
_, err = api.DB.CreateMediaItem(mediaItem)
|
||||||
|
if err != nil {
|
||||||
|
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Add resource location in response
|
|
||||||
successJSON(w, "Upload succeeded.", http.StatusCreated)
|
successJSON(w, "Upload succeeded.", http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deriveEXIFData(filePath string, mediaItem models.MediaItem) models.MediaItem {
|
func mediaItemFromEXIFData(filePath string) (models.MediaItem, error) {
|
||||||
rawExif, err := exif.SearchFileAndExtractExif(filePath)
|
rawExif, err := exif.SearchFileAndExtractExif(filePath)
|
||||||
entries, _, err := exif.GetFlatExifData(rawExif, nil)
|
entries, _, err := exif.GetFlatExifData(rawExif, nil)
|
||||||
_ = err
|
|
||||||
// parsedJSON, err := json.MarshalIndent(entries, "", " ")
|
|
||||||
// fmt.Printf("EXIF String: %+v\n", string(parsedJSON))
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
decLong := float32(1)
|
||||||
fmt.Printf("IFD-PATH=[%s] ID=(0x%04x) NAME=[%s] COUNT=(%d) TYPE=[%s] VALUE=[%s]\n", entry.IfdPath, entry.TagId, entry.TagName, entry.UnitCount, entry.TagTypeName, entry.Formatted)
|
decLat := float32(1)
|
||||||
|
|
||||||
|
var mediaItem models.MediaItem
|
||||||
|
for _, v := range entries {
|
||||||
|
if v.TagName == "DateTimeOriginal" {
|
||||||
|
formattedTime, _ := time.Parse("2006:01:02 15:04:05", v.Formatted)
|
||||||
|
mediaItem.EXIFDate = formattedTime
|
||||||
|
} else if v.TagName == "GPSLatitude" {
|
||||||
|
latStruct := v.Value.([]exifcommon.Rational)
|
||||||
|
decLat *= deriveDecimalCoordinate(
|
||||||
|
latStruct[0].Numerator / latStruct[0].Denominator,
|
||||||
|
latStruct[1].Numerator / latStruct[1].Denominator,
|
||||||
|
float32(latStruct[2].Numerator) / float32(latStruct[2].Denominator),
|
||||||
|
)
|
||||||
|
} else if v.TagName == "GPSLongitude" {
|
||||||
|
longStruct := v.Value.([]exifcommon.Rational)
|
||||||
|
decLong *= deriveDecimalCoordinate(
|
||||||
|
longStruct[0].Numerator / longStruct[0].Denominator,
|
||||||
|
longStruct[1].Numerator / longStruct[1].Denominator,
|
||||||
|
float32(longStruct[2].Numerator) / float32(longStruct[2].Denominator),
|
||||||
|
)
|
||||||
|
} else if v.TagName == "GPSLatitudeRef" && v.Formatted == "S" {
|
||||||
|
decLat *= -1
|
||||||
|
} else if v.TagName == "GPSLongitudeRef" && v.Formatted == "W" {
|
||||||
|
decLong *= -1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return mediaItem
|
mediaItem.Latitude = decLat
|
||||||
|
mediaItem.Longitude = decLong
|
||||||
|
|
||||||
|
return mediaItem, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveDecimalCoordinate(degrees, minutes uint32, seconds float32) float32 {
|
||||||
|
return float32(degrees) + (float32(minutes) / 60) + (seconds / 3600)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -30,17 +31,21 @@ func (api *API) authMiddleware(next http.Handler) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate JWT Tokens
|
// Validate JWT Tokens
|
||||||
// accessToken, accessOK := api.Auth.ValidateJWTAccessToken(accessCookie.Value)
|
accessToken, accessOK := api.Auth.ValidateJWTAccessToken(accessCookie.Value)
|
||||||
_, accessOK := api.Auth.ValidateJWTAccessToken(accessCookie.Value)
|
|
||||||
|
|
||||||
if accessOK {
|
if accessOK {
|
||||||
// Acquire UserID and DeviceID
|
// Acquire UserID and DeviceID
|
||||||
// uid, _ := accessToken.Get("sub")
|
reqInfo := make(map[string]string)
|
||||||
// did, _ := accessToken.Get("did")
|
uid, _ := accessToken.Get("sub")
|
||||||
|
did, _ := accessToken.Get("did")
|
||||||
|
reqInfo["uid"] = uid.(string)
|
||||||
|
reqInfo["did"] = did.(string)
|
||||||
|
|
||||||
// Set context uid & did
|
// Add context
|
||||||
|
ctx := context.WithValue(r.Context(), "uuids", reqInfo)
|
||||||
|
sr := r.WithContext(ctx)
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, sr)
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (api *API) registerRoutes() {
|
func (api *API) registerRoutes() {
|
||||||
|
api.Router.HandleFunc("/media/", multipleMiddleware(
|
||||||
|
api.mediaHandler,
|
||||||
|
api.authMiddleware,
|
||||||
|
))
|
||||||
api.Router.HandleFunc("/api/v1/MediaItems", multipleMiddleware(
|
api.Router.HandleFunc("/api/v1/MediaItems", multipleMiddleware(
|
||||||
api.mediaItemsHandler,
|
api.mediaItemsHandler,
|
||||||
api.authMiddleware,
|
api.authMiddleware,
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (dbm *DBManager) CreateMediaItem (mediaItem models.MediaItem) (models.MediaItem, error) {
|
func (dbm *DBManager) CreateMediaItem (mediaItem models.MediaItem) (models.MediaItem, error) {
|
||||||
log.Info("[db] Creating media item: ", mediaItem.RelPath)
|
log.Info("[db] Creating media item: ", mediaItem.FileName)
|
||||||
err := dbm.db.Create(&mediaItem).Error
|
err := dbm.db.Create(&mediaItem).Error
|
||||||
return mediaItem, err
|
return mediaItem, err
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,10 @@ import (
|
|||||||
|
|
||||||
// Base contains common columns for all tables.
|
// Base contains common columns for all tables.
|
||||||
type Base struct {
|
type Base struct {
|
||||||
UUID uuid.UUID `gorm:"type:uuid;primarykey"`
|
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;primarykey"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (base *Base) BeforeCreate(tx *gorm.DB) (err error) {
|
func (base *Base) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
@ -28,41 +28,42 @@ type ServerSetting struct {
|
|||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
Base
|
Base
|
||||||
User User `json:"user" gorm:"ForeignKey:UUID;not null"`
|
User User `json:"user" gorm:"ForeignKey:UUID;not null"` // User UUID
|
||||||
Name string `json:"name" gorm:"not null"`
|
Name string `json:"name" gorm:"not null"` // Name of Device
|
||||||
Type string `json:"type" gorm:"not null"` // Android, iOS, Chrome, FireFox, Edge, etc
|
Type string `json:"type" gorm:"not null"` // Android, iOS, Chrome, FireFox, Edge
|
||||||
RefreshKey string `json:"-"`
|
RefreshKey string `json:"-"` // Device Specific Refresh Key
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Base
|
Base
|
||||||
Email string `json:"email" gorm:"unique"`
|
Email string `json:"email" gorm:"unique"` // Email
|
||||||
Username string `json:"username" gorm:"unique"`
|
Username string `json:"username" gorm:"unique"` // Username
|
||||||
FirstName string `json:"first_name"`
|
FirstName string `json:"first_name"` // First Name
|
||||||
LastName string `json:"last_name"`
|
LastName string `json:"last_name"` // Last Name
|
||||||
Role string `json:"role"`
|
Role string `json:"role"` // Role
|
||||||
AuthType string `json:"auth_type" gorm:"default:Local;not null"`
|
AuthType string `json:"auth_type" gorm:"default:Local;not null"` // Auth Type (E.g. Local, LDAP)
|
||||||
Password string `json:"-"`
|
Password string `json:"-"` // Hased & Salted Password
|
||||||
}
|
}
|
||||||
|
|
||||||
type MediaItem struct {
|
type MediaItem struct {
|
||||||
Base
|
Base
|
||||||
User User `json:"user" gorm:"ForeignKey:UUID;not null"`
|
User User `json:"user" gorm:"ForeignKey:UUID;not null"` // User UUID
|
||||||
EXIFDate time.Time `json:"exif_date"`
|
EXIFDate time.Time `json:"exif_date"` // EXIF Date
|
||||||
Latitude string `json:"latitude"`
|
Latitude float32 `json:"latitude" gorm:"type:decimal(10,2)"` // Decimal Latitude
|
||||||
Longitude string `json:"longitude"`
|
Longitude float32 `json:"longitude" gorm:"type:decimal(10,2)"` // Decimal Longitude
|
||||||
MediaType string `json:"media_type" gorm:"default:Image;not null"` // Image, Video
|
MediaType string `json:"media_type" gorm:"default:Image;not null"` // Image, Video
|
||||||
RelPath string `json:"rel_path" gorm:"not null"`
|
OrigName string `json:"orig_name" gorm:"not null"` // Original Name
|
||||||
Tags []Tag `json:"tags" gorm:"many2many:media_tags;"`
|
FileName string `json:"file_name" gorm:"not null"` // File Name
|
||||||
Albums []Album `json:"albums" gorm:"many2many:media_albums;"`
|
Tags []Tag `json:"tags" gorm:"many2many:media_tags;"` // Associated Tag UUIDs
|
||||||
|
Albums []Album `json:"albums" gorm:"many2many:media_albums;"` // Associated Album UUIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
Base
|
Base
|
||||||
Name string `json:"name" gorm:"not null"`
|
Name string `json:"name" gorm:"not null"` // Tag Name
|
||||||
}
|
}
|
||||||
|
|
||||||
type Album struct {
|
type Album struct {
|
||||||
Base
|
Base
|
||||||
Name string `json:"name" gorm:"not null"`
|
Name string `json:"name" gorm:"not null"` // Album Name
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user