Files
tickstep studio team e62bdbb9f1 refractor OSS url parser
2026-06-29 10:27:00 +08:00

447 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}