refactor(driver,dbfs): share GPS parsing and tx-following, name DBFS deps

- Five storage drivers copy-pasted the same parseGpsInfo skeleton, DMS
  arithmetic, and rational-element parser with only format deltas.
  Shared helpers now live in pkg/mediameta (GpsMeta, DMSDecimal,
  GpsRationalElem); each driver keeps only its vendor-specific element
  parser.
- Four dbfs navigators repeated the same FollowTx boilerplate (tx check,
  WithTx swap, revert closure). A generic followTxClients helper now
  owns the mechanics; navigators declare which clients to swap.
- NewDatabaseFS took 14 positional parameters of overlapping types; the
  single call site now populates a named DBFSDependencies struct.
- SettingClient.Gets doc corrected: missing names are omitted, not an
  error.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3582/head
Tomas Dvorak 2 weeks ago
parent ded86c1617
commit 79d2ece4e0

@ -26,7 +26,7 @@ type (
Get(ctx context.Context, name string) (string, error)
// Set sets a setting value to DB.
Set(ctx context.Context, settings map[string]string) error
// Gets gets multiple setting values from DB, returns error if any setting cannot be found.
// Gets gets multiple setting values from DB, returns a map of found settings; names with no matching setting are omitted.
Gets(ctx context.Context, names []string) (map[string]string, error)
}
)

@ -9,9 +9,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/pkg/mediameta"
"github.com/cloudreve/Cloudreve/v4/pkg/request"
"github.com/samber/lo"
"math"
"net/http"
"strconv"
"strings"
"time"
)
@ -206,78 +204,25 @@ func (handler *Driver) extractMediaInfo(ctx context.Context, path string, opt *u
}
func parseGpsInfo(imageInfo ImageInfo) []driver.MediaMeta {
latitude := imageInfo["GPSLatitude"] // 31deg 16.26808'
longitude := imageInfo["GPSLongitude"] // 120deg 42.91039'
latRef := imageInfo["GPSLatitudeRef"] // North
lonRef := imageInfo["GPSLongitudeRef"] // East
// Make sure all value exist in map
if latitude.Value == "" || longitude.Value == "" || latRef.Value == "" || lonRef.Value == "" {
return nil
}
lat := parseRawGPS(latitude.Value, latRef.Value)
lon := parseRawGPS(longitude.Value, lonRef.Value)
if !math.IsNaN(lat) && !math.IsNaN(lon) {
lat, lng := mediameta.NormalizeGPS(lat, lon)
return []driver.MediaMeta{{
Key: mediameta.GpsLat,
Value: fmt.Sprintf("%f", lat),
}, {
Key: mediameta.GpsLng,
Value: fmt.Sprintf("%f", lng),
}}
}
return nil
return mediameta.GpsMeta(imageInfo["GPSLatitude"].Value, imageInfo["GPSLongitude"].Value,
imageInfo["GPSLatitudeRef"].Value, imageInfo["GPSLongitudeRef"].Value, parseRawGPS)
}
func parseRawGPS(gpsStr string, ref string) float64 {
elem := strings.Split(gpsStr, " ")
if len(elem) < 1 {
return 0
}
var (
deg float64
minutes float64
seconds float64
)
deg = getGpsElemValue(elem[0])
var deg, minutes, seconds float64
if len(elem) >= 1 {
deg = mediameta.GpsRationalElem(elem[0])
}
if len(elem) >= 2 {
minutes = getGpsElemValue(elem[1])
minutes = mediameta.GpsRationalElem(elem[1])
}
if len(elem) >= 3 {
seconds = getGpsElemValue(elem[2])
}
decimal := deg + minutes/60.0 + seconds/3600.0
if ref == "S" || ref == "W" {
return -decimal
}
return decimal
}
func getGpsElemValue(elm string) float64 {
elements := strings.Split(elm, "/")
if len(elements) != 2 {
return 0
}
numerator, err := strconv.ParseFloat(elements[0], 64)
if err != nil {
return 0
}
denominator, err := strconv.ParseFloat(elements[1], 64)
if err != nil || denominator == 0 {
return 0
seconds = mediameta.GpsRationalElem(elem[2])
}
return numerator / denominator
return mediameta.DMSDecimal(deg, minutes, seconds, ref == "S" || ref == "W")
}
func handleCosError(resp string, originErr error) error {

@ -4,9 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"net/http"
"strconv"
"strings"
"time"
@ -62,76 +60,23 @@ func (d *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]d
}
func parseGpsInfo(imageInfo map[string]string) []driver.MediaMeta {
latitude := imageInfo["GPSLatitude"] // 31/1, 162680820/10000000, 0/1
longitude := imageInfo["GPSLongitude"] // 120/1, 429103939/10000000, 0/1
latRef := imageInfo["GPSLatitudeRef"] // N
lonRef := imageInfo["GPSLongitudeRef"] // E
// Make sure all value exist in map
if latitude == "" || longitude == "" || latRef == "" || lonRef == "" {
return nil
}
lat := parseRawGPS(latitude, latRef)
lon := parseRawGPS(longitude, lonRef)
if !math.IsNaN(lat) && !math.IsNaN(lon) {
lat, lng := mediameta.NormalizeGPS(lat, lon)
return []driver.MediaMeta{{
Key: mediameta.GpsLat,
Value: fmt.Sprintf("%f", lat),
}, {
Key: mediameta.GpsLng,
Value: fmt.Sprintf("%f", lng),
}}
}
return nil
return mediameta.GpsMeta(imageInfo["GPSLatitude"], imageInfo["GPSLongitude"],
imageInfo["GPSLatitudeRef"], imageInfo["GPSLongitudeRef"], parseRawGPS)
}
func parseRawGPS(gpsStr string, ref string) float64 {
elem := strings.Split(gpsStr, ", ")
if len(elem) < 1 {
return 0
}
var (
deg float64
minutes float64
seconds float64
)
deg = getGpsElemValue(elem[0])
var deg, minutes, seconds float64
if len(elem) >= 1 {
deg = mediameta.GpsRationalElem(elem[0])
}
if len(elem) >= 2 {
minutes = getGpsElemValue(elem[1])
minutes = mediameta.GpsRationalElem(elem[1])
}
if len(elem) >= 3 {
seconds = getGpsElemValue(elem[2])
}
decimal := deg + minutes/60.0 + seconds/3600.0
if ref == "S" || ref == "W" {
return -decimal
}
return decimal
}
func getGpsElemValue(elm string) float64 {
elements := strings.Split(elm, "/")
if len(elements) != 2 {
return 0
}
numerator, err := strconv.ParseFloat(elements[0], 64)
if err != nil {
return 0
}
denominator, err := strconv.ParseFloat(elements[1], 64)
if err != nil || denominator == 0 {
return 0
seconds = mediameta.GpsRationalElem(elem[2])
}
return numerator / denominator
return mediameta.DMSDecimal(deg, minutes, seconds, ref == "S" || ref == "W")
}

@ -5,7 +5,6 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"math"
"net/http"
"strconv"
"strings"
@ -292,45 +291,17 @@ func (handler *Driver) extractMediaInfo(ctx context.Context, path string, catego
}
func parseGpsInfo(imageInfo ImageInfo) []driver.MediaMeta {
latitude := imageInfo["GPSLatitude"] // 31deg 16.26808'
longitude := imageInfo["GPSLongitude"] // 120deg 42.91039'
latRef := imageInfo["GPSLatitudeRef"] // North
lonRef := imageInfo["GPSLongitudeRef"] // East
// Make sure all value exist in map
if latitude.Value == "" || longitude.Value == "" || latRef.Value == "" || lonRef.Value == "" {
return nil
}
lat := parseRawGPS(latitude.Value, latRef.Value)
lon := parseRawGPS(longitude.Value, lonRef.Value)
if !math.IsNaN(lat) && !math.IsNaN(lon) {
lat, lng := mediameta.NormalizeGPS(lat, lon)
return []driver.MediaMeta{{
Key: mediameta.GpsLat,
Value: fmt.Sprintf("%f", lat),
}, {
Key: mediameta.GpsLng,
Value: fmt.Sprintf("%f", lng),
}}
}
return nil
return mediameta.GpsMeta(imageInfo["GPSLatitude"].Value, imageInfo["GPSLongitude"].Value,
imageInfo["GPSLatitudeRef"].Value, imageInfo["GPSLongitudeRef"].Value, parseRawGPS)
}
func parseRawGPS(gpsStr string, ref string) float64 {
elem := strings.Split(gpsStr, " ")
if len(elem) < 1 {
return 0
}
var (
deg float64
minutes float64
seconds float64
)
var deg, minutes, seconds float64
if len(elem) >= 1 {
deg, _ = strconv.ParseFloat(strings.TrimSuffix(elem[0], "deg"), 64)
}
if len(elem) >= 2 {
minutes, _ = strconv.ParseFloat(strings.TrimSuffix(elem[1], "'"), 64)
}
@ -338,13 +309,7 @@ func parseRawGPS(gpsStr string, ref string) float64 {
seconds, _ = strconv.ParseFloat(strings.TrimSuffix(elem[2], "\""), 64)
}
decimal := deg + minutes/60.0 + seconds/3600.0
if ref == "South" || ref == "West" {
return -decimal
}
return decimal
return mediameta.DMSDecimal(deg, minutes, seconds, ref == "South" || ref == "West")
}
func handleOssError(resp string, originErr error) error {

@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"net/http"
"net/url"
"strconv"
@ -131,45 +130,17 @@ func unmarshalError(resp string, originErr error) error {
}
func parseGpsInfo(imageInfo ImageInfo) []driver.MediaMeta {
latitude := imageInfo["GPSLatitude"] // 31, 16.2680820, 0
longitude := imageInfo["GPSLongitude"] // 120, 42.9103939, 0
latRef := imageInfo["GPSLatitudeRef"] // N
lonRef := imageInfo["GPSLongitudeRef"] // E
// Make sure all value exist in map
if latitude.Value == "" || longitude.Value == "" || latRef.Value == "" || lonRef.Value == "" {
return nil
}
lat := parseRawGPS(latitude.Value, latRef.Value)
lon := parseRawGPS(longitude.Value, lonRef.Value)
if !math.IsNaN(lat) && !math.IsNaN(lon) {
lat, lng := mediameta.NormalizeGPS(lat, lon)
return []driver.MediaMeta{{
Key: mediameta.GpsLat,
Value: fmt.Sprintf("%f", lat),
}, {
Key: mediameta.GpsLng,
Value: fmt.Sprintf("%f", lng),
}}
}
return nil
return mediameta.GpsMeta(imageInfo["GPSLatitude"].Value, imageInfo["GPSLongitude"].Value,
imageInfo["GPSLatitudeRef"].Value, imageInfo["GPSLongitudeRef"].Value, parseRawGPS)
}
func parseRawGPS(gpsStr string, ref string) float64 {
elem := strings.Split(gpsStr, ", ")
if len(elem) < 1 {
return 0
}
var (
deg float64
minutes float64
seconds float64
)
var deg, minutes, seconds float64
if len(elem) >= 1 {
deg, _ = strconv.ParseFloat(elem[0], 64)
}
if len(elem) >= 2 {
minutes, _ = strconv.ParseFloat(elem[1], 64)
}
@ -177,11 +148,5 @@ func parseRawGPS(gpsStr string, ref string) float64 {
seconds, _ = strconv.ParseFloat(elem[2], 64)
}
decimal := deg + minutes/60.0 + seconds/3600.0
if ref == "S" || ref == "W" {
return -decimal
}
return decimal
return mediameta.DMSDecimal(deg, minutes, seconds, ref == "S" || ref == "W")
}

@ -8,9 +8,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/pkg/mediameta"
"github.com/cloudreve/Cloudreve/v4/pkg/request"
"github.com/samber/lo"
"math"
"net/http"
"strconv"
"strings"
"time"
)
@ -77,76 +75,23 @@ func unmarshalError(resp string, err error) error {
}
func parseGpsInfo(imageInfo map[string]string) []driver.MediaMeta {
latitude := imageInfo["GPSLatitude"] // 31/1, 162680820/10000000, 0/1
longitude := imageInfo["GPSLongitude"] // 120/1, 429103939/10000000, 0/1
latRef := imageInfo["GPSLatitudeRef"] // N
lonRef := imageInfo["GPSLongitudeRef"] // E
// Make sure all value exist in map
if latitude == "" || longitude == "" || latRef == "" || lonRef == "" {
return nil
}
lat := parseRawGPS(latitude, latRef)
lon := parseRawGPS(longitude, lonRef)
if !math.IsNaN(lat) && !math.IsNaN(lon) {
lat, lng := mediameta.NormalizeGPS(lat, lon)
return []driver.MediaMeta{{
Key: mediameta.GpsLat,
Value: fmt.Sprintf("%f", lat),
}, {
Key: mediameta.GpsLng,
Value: fmt.Sprintf("%f", lng),
}}
}
return nil
return mediameta.GpsMeta(imageInfo["GPSLatitude"], imageInfo["GPSLongitude"],
imageInfo["GPSLatitudeRef"], imageInfo["GPSLongitudeRef"], parseRawGPS)
}
func parseRawGPS(gpsStr string, ref string) float64 {
elem := strings.Split(gpsStr, ",")
if len(elem) < 1 {
return 0
}
var (
deg float64
minutes float64
seconds float64
)
deg = getGpsElemValue(elem[0])
var deg, minutes, seconds float64
if len(elem) >= 1 {
deg = mediameta.GpsRationalElem(elem[0])
}
if len(elem) >= 2 {
minutes = getGpsElemValue(elem[1])
minutes = mediameta.GpsRationalElem(elem[1])
}
if len(elem) >= 3 {
seconds = getGpsElemValue(elem[2])
}
decimal := deg + minutes/60.0 + seconds/3600.0
if ref == "S" || ref == "W" {
return -decimal
}
return decimal
}
func getGpsElemValue(elm string) float64 {
elements := strings.Split(elm, "/")
if len(elements) != 2 {
return 0
}
numerator, err := strconv.ParseFloat(elements[0], 64)
if err != nil {
return 0
}
denominator, err := strconv.ParseFloat(elements[1], 64)
if err != nil || denominator == 0 {
return 0
seconds = mediameta.GpsRationalElem(elem[2])
}
return numerator / denominator
return mediameta.DMSDecimal(deg, minutes, seconds, ref == "S" || ref == "W")
}

@ -93,26 +93,41 @@ func (f *DBFS) writePermitted(file *File, capability NavigatorCapability) bool {
return caps != nil && caps.Enabled(int(capability))
}
func NewDatabaseFS(u *ent.User, fileClient inventory.FileClient, shareClient inventory.ShareClient,
l logging.Logger, ls lock.LockSystem, settingClient setting.Provider,
storagePolicyClient inventory.StoragePolicyClient, hasher hashid.Encoder, userClient inventory.UserClient,
cache, stateKv cache.Driver, directLinkClient inventory.DirectLinkClient, encryptorFactory encrypt.CryptorFactory, eventHub eventhub.EventHub) fs.FileSystem {
// DBFSDependencies groups the collaborators wired into a DBFS instance so the
// constructor call site names each dependency.
type DBFSDependencies struct {
FileClient inventory.FileClient
ShareClient inventory.ShareClient
UserClient inventory.UserClient
StoragePolicyClient inventory.StoragePolicyClient
DirectLinkClient inventory.DirectLinkClient
Logger logging.Logger
LockSystem lock.LockSystem
SettingProvider setting.Provider
Hasher hashid.Encoder
Cache cache.Driver
StateKV cache.Driver
EncryptorFactory encrypt.CryptorFactory
EventHub eventhub.EventHub
}
func NewDatabaseFS(u *ent.User, deps DBFSDependencies) fs.FileSystem {
return &DBFS{
user: u,
navigators: make(map[string]Navigator),
fileClient: fileClient,
shareClient: shareClient,
l: l,
ls: ls,
settingClient: settingClient,
storagePolicyClient: storagePolicyClient,
hasher: hasher,
userClient: userClient,
cache: cache,
stateKv: stateKv,
directLinkClient: directLinkClient,
encryptorFactory: encryptorFactory,
eventHub: eventHub,
fileClient: deps.FileClient,
shareClient: deps.ShareClient,
l: deps.Logger,
ls: deps.LockSystem,
settingClient: deps.SettingProvider,
storagePolicyClient: deps.StoragePolicyClient,
hasher: deps.Hasher,
userClient: deps.UserClient,
cache: deps.Cache,
stateKv: deps.StateKV,
directLinkClient: deps.DirectLinkClient,
encryptorFactory: deps.EncryptorFactory,
eventHub: deps.EventHub,
}
}

@ -153,27 +153,24 @@ func (n *myNavigator) Walk(ctx context.Context, levelFiles []*File, limit, depth
}
func (n *myNavigator) FollowTx(ctx context.Context) (func(), error) {
if _, ok := ctx.Value(inventory.TxCtx{}).(*inventory.Tx); !ok {
return nil, fmt.Errorf("navigator: no inherited transaction found in context")
}
newFileClient, _, _, err := inventory.WithTx(ctx, n.fileClient)
oldBase := n.baseNavigator.fileClient
revertFile, err := followTxClients(ctx, &n.fileClient)
if err != nil {
return nil, err
}
newUserClient, _, _, err := inventory.WithTx(ctx, n.userClient)
oldFileClient, oldUserClient := n.fileClient, n.userClient
revert := func() {
n.fileClient = oldFileClient
n.userClient = oldUserClient
n.baseNavigator.fileClient = oldFileClient
revertUser, err := followTxClients(ctx, &n.userClient)
if err != nil {
revertFile()
return nil, err
}
n.fileClient = newFileClient
n.userClient = newUserClient
n.baseNavigator.fileClient = newFileClient
return revert, nil
n.baseNavigator.fileClient = n.fileClient
return func() {
revertUser()
revertFile()
n.baseNavigator.fileClient = oldBase
}, nil
}
func (n *myNavigator) ExecuteHook(ctx context.Context, hookType fs.HookType, file *File) error {

@ -19,6 +19,31 @@ import (
"github.com/samber/lo"
)
// followTxClients swaps each client field to its tx-bound instance from the
// transaction in ctx, returning a revert closure that restores the originals.
func followTxClients[T inventory.TxOperator](ctx context.Context, fields ...*T) (func(), error) {
if _, ok := ctx.Value(inventory.TxCtx{}).(*inventory.Tx); !ok {
return nil, fmt.Errorf("navigator: no inherited transaction found in context")
}
olds := make([]T, len(fields))
for i, f := range fields {
newClient, _, _, err := inventory.WithTx(ctx, *f)
if err != nil {
return nil, err
}
olds[i] = *f
*f = newClient
}
return func() {
for i, f := range fields {
*f = olds[i]
}
}, nil
}
var (
ErrFsNotInitialized = fmt.Errorf("fs not initialized")
ErrPermissionDenied = serializer.NewError(serializer.CodeNoPermissionErr, "Permission denied", nil)

@ -351,27 +351,24 @@ func (n *shareNavigator) shareCapabilities() *boolset.BooleanSet {
}
func (n *shareNavigator) FollowTx(ctx context.Context) (func(), error) {
if _, ok := ctx.Value(inventory.TxCtx{}).(*inventory.Tx); !ok {
return nil, fmt.Errorf("navigator: no inherited transaction found in context")
}
newFileClient, _, _, err := inventory.WithTx(ctx, n.fileClient)
oldBase := n.baseNavigator.fileClient
revertFile, err := followTxClients(ctx, &n.fileClient)
if err != nil {
return nil, err
}
newSharClient, _, _, err := inventory.WithTx(ctx, n.shareClient)
oldFileClient, oldShareClient := n.fileClient, n.shareClient
revert := func() {
n.fileClient = oldFileClient
n.shareClient = oldShareClient
n.baseNavigator.fileClient = oldFileClient
revertShare, err := followTxClients(ctx, &n.shareClient)
if err != nil {
revertFile()
return nil, err
}
n.fileClient = newFileClient
n.shareClient = newSharClient
n.baseNavigator.fileClient = newFileClient
return revert, nil
n.baseNavigator.fileClient = n.fileClient
return func() {
revertShare()
revertFile()
n.baseNavigator.fileClient = oldBase
}, nil
}
func (n *shareNavigator) ExecuteHook(ctx context.Context, hookType fs.HookType, file *File) error {

@ -120,23 +120,17 @@ func (t *sharedWithMeNavigator) Walk(ctx context.Context, levelFiles []*File, li
}
func (n *sharedWithMeNavigator) FollowTx(ctx context.Context) (func(), error) {
if _, ok := ctx.Value(inventory.TxCtx{}).(*inventory.Tx); !ok {
return nil, fmt.Errorf("navigator: no inherited transaction found in context")
}
newFileClient, _, _, err := inventory.WithTx(ctx, n.fileClient)
oldBase := n.baseNavigator.fileClient
revert, err := followTxClients(ctx, &n.fileClient)
if err != nil {
return nil, err
}
oldFileClient := n.fileClient
revert := func() {
n.fileClient = oldFileClient
n.baseNavigator.fileClient = oldFileClient
}
n.fileClient = newFileClient
n.baseNavigator.fileClient = newFileClient
return revert, nil
n.baseNavigator.fileClient = n.fileClient
return func() {
revert()
n.baseNavigator.fileClient = oldBase
}, nil
}
func (n *sharedWithMeNavigator) ExecuteHook(ctx context.Context, hookType fs.HookType, file *File) error {

@ -135,23 +135,17 @@ func (t *trashNavigator) Walk(ctx context.Context, levelFiles []*File, limit, de
}
func (n *trashNavigator) FollowTx(ctx context.Context) (func(), error) {
if _, ok := ctx.Value(inventory.TxCtx{}).(*inventory.Tx); !ok {
return nil, fmt.Errorf("navigator: no inherited transaction found in context")
}
newFileClient, _, _, err := inventory.WithTx(ctx, n.fileClient)
oldBase := n.baseNavigator.fileClient
revert, err := followTxClients(ctx, &n.fileClient)
if err != nil {
return nil, err
}
oldFileClient := n.fileClient
revert := func() {
n.fileClient = oldFileClient
n.baseNavigator.fileClient = oldFileClient
}
n.fileClient = newFileClient
n.baseNavigator.fileClient = newFileClient
return revert, nil
n.baseNavigator.fileClient = n.fileClient
return func() {
revert()
n.baseNavigator.fileClient = oldBase
}, nil
}
func (n *trashNavigator) ExecuteHook(ctx context.Context, hookType fs.HookType, file *File) error {

@ -165,9 +165,21 @@ func NewFileManager(dep dependency.Dep, u *ent.User) FileManager {
l: dep.Logger(),
user: u,
settings: dep.SettingProvider(),
fs: dbfs.NewDatabaseFS(u, dep.FileClient(), dep.ShareClient(), dep.Logger(), dep.LockSystem(),
dep.SettingProvider(), dep.StoragePolicyClient(), dep.HashIDEncoder(), dep.UserClient(), dep.KV(), dep.NavigatorStateKV(),
dep.DirectLinkClient(), dep.EncryptorFactory(context.TODO()), dep.EventHub()),
fs: dbfs.NewDatabaseFS(u, dbfs.DBFSDependencies{
FileClient: dep.FileClient(),
ShareClient: dep.ShareClient(),
UserClient: dep.UserClient(),
StoragePolicyClient: dep.StoragePolicyClient(),
DirectLinkClient: dep.DirectLinkClient(),
Logger: dep.Logger(),
LockSystem: dep.LockSystem(),
SettingProvider: dep.SettingProvider(),
Hasher: dep.HashIDEncoder(),
Cache: dep.KV(),
StateKV: dep.NavigatorStateKV(),
EncryptorFactory: dep.EncryptorFactory(context.TODO()),
EventHub: dep.EventHub(),
}),
kv: dep.KV(),
config: config,
auth: dep.GeneralAuth(),

@ -0,0 +1,65 @@
package mediameta
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
)
// DMSDecimal converts degree/minute/second GPS components to a signed
// decimal degree; negative flips the sign for south/west references.
func DMSDecimal(deg, minutes, seconds float64, negative bool) float64 {
decimal := deg + minutes/60.0 + seconds/3600.0
if negative {
return -decimal
}
return decimal
}
// GpsRationalElem parses one rational GPS element of the form "n/d" into a
// decimal value. Returns 0 for malformed or zero-denominator elements.
func GpsRationalElem(elm string) float64 {
elements := strings.Split(elm, "/")
if len(elements) != 2 {
return 0
}
numerator, err := strconv.ParseFloat(elements[0], 64)
if err != nil {
return 0
}
denominator, err := strconv.ParseFloat(elements[1], 64)
if err != nil || denominator == 0 {
return 0
}
return numerator / denominator
}
// GpsMeta builds the latitude/longitude MediaMeta pair from raw EXIF parts.
// parse converts a raw coordinate string and its hemisphere reference into
// decimal degrees; drivers pass their vendor-specific parser.
func GpsMeta(latitude, longitude, latRef, lonRef string, parse func(raw, ref string) float64) []driver.MediaMeta {
if latitude == "" || longitude == "" || latRef == "" || lonRef == "" {
return nil
}
lat := parse(latitude, latRef)
lon := parse(longitude, lonRef)
if math.IsNaN(lat) || math.IsNaN(lon) {
return nil
}
latN, lngN := NormalizeGPS(lat, lon)
return []driver.MediaMeta{{
Key: GpsLat,
Value: fmt.Sprintf("%f", latN),
}, {
Key: GpsLng,
Value: fmt.Sprintf("%f", lngN),
}}
}
Loading…
Cancel
Save