2021-01-16 22:00:17 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
2021-01-20 02:18:08 +00:00
|
|
|
"io"
|
|
|
|
"os"
|
2021-01-22 05:00:55 +00:00
|
|
|
"time"
|
|
|
|
"path"
|
2021-01-20 02:18:08 +00:00
|
|
|
"strings"
|
|
|
|
"net/http"
|
2021-01-22 05:00:55 +00:00
|
|
|
"github.com/google/uuid"
|
2021-01-20 02:18:08 +00:00
|
|
|
"github.com/dsoprea/go-exif/v3"
|
2021-01-22 05:00:55 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/gabriel-vasile/mimetype"
|
|
|
|
"github.com/dsoprea/go-exif/v3/common"
|
2021-01-20 02:18:08 +00:00
|
|
|
|
|
|
|
"reichard.io/imagini/internal/models"
|
2021-01-16 22:00:17 +00:00
|
|
|
)
|
|
|
|
|
2021-01-20 02:18:08 +00:00
|
|
|
|
2021-01-22 05:00:55 +00:00
|
|
|
// GET
|
|
|
|
// - /api/v1/MediaItems/<GUID>
|
|
|
|
// - JSON Struct
|
|
|
|
// - /api/v1/MediaItems/<GUID>/content
|
|
|
|
// - The raw file
|
2021-01-16 22:00:17 +00:00
|
|
|
func (api *API) mediaItemsHandler(w http.ResponseWriter, r *http.Request) {
|
2021-01-19 21:26:10 +00:00
|
|
|
if r.Method == http.MethodPost {
|
|
|
|
// CREATE
|
2021-01-22 05:00:55 +00:00
|
|
|
api.mediaItemPOSTHandler(w, r)
|
2021-01-19 21:26:10 +00:00
|
|
|
} else if r.Method == http.MethodPut {
|
|
|
|
// UPDATE / REPLACE
|
|
|
|
} else if r.Method == http.MethodPatch {
|
|
|
|
// UPDATE / MODIFY
|
|
|
|
} else if r.Method == http.MethodDelete {
|
|
|
|
// DELETE
|
|
|
|
} else if r.Method == http.MethodGet {
|
|
|
|
// GET
|
|
|
|
} else {
|
|
|
|
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-22 05:00:55 +00:00
|
|
|
func (api *API) mediaItemPOSTHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// 64MB limit (TODO: Change this)
|
2021-01-20 02:18:08 +00:00
|
|
|
r.ParseMultipartForm(64 << 20)
|
|
|
|
|
|
|
|
// Open form file
|
|
|
|
formFile, multipartFileHeader, err := r.FormFile("file")
|
|
|
|
if err != nil {
|
|
|
|
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer formFile.Close()
|
|
|
|
|
|
|
|
// File header placeholder
|
2021-01-22 05:00:55 +00:00
|
|
|
fileHeader := make([]byte, 64)
|
2021-01-20 02:18:08 +00:00
|
|
|
|
|
|
|
// Copy headers into the buffer
|
|
|
|
if _, err := formFile.Read(fileHeader); err != nil {
|
|
|
|
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset position
|
|
|
|
if _, err := formFile.Seek(0, 0); err != nil {
|
|
|
|
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine media type
|
2021-01-22 05:00:55 +00:00
|
|
|
fileMime := mimetype.Detect(fileHeader)
|
|
|
|
contentType := fileMime.String()
|
2021-01-20 02:18:08 +00:00
|
|
|
var mediaType string
|
|
|
|
if strings.HasPrefix(contentType, "image/") {
|
|
|
|
mediaType = "Image"
|
|
|
|
} else if strings.HasPrefix(contentType, "video/") {
|
|
|
|
mediaType = "Video"
|
|
|
|
} else {
|
|
|
|
errorJSON(w, "Invalid filetype.", http.StatusUnsupportedMediaType)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-01-22 05:00:55 +00:00
|
|
|
// Pull out UUIDs
|
|
|
|
reqInfo := r.Context().Value("uuids").(map[string]string)
|
|
|
|
uid := reqInfo["uid"]
|
2021-01-20 02:18:08 +00:00
|
|
|
|
2021-01-22 05:00:55 +00:00
|
|
|
// Derive Folder & File Path
|
|
|
|
mediaItemUUID := uuid.New()
|
|
|
|
fileName := mediaItemUUID.String() + fileMime.Extension()
|
|
|
|
folderPath := path.Join("/" + api.Config.DataPath + "/media/" + uid)
|
|
|
|
os.MkdirAll(folderPath, 0700)
|
|
|
|
filePath := path.Join(folderPath + "/" + fileName)
|
2021-01-20 02:18:08 +00:00
|
|
|
|
2021-01-22 05:00:55 +00:00
|
|
|
// Create File
|
2021-01-20 02:18:08 +00:00
|
|
|
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0600)
|
|
|
|
if err != nil {
|
|
|
|
log.Warn("[api] createMediaItem - Unable to open file: ", filePath)
|
|
|
|
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
// Copy data to file
|
|
|
|
_, err = io.Copy(f, formFile)
|
|
|
|
if err != nil {
|
|
|
|
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-01-22 05:00:55 +00:00
|
|
|
// Create MediaItem From EXIF Data
|
|
|
|
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
|
|
|
|
}
|
2021-01-20 02:18:08 +00:00
|
|
|
|
|
|
|
successJSON(w, "Upload succeeded.", http.StatusCreated)
|
|
|
|
}
|
|
|
|
|
2021-01-22 05:00:55 +00:00
|
|
|
func mediaItemFromEXIFData(filePath string) (models.MediaItem, error) {
|
2021-01-20 02:18:08 +00:00
|
|
|
rawExif, err := exif.SearchFileAndExtractExif(filePath)
|
|
|
|
entries, _, err := exif.GetFlatExifData(rawExif, nil)
|
|
|
|
|
2021-01-22 05:00:55 +00:00
|
|
|
decLong := float32(1)
|
|
|
|
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
|
|
|
|
}
|
2021-01-20 02:18:08 +00:00
|
|
|
}
|
2021-01-16 22:00:17 +00:00
|
|
|
|
2021-01-22 05:00:55 +00:00
|
|
|
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)
|
2021-01-16 22:00:17 +00:00
|
|
|
}
|