diff --git a/assets b/assets index e2d4f13..1c827ee 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit e2d4f13a54dfd424cfbc129664772e104ccf97fc +Subproject commit 1c827ee20a1628089cbab73ae8cbd81e2c8310c9 diff --git a/go.mod b/go.mod index e7233f5..2253201 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1 github.com/rakyll/statik v0.1.7 github.com/robfig/cron/v3 v3.0.1 + github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect github.com/smartystreets/goconvey v1.6.4 // indirect github.com/speps/go-hashids v2.0.0+incompatible github.com/stretchr/testify v1.5.1 diff --git a/go.sum b/go.sum index 280b0f8..4681050 100644 --- a/go.sum +++ b/go.sum @@ -208,6 +208,8 @@ github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Ung github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= diff --git a/models/file.go b/models/file.go index 7b1d6e8..86ea4b2 100644 --- a/models/file.go +++ b/models/file.go @@ -26,6 +26,12 @@ type File struct { // 数据库忽略字段 Position string `gorm:"-"` + + // exif 信息 + ExifModel string + ExifDateTime time.Time + ExifLatLong string + ExifAddress string } func init() { @@ -79,6 +85,16 @@ func GetFilesByIDs(ids []uint, uid uint) ([]File, error) { return files, result.Error } +// GetEmptyLocationFilesByPage 分页获取所有经纬度未更新文件 +func GetEmptyLocationFilesByPage(page uint, pageSize uint) ([]File, error) { + var files []File + var result *gorm.DB + var offset = int(page) * int(pageSize) - int(pageSize) + result = DB.Where("exif_lat_long != '' AND (exif_address is null or exif_address = '')").Limit(pageSize). + Offset(offset).Find(&files) + return files, result.Error +} + // GetFilesByKeywords 根据关键字搜索文件, // UID为0表示忽略用户,只根据文件ID检索 func GetFilesByKeywords(uid uint, keywords ...interface{}) ([]File, error) { @@ -199,6 +215,23 @@ func (file *File) UpdateSourceName(value string) error { return DB.Model(&file).Set("gorm:association_autoupdate", false).Update("source_name", value).Error } +// UpdatePicExifModel 更新文件的设备信息 +func (file *File) UpdatePicExifModel(value string) error { + return DB.Model(&file).Update("exif_model", value).Error +} +// UpdatePicExifDateTime 更新图片EXIF时间 +func (file *File) UpdatePicExifDateTime(value time.Time) error { + return DB.Model(&file).Update("exif_date_time", value).Error +} +// UpdatePicExifLatLong 更新图片EXIF坐标 +func (file *File) UpdatePicExifLatLong(value string) error { + return DB.Model(&file).Update("exif_lat_long", value).Error +} +// UpdatePicExifAddress 更新图片EXIF位置信息 +func (file *File) UpdatePicExifAddress(value string) error { + return DB.Model(&file).Update("exif_address", value).Error +} + /* 实现 webdav.FileInfo 接口 */ diff --git a/models/migration.go b/models/migration.go index 4683d81..73c1856 100644 --- a/models/migration.go +++ b/models/migration.go @@ -147,6 +147,7 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti {Name: "home_view_method", Value: "icon", Type: "view"}, {Name: "share_view_method", Value: "list", Type: "view"}, {Name: "cron_garbage_collect", Value: "@hourly", Type: "cron"}, + {Name: "cron_sync_photo_lat_long_to_address", Value: "@hourly", Type: "cron"}, {Name: "authn_enabled", Value: "0", Type: "authn"}, {Name: "captcha_height", Value: "60", Type: "captcha"}, {Name: "captcha_width", Value: "240", Type: "captcha"}, diff --git a/pkg/conf/version.go b/pkg/conf/version.go index 890cbec..51c75d1 100644 --- a/pkg/conf/version.go +++ b/pkg/conf/version.go @@ -4,7 +4,7 @@ package conf var BackendVersion = "3.2.1" // RequiredDBVersion 与当前版本匹配的数据库版本 -var RequiredDBVersion = "3.2.0" +var RequiredDBVersion = "3.2.1" // RequiredStaticVersion 与当前版本匹配的静态资源版本 var RequiredStaticVersion = "3.2.1" diff --git a/pkg/crontab/address.go b/pkg/crontab/address.go new file mode 100644 index 0000000..dea5a84 --- /dev/null +++ b/pkg/crontab/address.go @@ -0,0 +1,108 @@ +package crontab + +import ( + "net/http" + "encoding/json" + + model "github.com/cloudreve/Cloudreve/v3/models" + "github.com/cloudreve/Cloudreve/v3/pkg/util" + "github.com/cloudreve/Cloudreve/v3/pkg/request" +) + +type AddressJson struct { + Status int `json:"status"` + Message string `json:"message"` + RequestID string `json:"request_id"` + Result struct { + Location struct { + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` + } `json:"location"` + Address string `json:"address"` + FormattedAddresses struct { + Recommend string `json:"recommend"` + Rough string `json:"rough"` + } `json:"formatted_addresses"` + AddressComponent struct { + Nation string `json:"nation"` + Province string `json:"province"` + City string `json:"city"` + District string `json:"district"` + Street string `json:"street"` + StreetNumber string `json:"street_number"` + } `json:"address_component"` + } `json:"result"` +} + +type AddressExif struct{ + Address string `json:"address"` + Nation string `json:"nation"` + Province string `json:"province"` + City string `json:"city"` + District string `json:"district"` + Street string `json:"street"` + StreetNumber string `json:"street_number"` +} + +func syncPhotoAddress() { + // 同步照片的经纬度为文本地址 + syncPhotoLatLongToAddress() + + util.Log().Info("定时任务 [cron_sync_photo_lat_long_to_address] 执行完毕") +} + +func syncPhotoLatLongToAddress() { + page := 1 + pageSize := 10 + for true{ + files , _ := model.GetEmptyLocationFilesByPage(uint(page), uint(pageSize)) + if len(files) <= 0 { + break + } + for i := 0; i < len(files); i++{ + file := files[i] + util.Log().Debug("file name: %s",file.Name) + util.Log().Debug("file ExifLatLong: %s",file.ExifLatLong) + util.Log().Debug("file ExifAddress: %s",file.ExifAddress) + + // 获取文件数据流 + url := "https://apis.map.qq.com/ws/geocoder/v1/?location="+ file.ExifLatLong +"&get_poi=1&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77" + client := request.HTTPClient{} + resp := client.Request( + "GET", + url, + nil, + request.WithHeader( + http.Header{"Referer": {"https://lbs.qq.com/"}}, + ), + ) + + respString, err := resp.GetResponse() + if err != nil{ + util.Log().Warning("response error: %s",err) + } + + var addressJson AddressJson + err = json.Unmarshal([]byte(respString), &addressJson) + if err != nil { + util.Log().Warning("解析经纬度结果错误原始文本: %s",respString) + util.Log().Warning("解析经纬度结果错误: %s",err) + continue + } + var addressInfo AddressExif + addressInfo.Address = addressJson.Result.Address + addressInfo.StreetNumber = addressJson.Result.AddressComponent.StreetNumber + addressInfo.District = addressJson.Result.AddressComponent.District + addressInfo.City = addressJson.Result.AddressComponent.City + addressInfo.Province = addressJson.Result.AddressComponent.Province + addressInfo.Nation = addressJson.Result.AddressComponent.Nation + + if addressInfoStr, err := json.Marshal(addressInfo); err == nil { + file.UpdatePicExifAddress(string(addressInfoStr)) + util.Log().Debug("解析经纬度结果重组: %s",string(addressInfoStr)) + } + } + + page++ + } +} diff --git a/pkg/crontab/init.go b/pkg/crontab/init.go index 1b8a322..9d91253 100644 --- a/pkg/crontab/init.go +++ b/pkg/crontab/init.go @@ -21,13 +21,15 @@ func Reload() { func Init() { util.Log().Info("初始化定时任务...") // 读取cron日程设置 - options := model.GetSettingByNames("cron_garbage_collect") + options := model.GetSettingByNames("cron_garbage_collect","cron_sync_photo_lat_long_to_address") Cron := cron.New() for k, v := range options { var handler func() switch k { case "cron_garbage_collect": handler = garbageCollect + case "cron_sync_photo_lat_long_to_address": + handler = syncPhotoAddress default: util.Log().Warning("未知定时任务类型 [%s],跳过", k) continue diff --git a/pkg/filesystem/hooks.go b/pkg/filesystem/hooks.go index 3b5755d..062edec 100644 --- a/pkg/filesystem/hooks.go +++ b/pkg/filesystem/hooks.go @@ -321,6 +321,7 @@ func GenericAfterUpload(ctx context.Context, fs *FileSystem) error { fs.recycleLock.Lock() go func() { defer fs.recycleLock.Unlock() + file.Name = strings.TrimRight(file.Name, ".tacitpart") fs.GenerateThumbnail(ctx, file) }() } diff --git a/pkg/filesystem/image.go b/pkg/filesystem/image.go index a2e7e08..7c5cbf1 100644 --- a/pkg/filesystem/image.go +++ b/pkg/filesystem/image.go @@ -11,6 +11,8 @@ import ( "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response" "github.com/cloudreve/Cloudreve/v3/pkg/thumb" "github.com/cloudreve/Cloudreve/v3/pkg/util" + + "github.com/rwcarlsen/goexif/exif" ) /* ================ @@ -54,7 +56,6 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) { if !IsInExtensionList(HandledExtension, file.Name) { return } - // 新建上下文 newCtx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -65,7 +66,6 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) { return } defer source.Close() - image, err := thumb.NewThumbFromFile(source, file.Name) if err != nil { util.Log().Warning("生成缩略图时无法解析 [%s] 图像数据:%s", file.SourceName, err) @@ -74,7 +74,6 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) { // 获取原始图像尺寸 w, h := image.GetSize() - // 生成缩略图 image.GetThumb(fs.GenerateThumbnailSize(w, h)) // 保存到文件 @@ -83,7 +82,6 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) { util.Log().Warning("无法保存缩略图:%s", err) return } - // 更新文件的图像信息 if file.Model.ID > 0 { err = file.UpdatePicInfo(fmt.Sprintf("%d,%d", w, h)) @@ -91,6 +89,40 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) { file.PicInfo = fmt.Sprintf("%d,%d", w, h) } + // 更新文件的图像信息 + // 记录文件句柄位置并还原, 获取 exif 信息 + currentPosition, err := source.Seek(0, 1) + source.Seek(0,0) + x, err := exif.Decode(source) + source.Seek(currentPosition, 0) + if err != nil { + util.Log().Warning("照片解析EXIF失败:%s", err) + }else{ + ExifCamModel, _ := x.Get(exif.Model) + file.ExifModel,_ = ExifCamModel.StringVal() + + ExifDateTime, _ := x.DateTime() + if !ExifDateTime.IsZero() { + file.ExifDateTime = ExifDateTime + } + + lat, long, _ := x.LatLong() + if lat > 0 && long > 0 { + file.ExifLatLong = fmt.Sprintf("%f,%f", lat, long) + } + util.Log().Debug("照片的经纬度:%f,%f", lat,long) + if file.Model.ID > 0 { + file.UpdatePicExifModel(file.ExifModel) + if !ExifDateTime.IsZero() { + file.UpdatePicExifDateTime(file.ExifDateTime) + } + if lat > 0 && long > 0 { + file.UpdatePicExifLatLong(file.ExifLatLong) + } + } + } + + // 失败时删除缩略图文件 if err != nil { _, _ = fs.Handler.Delete(newCtx, []string{file.SourceName + conf.ThumbConfig.FileSuffix}) diff --git a/pkg/filesystem/manage.go b/pkg/filesystem/manage.go index 91100a5..3ee5362 100644 --- a/pkg/filesystem/manage.go +++ b/pkg/filesystem/manage.go @@ -28,6 +28,10 @@ type Object struct { Type string `json:"type"` Date string `json:"date"` Key string `json:"key,omitempty"` + ExifModel string `json:"exif_model"` + ExifDateTime string `json:"exif_date_time"` + ExifLatLong string `json:"exif_lat_long"` + ExifAddress string `json:"exit_address"` } // Rename 重命名对象 @@ -350,6 +354,10 @@ func (fs *FileSystem) listObjects(ctx context.Context, parent string, files []mo Size: 0, Type: "dir", Date: subFolder.CreatedAt.Format("2006-01-02 15:04:05"), + ExifModel: "", + ExifDateTime: "", + ExifLatLong: "", + ExifAddress: "", }) } @@ -361,7 +369,10 @@ func (fs *FileSystem) listObjects(ctx context.Context, parent string, files []mo processedPath = parent } } - + ExifDateTimeText := "" + if !file.ExifDateTime.IsZero(){ + ExifDateTimeText = file.ExifDateTime.Format("2006-01-02 15:04:05") + } newFile := Object{ ID: hashid.HashID(file.ID, hashid.FileID), Name: file.Name, @@ -370,6 +381,10 @@ func (fs *FileSystem) listObjects(ctx context.Context, parent string, files []mo Size: file.Size, Type: "file", Date: file.CreatedAt.Format("2006-01-02 15:04:05"), + ExifModel: file.ExifModel, + ExifDateTime: ExifDateTimeText, + ExifLatLong: file.ExifLatLong, + ExifAddress: file.ExifAddress, } if shareKey != "" { newFile.Key = shareKey