This repository has been archived on 2023-11-13. You can view files and clone it, but cannot push or open issues or pull requests.
imagini/internal/api/media_items.go

176 lines
5.4 KiB
Go

package api
import (
"io"
"os"
"time"
"path"
"strings"
"net/http"
"github.com/google/uuid"
"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"
)
// 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) {
if r.Method == http.MethodPost {
// CREATE
api.mediaItemPOSTHandler(w, r)
} 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
}
}
func (api *API) mediaItemPOSTHandler(w http.ResponseWriter, r *http.Request) {
// 64MB limit (TODO: Change this)
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
fileHeader := make([]byte, 64)
// 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
fileMime := mimetype.Detect(fileHeader)
contentType := fileMime.String()
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
}
// Pull out UUIDs
reqInfo := r.Context().Value("uuids").(map[string]string)
uid := reqInfo["uid"]
// 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)
// Create File
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
}
// 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
}
successJSON(w, "Upload succeeded.", http.StatusCreated)
}
func mediaItemFromEXIFData(filePath string) (models.MediaItem, error) {
rawExif, err := exif.SearchFileAndExtractExif(filePath)
entries, _, err := exif.GetFlatExifData(rawExif, nil)
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
}
}
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)
}