328 lines
9.8 KiB
Go
328 lines
9.8 KiB
Go
package api
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"fmt"
|
|
"path"
|
|
"time"
|
|
"regexp"
|
|
"strings"
|
|
"errors"
|
|
"net/url"
|
|
"net/http"
|
|
"encoding/json"
|
|
|
|
"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
|
|
api.mediaItemGETHandler(w, r)
|
|
} else {
|
|
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Paging:
|
|
// - Regular Pagination:
|
|
// - /api/v1/MediaItems?page[limit]=50&page=2
|
|
// - Meta Count Only
|
|
// - /api/v1/MediaItems?page[limit]=0
|
|
|
|
// Sorting:
|
|
// - Ascending Sort:
|
|
// - /api/v1/MediaItems?sort=created_at
|
|
// - Descending Sort:
|
|
// - /api/v1/MediaItems?sort=-created_at
|
|
|
|
// Filters:
|
|
// - Greater Than / Less Than (created_at, updated_at, exif_date)
|
|
// - /api/v1/MediaItems?filter[created_at]>=2020-01-01&filter[created_at]<=2021-01-01
|
|
// - Long / Lat Range (latitude, longitude)
|
|
// - /api/v1/MediaItems?filter[latitude]>=71.1827&filter[latitude]<=72.0000&filter[longitude]>=100.000&filter[longitude]<=101.0000
|
|
// - Image / Video (media_type)
|
|
// - /api/v1/MediaItems?filter[media_type]=Image
|
|
// - Tags (tags)
|
|
// - /api/v1/MediaItems?filter[tags]=id1,id2,id3
|
|
// - Albums (albums)
|
|
// - /api/v1/MediaItems?filter[albums]=id1
|
|
func (api *API) mediaItemGETHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
// Handle error
|
|
}
|
|
|
|
testObj := models.MediaItem{}
|
|
json.NewDecoder().Decode(&testObj)
|
|
fmt.Printf("Result: %+v\n", testObj)
|
|
|
|
// allParams, err := json.Marshal(r.Form)
|
|
// if err != nil {
|
|
// // Handle error
|
|
// }
|
|
|
|
// filter := &models.MediaItem{}
|
|
// if err = json.Unmarshal(allParams, filter); err != nil {
|
|
// // Handle error
|
|
// fmt.Printf("Fuck: %s\n", err)
|
|
// }
|
|
|
|
// fmt.Printf("Result: %+v\n", filter)
|
|
|
|
// err = normalizeForm(r.Form, models.MediaItem{})
|
|
// if err != nil {
|
|
// fmt.Printf("Error: %s\n", err)
|
|
// }
|
|
|
|
// var testItems []models.MediaItem
|
|
// api.DB.QueryBuilder(&testItems, allParams)
|
|
|
|
// fmt.Printf("\n\nItems: %+v", testItems)
|
|
|
|
|
|
// Pull out UUIDs
|
|
reqInfo := r.Context().Value("uuids").(map[string]string)
|
|
uid := reqInfo["uid"]
|
|
userUUID, _ := uuid.Parse(uid)
|
|
|
|
// TODO: Can apply multiple filters based on query parameters
|
|
mediaItemFilter := &models.MediaItem{UserUUID: userUUID}
|
|
mediaItemFilter.UserUUID = userUUID
|
|
|
|
mediaItems, count, _ := api.DB.MediaItems(mediaItemFilter)
|
|
response := &models.APIResponse{
|
|
Data: &mediaItems,
|
|
Meta: &models.APIMeta{Count: count},
|
|
}
|
|
responseJSON(w, &response, http.StatusOK)
|
|
}
|
|
|
|
func (api *API) mediaItemPOSTHandler(w http.ResponseWriter, r *http.Request) {
|
|
// 64MB limit (TODO: Change this - video)
|
|
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.UserUUID, 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)
|
|
|
|
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)
|
|
}
|
|
|
|
// {
|
|
// filters: [
|
|
// { field: "", operator: ""},
|
|
// { field: "", operator: ""},
|
|
// { field: "", operator: ""},
|
|
// ],
|
|
// sort: ""
|
|
// page: {}
|
|
// }
|
|
func normalizeForm(form url.Values, typeObj interface{}) error {
|
|
allowedFields := models.JSONFields(typeObj)
|
|
|
|
for key, val := range form {
|
|
key = strings.ToLower(key)
|
|
|
|
re := regexp.MustCompile(`^(filter|page)\[(\w*)]($|>|<)$`)
|
|
matches := re.FindStringSubmatch(key)
|
|
|
|
if len(matches) == 4 {
|
|
cmd := strings.ToLower(matches[1])
|
|
field := strings.ToLower(matches[2])
|
|
operator := strings.ToLower(matches[3])
|
|
|
|
if cmd == "page" && field == "limit" {
|
|
fmt.Printf("cmd: %s field: %s op: %s\n", cmd, field, operator)
|
|
continue
|
|
}
|
|
|
|
// Validate field
|
|
_, ok := allowedFields[field]
|
|
if !ok {
|
|
return errors.New("Invalid field.")
|
|
}
|
|
|
|
// Val assertions
|
|
tempObj := make(map[string]string)
|
|
tempObj[field] = val[0]
|
|
|
|
mi, err := json.Marshal(tempObj)
|
|
if err != nil {
|
|
// Handle error
|
|
fmt.Printf("1 Type Assertion Failed For Field: [%s] with value: [%s]\n", field, val)
|
|
}
|
|
fmt.Printf("String JSON: %s", string(mi))
|
|
refObj := &models.MediaItem{}
|
|
if err = json.Unmarshal(mi, refObj); err != nil {
|
|
// Handle error
|
|
fmt.Printf("2 Type Assertion Failed For Field: [%s] with value: [%s]\n", field, val[0])
|
|
fmt.Println(err)
|
|
}
|
|
|
|
fmt.Printf("Result: %+v\n", refObj)
|
|
|
|
fmt.Printf("cmd: %s field: %s op: %s\n", cmd, field, operator)
|
|
} else if key == "sort" {
|
|
field := strings.ToLower(val[0])
|
|
|
|
// Validate field
|
|
_, ok := allowedFields[field]
|
|
if !ok {
|
|
return errors.New("Invalid field.")
|
|
}
|
|
|
|
// TODO: Validate val
|
|
|
|
fmt.Printf("cmd: %s\n", key)
|
|
} else {
|
|
return errors.New("Invalid parameter(s)")
|
|
}
|
|
}
|
|
return nil
|
|
}
|