mirror of
https://gitee.com/zhangsken/aliyunpan.git
synced 2026-10-16 02:23:49 +08:00
447 lines
11 KiB
Go
447 lines
11 KiB
Go
// Copyright (c) 2020 tickstep.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
package utils
|
||
|
||
import (
|
||
"compress/gzip"
|
||
"crypto/md5"
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"io"
|
||
"io/ioutil"
|
||
"math"
|
||
"net/http"
|
||
"net/http/cookiejar"
|
||
"net/url"
|
||
"os"
|
||
"path"
|
||
"regexp"
|
||
"runtime"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
jsoniter "github.com/json-iterator/go"
|
||
uuid "github.com/satori/go.uuid"
|
||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||
"github.com/tickstep/library-go/ids"
|
||
)
|
||
|
||
// TrimPathPrefix 去除目录的前缀
|
||
func TrimPathPrefix(path, prefixPath string) string {
|
||
if prefixPath == "/" {
|
||
return path
|
||
}
|
||
return strings.TrimPrefix(path, prefixPath)
|
||
}
|
||
|
||
// ContainsString 检测字符串是否在字符串数组里
|
||
func ContainsString(ss []string, s string) bool {
|
||
for k := range ss {
|
||
if ss[k] == s {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// GetURLCookieString 返回cookie字串
|
||
func GetURLCookieString(urlString string, jar *cookiejar.Jar) string {
|
||
u, _ := url.Parse(urlString)
|
||
cookies := jar.Cookies(u)
|
||
cookieString := ""
|
||
for _, v := range cookies {
|
||
cookieString += v.String() + "; "
|
||
}
|
||
cookieString = strings.TrimRight(cookieString, "; ")
|
||
return cookieString
|
||
}
|
||
|
||
// DecompressGZIP 对 io.Reader 数据, 进行 gzip 解压
|
||
func DecompressGZIP(r io.Reader) ([]byte, error) {
|
||
gzipReader, err := gzip.NewReader(r)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
gzipReader.Close()
|
||
return ioutil.ReadAll(gzipReader)
|
||
}
|
||
|
||
// FlagProvided 检测命令行是否提供名为 name 的 flag, 支持多个name(names)
|
||
func FlagProvided(names ...string) bool {
|
||
if len(names) == 0 {
|
||
return false
|
||
}
|
||
var targetFlag *flag.Flag
|
||
for _, name := range names {
|
||
targetFlag = flag.Lookup(name)
|
||
if targetFlag == nil {
|
||
return false
|
||
}
|
||
if targetFlag.DefValue == targetFlag.Value.String() {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// Trigger 用于触发事件
|
||
func Trigger(f func()) {
|
||
if f == nil {
|
||
return
|
||
}
|
||
go f()
|
||
}
|
||
|
||
// TriggerOnSync 用于触发事件, 同步触发
|
||
func TriggerOnSync(f func()) {
|
||
if f == nil {
|
||
return
|
||
}
|
||
f()
|
||
}
|
||
|
||
func ParseVersionNum(versionStr string) int {
|
||
// 解析版本号字符串
|
||
// 正式版本号:v0.3.5
|
||
// 开发版本号:v0.3.5-dev
|
||
versionStr = strings.ReplaceAll(versionStr, "-dev", "")
|
||
versionStr = strings.ReplaceAll(versionStr, "v", "")
|
||
|
||
// 适配版本号:v0.3.5-1
|
||
if strings.Contains(versionStr, "-") {
|
||
versionStr = strings.Split(versionStr, "-")[0]
|
||
}
|
||
|
||
// 解析版本号数值
|
||
versionParts := strings.Split(versionStr, ".")
|
||
if len(versionParts) < 3 {
|
||
return 0
|
||
}
|
||
verNum := parseInt(versionParts[0])*1e4 + parseInt(versionParts[1])*1e2 + parseInt(versionParts[2])
|
||
return verNum
|
||
}
|
||
func parseInt(numStr string) int {
|
||
num, e := strconv.Atoi(numStr)
|
||
if e != nil {
|
||
return 0
|
||
}
|
||
return num
|
||
}
|
||
|
||
func ConvertTime(t time.Duration) string {
|
||
seconds := int64(t.Seconds())
|
||
return ConvertTimeSecond(seconds)
|
||
}
|
||
|
||
func ConvertTimeSecond(seconds int64) string {
|
||
MT := int64(1 * 60)
|
||
HT := int64(1 * 60 * 60)
|
||
|
||
if seconds <= 0 {
|
||
return "0秒"
|
||
}
|
||
if seconds < MT {
|
||
return fmt.Sprintf("%d秒", seconds)
|
||
}
|
||
if seconds >= MT && seconds < HT {
|
||
return fmt.Sprintf("%d分%d秒", seconds/MT, seconds%MT)
|
||
}
|
||
if seconds >= HT {
|
||
h := seconds / HT
|
||
tmp := seconds % HT
|
||
return fmt.Sprintf("%d小时%d分%d秒", h, tmp/MT, tmp%MT)
|
||
}
|
||
return "0秒"
|
||
}
|
||
|
||
// HasSuffix 判断是否以某字符串作为结尾
|
||
func HasSuffix(s, suffix string) bool {
|
||
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
|
||
}
|
||
|
||
// HasPrefix 判断是否以某字符串作为开始
|
||
func HasPrefix(s, prefix string) bool {
|
||
return len(s) >= len(prefix) && s[0:len(prefix)] == prefix
|
||
}
|
||
|
||
// GetUniqueKeyStr 获取本机唯一标识
|
||
func GetUniqueKeyStr() string {
|
||
keyStr := ids.GetUniqueId("", 32)
|
||
if len(keyStr) == 0 || keyStr == "" {
|
||
// default
|
||
keyStr = "AE8627B0296A4126A1434999C45ECAB2"
|
||
}
|
||
return keyStr
|
||
}
|
||
|
||
// PathExists 文件路径是否存在
|
||
func PathExists(path string) (bool, error) {
|
||
_, err := os.Stat(path)
|
||
if err == nil {
|
||
return true, nil
|
||
}
|
||
if os.IsNotExist(err) {
|
||
return false, nil
|
||
}
|
||
return false, err
|
||
}
|
||
|
||
// ObjectToJsonStr 转换成json字符串
|
||
func ObjectToJsonStr(v interface{}, useIndent bool) string {
|
||
r := ""
|
||
if useIndent {
|
||
if data, err := jsoniter.MarshalIndent(v, "", " "); err == nil {
|
||
r = string(data)
|
||
}
|
||
} else {
|
||
if data, err := jsoniter.MarshalIndent(v, "", ""); err == nil {
|
||
r = string(data)
|
||
}
|
||
|
||
}
|
||
return r
|
||
}
|
||
|
||
func UuidStr() string {
|
||
u4 := uuid.NewV4()
|
||
return u4.String()
|
||
}
|
||
|
||
// NowTimeStr 当前时间字符串,格式为:2006-01-02 15:04:05
|
||
func NowTimeStr() string {
|
||
return time.Now().Format("2006-01-02 15:04:05")
|
||
}
|
||
|
||
func defaultZeroTime() time.Time {
|
||
value := "1971-01-01 08:00:00"
|
||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||
if t, e := time.ParseInLocation("2006-01-02 15:04:05", value, cz); e == nil {
|
||
return t
|
||
}
|
||
return time.Time{}
|
||
}
|
||
|
||
// ParseTimeStr 反解析时间字符串
|
||
func ParseTimeStr(value string) time.Time {
|
||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||
if t, e := time.ParseInLocation("2006-01-02 15:04:05", value, cz); e == nil {
|
||
return t
|
||
}
|
||
return defaultZeroTime()
|
||
}
|
||
|
||
// Md5Str MD5哈希计算
|
||
func Md5Str(text string) string {
|
||
h := md5.New()
|
||
h.Write([]byte(text))
|
||
re := h.Sum(nil)
|
||
sb := &strings.Builder{}
|
||
fmt.Fprintf(sb, "%x", re)
|
||
return strings.ToLower(sb.String())
|
||
}
|
||
|
||
// IsLocalAbsPath 是否是本地绝对路径
|
||
func IsLocalAbsPath(filePath string) bool {
|
||
if runtime.GOOS == "windows" {
|
||
// 是否是windows路径
|
||
matched, _ := regexp.MatchString("^([a-zA-Z]:)", filePath)
|
||
if matched {
|
||
// windows volume label
|
||
return true
|
||
}
|
||
return false
|
||
} else {
|
||
return path.IsAbs(filePath)
|
||
}
|
||
}
|
||
|
||
// IsPanAbsPath 是否是云盘绝对路径
|
||
func IsPanAbsPath(filePath string) bool {
|
||
return path.IsAbs(filePath)
|
||
}
|
||
|
||
// IsExcludeFile 是否是指定排除的文件
|
||
func IsExcludeFile(filePath string, excludeNames *[]string) bool {
|
||
if excludeNames == nil || len(*excludeNames) == 0 {
|
||
return false
|
||
}
|
||
|
||
for _, pattern := range *excludeNames {
|
||
fileName := path.Base(strings.ReplaceAll(filePath, "\\", "/"))
|
||
m, _ := regexp.MatchString(pattern, fileName)
|
||
if m {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// ResizeUploadBlockSize 自动调整分片大小,方便支持极大单文件上传。返回新的分片大小
|
||
func ResizeUploadBlockSize(fileSize, defaultBlockSize int64) int64 {
|
||
if (aliyunpan.MaxPartNum * defaultBlockSize) > fileSize {
|
||
return defaultBlockSize
|
||
}
|
||
sizeOfMB := int64(math.Ceil(float64(fileSize) / float64(aliyunpan.MaxPartNum) / 1024.0))
|
||
return sizeOfMB * 1024
|
||
}
|
||
|
||
// UnixTime2LocalFormatStr 时间戳转换为东8区时间字符串
|
||
func UnixTime2LocalFormatStr(unixTime int64) string {
|
||
t := time.Unix(unixTime, 0)
|
||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||
return t.In(cz).Format("2006-01-02T15:04:05.000Z")
|
||
}
|
||
|
||
// FormatSpeedFixedWidth 格式化速度显示
|
||
// speed: 速度值,单位 Byte/s
|
||
// 返回格式: " xxx.xx MB/s" (固定长度,左侧用空格补齐)
|
||
func FormatSpeedFixedWidth(speed int64, totalWidth int) string {
|
||
const (
|
||
kb = 1024.0
|
||
mb = kb * 1024
|
||
gb = mb * 1024
|
||
tb = gb * 1024
|
||
)
|
||
|
||
var value float64
|
||
var unit string
|
||
|
||
speedFloat := float64(speed)
|
||
|
||
switch {
|
||
case speedFloat >= tb:
|
||
value = speedFloat / tb
|
||
unit = "TB/s"
|
||
case speedFloat >= gb:
|
||
value = speedFloat / gb
|
||
unit = "GB/s"
|
||
case speedFloat >= mb:
|
||
value = speedFloat / mb
|
||
unit = "MB/s"
|
||
case speedFloat >= kb:
|
||
value = speedFloat / kb
|
||
unit = "KB/s"
|
||
default:
|
||
value = speedFloat
|
||
unit = "B/s"
|
||
}
|
||
|
||
// 格式化为两位小数
|
||
formatted := fmt.Sprintf("%.2f %s", value, unit)
|
||
|
||
// 如果不足指定宽度,左侧用空格补齐
|
||
if len(formatted) < totalWidth {
|
||
spaces := strings.Repeat(" ", totalWidth-len(formatted))
|
||
return spaces + formatted
|
||
}
|
||
|
||
return formatted
|
||
}
|
||
|
||
func GetPublicIP() (string, error) {
|
||
type IPResponse struct {
|
||
Origin string `json:"origin"`
|
||
}
|
||
resp, err := http.Get("https://httpbin.org/ip")
|
||
if err != nil {
|
||
return "", fmt.Errorf("请求失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return "", fmt.Errorf("HTTP 状态码异常: %d", resp.StatusCode)
|
||
}
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return "", fmt.Errorf("读取响应体失败: %w", err)
|
||
}
|
||
|
||
var ipResp IPResponse
|
||
if err := json.Unmarshal(body, &ipResp); err != nil {
|
||
return "", fmt.Errorf("解析 JSON 失败: %w", err)
|
||
}
|
||
|
||
if ipResp.Origin == "" {
|
||
return "", fmt.Errorf("响应中无 IP 信息")
|
||
}
|
||
|
||
// 取第一个 IP(处理代理/X-Forwarded-For 情况)
|
||
ip := strings.TrimSpace(strings.Split(ipResp.Origin, ",")[0])
|
||
return ip, nil
|
||
}
|
||
|
||
// GetExpiredTimeSecFromOSSUrl 从OSS URL中解析出统一的过期时间戳(秒)
|
||
func GetExpiredTimeSecFromOSSUrl(rawURL string) (int64, error) {
|
||
parsedURL, err := url.Parse(rawURL)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("failed to parse URL: %w", err)
|
||
}
|
||
|
||
query := parsedURL.Query()
|
||
sigVersion := query.Get("x-oss-signature-version")
|
||
|
||
var expires int64 = 0
|
||
|
||
// 检测版本标识(不区分大小写)
|
||
if strings.Contains(sigVersion, "OSS4") {
|
||
// V4 签名:x-oss-expires 为相对秒数,需要结合 x-oss-date
|
||
expiresStr := query.Get("x-oss-expires")
|
||
if expiresStr == "" {
|
||
return 0, fmt.Errorf("V4 signature: 'x-oss-expires' parameter not found")
|
||
}
|
||
relSec, err := strconv.ParseInt(expiresStr, 10, 64)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("V4 signature: invalid 'x-oss-expires' value: %w", err)
|
||
}
|
||
|
||
dateStr := query.Get("x-oss-date")
|
||
if dateStr == "" {
|
||
return 0, fmt.Errorf("V4 signature: 'x-oss-date' parameter not found")
|
||
}
|
||
// 解析 ISO8601 UTC 时间,例如 "20231220T083018Z"
|
||
t, err := time.Parse("20060102T150405Z", dateStr)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("V4 signature: failed to parse 'x-oss-date': %w", err)
|
||
}
|
||
expires = t.Unix() + relSec
|
||
|
||
} else if strings.Contains(sigVersion, "OSS3") || strings.Contains(sigVersion, "OSS2") {
|
||
// V2 / V3:x-oss-expires 为绝对时间戳
|
||
expiresStr := query.Get("x-oss-expires")
|
||
if expiresStr == "" {
|
||
return 0, fmt.Errorf("V2/V3 signature: 'x-oss-expires' parameter not found")
|
||
}
|
||
expires, err = strconv.ParseInt(expiresStr, 10, 64)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("V2/V3 signature: invalid 'x-oss-expires' value: %w", err)
|
||
}
|
||
|
||
} else {
|
||
// 默认为 V1 签名(没有 x-oss-signature-version 或为空)
|
||
expiresStr := query.Get("Expires")
|
||
if expiresStr == "" {
|
||
return 0, fmt.Errorf("V1 signature: 'Expires' parameter not found")
|
||
}
|
||
expires, err = strconv.ParseInt(expiresStr, 10, 64)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("V1 signature: invalid 'Expires' value: %w", err)
|
||
}
|
||
}
|
||
|
||
return expires, nil
|
||
}
|