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/ // - JSON Struct // - /api/v1/MediaItems//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) }