oss对象存储操作模块
参考文档 对象/文件(Object)_对象存储(OSS)-阿里云帮助中心
代码已上传至仓库lemonnmin/oss_operation
package main
import (
"fmt"
"io"
"log"
"math/rand"
"mime"
"net/http"
"path/filepath"
"time"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/gin-gonic/gin"
)
func main() {
const downloadPath = "C:/Users/liu/Downloads"
// 配置 OSS 相关信息
endpoint := "oss-cn-hangzhou.aliyuncs.com" // OSS Endpoint
accessKeyID := "LTAI5tCiXp2gh43NSBQGMSZ7"
accessKeySecret := "xNDV5YzOkjTpfSjAGwXVgr5qncP99k"
bucketName := "liulemon-blog"
// region := "oss-cn-hangzhou"
client, err := oss.New(endpoint, accessKeyID, accessKeySecret)
if err != nil {
log.Fatal("Failed to create OSS client: ", err)
}
// 获取 Bucket 对象
bucket, err := client.Bucket(bucketName)
if err != nil {
log.Fatal("Failed to get bucket: ", err)
}
// 创建一个默认的 Gin 路由引擎
r := gin.Default()
// 定义一个 GET 路由
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello, Gin!",
})
})
// 定义一个带参数的 GET 路由
r.GET("/isexist/:name", func(c *gin.Context) {
name := c.Param("name") // 获取 URL 路径参数
_, err := bucket.GetObjectMeta(name)
if err != nil {
if ossError, ok := err.(*oss.ServiceError); ok {
// 如果是 404 错误,表示对象不存在
if ossError.Code == "NoSuchKey" {
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Object '%s' does not exist", name),
})
} else {
// 其他错误
c.JSON(http.StatusInternalServerError, gin.H{
"message": "Error checking object: " + ossError.Message,
})
}
} else {
// 如果出现非 OSS 错误
c.JSON(http.StatusInternalServerError, gin.H{
"message": "Error: " + err.Error(),
})
}
} else {
// 如果没有错误,表示对象存在
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Object '%s' exists", name),
})
}
})
// 路由处理文件下载
r.GET("/download/:object", func(c *gin.Context) {
objectName := c.Param("object") // 从URL参数获取对象名
ext := filepath.Ext(objectName)
if ext == "" {
// 如果没有扩展名,可以选择给它一个默认的扩展名
ext = ".bin"
}
// 获取文件元数据,查看文件大小
meta, err := bucket.GetObjectMeta(objectName)
if err != nil {
log.Printf("Failed to get object metadata: %v", err)
c.JSON(500, gin.H{
"message": "Failed to get object metadata",
})
return
}
// 获取文件大小
fileSize := meta["Content-Length"] // 从 metadata 中获取文件大小
// 获取文件流
body, err := bucket.GetObject(objectName)
if err != nil {
log.Fatalf("Failed to get object: %v", err)
c.JSON(500, gin.H{
"message": "Failed to get object",
})
}
defer body.Close()
filename := generateRandomFilename(ext)
// 设置响应头
c.Header("Content-Disposition", "attachment; filename="+filename)
c.Header("Content-Type", mime.TypeByExtension(ext)) // 根据扩展名设置 MIME 类型
c.Header("Content-Length", fmt.Sprintf("%d", fileSize)) // 设置文件大小
// 流式传输文件内容返回给客户端
_, err = io.Copy(c.Writer, body)
if err != nil {
log.Printf("Failed to send file to client: %v", err)
c.JSON(500, gin.H{
"message": "Failed to send file to client",
})
return
}
log.Println("File downloaded successfully:", filename)
c.JSON(200, gin.H{
"message": "File downloaded successfully",
"file": filename,
})
})
r.POST("/upload", func(c *gin.Context) {
// 获取上传的文件
file, err := c.FormFile("file")
if err != nil {
log.Printf("Failed to get file from form: %v", err)
c.JSON(400, gin.H{"message": "Failed to get file"})
return
}
// 指定要上传到 OSS 的文件路径(可以使用文件名或自定义路径)
objectName := file.Filename
src, err := file.Open()
if err != nil {
log.Fatalf("Failed to open file: %v", err)
c.JSON(400, gin.H{"message": "Failed to open file"})
return
}
defer src.Close()
// 指定待上传的网络流。
// 从网络流中读取数据,并将其上传至 OSS。
err = bucket.PutObject(objectName, src)
if err != nil {
log.Fatalf("Failed to fetch URL: %v", err)
c.JSON(500, gin.H{"message": "Failed to upload file to OSS"})
return
}
log.Println("File uploaded successfully.")
c.JSON(200, gin.H{"message": "File uploaded successfully"})
})
// 定义一个 POST 路由
r.DELETE("/delete/:object", func(c *gin.Context) {
objectName := c.Param("object") // 从URL参数获取对象名
// 调用 OSS DeleteObject 方法删除对象
err := bucket.DeleteObject(objectName)
if err != nil {
// 如果发生错误,返回失败响应
c.JSON(500, gin.H{
"status": "error",
"message": fmt.Sprintf("Failed to delete object: %s", err.Error()),
})
return
}
// 如果删除成功,返回成功响应
c.JSON(200, gin.H{
"status": "success",
"message": fmt.Sprintf("Object '%s' deleted successfully", objectName),
})
})
r.GET("/list", func(c *gin.Context) {
// 假设你已经设置好了 OSS 客户端和存储桶
var allObjects []string
marker := ""
for {
lsRes, err := bucket.ListObjects(oss.Marker(marker))
if err != nil {
log.Fatalf("Failed to list objects: %v", err)
c.JSON(500, gin.H{
"status": "error",
"message": fmt.Sprintf("Failed to list objects: %s", err.Error()),
})
}
// 打印列举结果。默认情况下,一次返回100条记录。
for _, object := range lsRes.Objects {
allObjects = append(allObjects, object.Key)
}
// 如果还有更多对象需要列举,则更新marker并继续循环。
if lsRes.IsTruncated {
marker = lsRes.NextMarker
} else {
break
}
}
log.Println("All objects have been listed.")
c.JSON(200, gin.H{
"status": "success",
"message": "All objects have been listed",
"objects": allObjects,
})
})
r.GET("/invertcode/:audio", func(c *gin.Context) {
audio := c.Param("audio")
// 调用 OSS GetObject 方法获取对象
object, err := bucket.GetObject(audio)
if err != nil {
log.Println("Error getting object:", err)
c.JSON(500, gin.H{
"status": "error",
"message": "Failed to get object",
})
return
}
// 转码操作 TODO!
c.JSON(200, gin.H{
"message": "invertcode success",
"file": object,
})
defer object.Close()
})
// 启动服务器,监听端口 8080
r.Run(":8080")
}
func generateRandomFilename(ext string) string {
// 设置随机数种子为当前时间戳
rand.Seed(time.Now().UnixNano())
// 生成一个随机字符串
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
length := 10 // 文件名长度
var filename []byte
for i := 0; i < length; i++ {
filename = append(filename, charset[rand.Intn(len(charset))])
}
// 使用时间戳作为文件名的一部分
timestamp := time.Now().Unix()
// 组合随机字符串和时间戳来生成文件名
return fmt.Sprintf("%d_%s%s", timestamp, string(filename), ext)
}
主要功能
-
环境变量管理:
- 自动创建
.env文件以存储 OSS 配置信息。 - 加载环境变量以初始化 OSS 客户端。
- 自动创建
-
基本路由:
GET /:返回简单的 JSON 消息。GET /isexist/:name:检查指定对象是否存在于 OSS 中。
- 在这个代码中用到了GetObjectMeta方法,路由模式是get类型
// 定义一个带参数的 GET 路由 r.GET("/isexist/:name", func(c *gin.Context) { name := c.Param("name") // 获取 URL 路径参数 _, err := bucket.GetObjectMeta(name) if err != nil { if ossError, ok := err.(*oss.ServiceError); ok { // 如果是 404 错误,表示对象不存在 if ossError.Code == "NoSuchKey" { c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("Object '%s' does not exist", name), }) } else { // 其他错误 c.JSON(http.StatusInternalServerError, gin.H{ "message": "Error checking object: " + ossError.Message, }) } } else { // 如果出现非 OSS 错误 c.JSON(http.StatusInternalServerError, gin.H{ "message": "Error: " + err.Error(), }) } } else { // 如果没有错误,表示对象存在 c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("Object '%s' exists", name), }) } })GET /download/:object:下载指定的对象。 下载网络流
- 注意获取扩展名,方便之后生成随机的文件名,用到了filepath.Ext()
- 获取文件大小,从获得的元数据中读取信息
- GetObject获得文件流之后设置响应的信息,流式传输文件内容返回给客户端
r.GET("/download/:object", func(c *gin.Context) {...}:定义了一个GET路由/download/:object,其中:object是一个路径参数,表示要下载的文件的名称。当客户端访问这个路由时,Gin框架会调用这个匿名函数来处理请求。objectName := c.Param("object"):从请求的URL中获取object参数的值,即要下载的文件的名称。ext := filepath.Ext(objectName):使用filepath.Ext函数获取文件的扩展名。if ext == "" { ext = ".bin" }:如果文件没有扩展名,则给它一个默认的扩展名.bin。meta, err := bucket.GetObjectMeta(objectName):调用阿里云OSS的GetObjectMeta方法获取文件的元数据,包括文件大小等信息。fileSize := meta["Content-Length"]:从元数据中获取文件的大小,并将其赋值给fileSize变量。body, err := bucket.GetObject(objectName):调用阿里云OSS的GetObject方法获取文件的内容。filename := generateRandomFilename(ext):调用generateRandomFilename函数生成一个随机的文件名,用于保存下载的文件。c.Header("Content-Disposition", "attachment; filename="+filename):设置HTTP响应头中的Content-Disposition,指示客户端将文件作为附件下载,并指定文件名。c.Header("Content-Type", mime.TypeByExtension(ext)):根据文件的扩展名设置HTTP响应头中的Content-Type,以便客户端正确处理文件类型。c.Header("Content-Length", fmt.Sprintf("%d", fileSize)):设置HTTP响应头中的Content-Length,指示文件的大小。_, err = io.Copy(c.Writer, body):使用io.Copy函数将文件内容流式传输到客户端。log.Println("File downloaded successfully:", filename):在日志中记录文件下载成功的信息。
-
c.JSON(200, gin.H{ "message": "File downloaded successfully", "file": filename }):向客户端返回一个JSON响应,指示文件下载成功,并包含下载的文件名。r.GET("/download/:object", func(c *gin.Context) { objectName := c.Param("object") // 从URL参数获取对象名 ext := filepath.Ext(objectName) if ext == "" { // 如果没有扩展名,可以选择给它一个默认的扩展名 ext = ".bin" } // 获取文件元数据,查看文件大小 meta, err := bucket.GetObjectMeta(objectName) if err != nil { log.Printf("Failed to get object metadata: %v", err) c.JSON(500, gin.H{ "message": "Failed to get object metadata", }) return } // 获取文件大小 fileSize := meta["Content-Length"] // 从 metadata 中获取文件大小 // 获取文件流 body, err := bucket.GetObject(objectName) if err != nil { log.Fatalf("Failed to get object: %v", err) c.JSON(500, gin.H{ "message": "Failed to get object", }) } defer body.Close() filename := generateRandomFilename(ext) // 设置响应头 c.Header("Content-Disposition", "attachment; filename="+filename) c.Header("Content-Type", mime.TypeByExtension(ext)) // 根据扩展名设置 MIME 类型 c.Header("Content-Length", fmt.Sprintf("%d", fileSize)) // 设置文件大小 // 流式传输文件内容返回给客户端 _, err = io.Copy(c.Writer, body) if err != nil { log.Printf("Failed to send file to client: %v", err) c.JSON(500, gin.H{ "message": "Failed to send file to client", }) return } log.Println("File downloaded successfully:", filename) c.JSON(200, gin.H{ "message": "File downloaded successfully", "file": filename, }) })POST /upload:上传文件到 OSS。
- 先从网络流(请求)中获得文件,file.Open()打开文件
- 使用PutObject上传到oss
r.POST("/upload", func(c *gin.Context) { // 获取上传的文件 file, err := c.FormFile("file") if err != nil { log.Printf("Failed to get file from form: %v", err) c.JSON(400, gin.H{"message": "Failed to get file"}) return } // 指定要上传到 OSS 的文件路径(可以使用文件名或自定义路径) objectName := file.Filename src, err := file.Open() if err != nil { log.Fatalf("Failed to open file: %v", err) c.JSON(400, gin.H{"message": "Failed to open file"}) return } defer src.Close() // 指定待上传的网络流。 // 从网络流中读取数据,并将其上传至 OSS。 err = bucket.PutObject(objectName, src) if err != nil { log.Fatalf("Failed to fetch URL: %v", err) c.JSON(500, gin.H{"message": "Failed to upload file to OSS"}) return } log.Println("File uploaded successfully.") c.JSON(200, gin.H{"message": "File uploaded successfully"}) })DELETE /delete/:object:删除指定的对象。
- 获取文件名字
- bucket.DeleteObject删除oss对象
// 定义一个 POST 路由 r.DELETE("/delete/:object", func(c *gin.Context) { objectName := c.Param("object") // 从URL参数获取对象名 // 调用 OSS DeleteObject 方法删除对象 err := bucket.DeleteObject(objectName) if err != nil { // 如果发生错误,返回失败响应 c.JSON(500, gin.H{ "status": "error", "message": fmt.Sprintf("Failed to delete object: %s", err.Error()), }) return } // 如果删除成功,返回成功响应 c.JSON(200, gin.H{ "status": "success", "message": fmt.Sprintf("Object '%s' deleted successfully", objectName), }) })GET /list:列出所有对象。
- bucket.ListObjects(oss.Marker(marker)),-
oss.Marker(marker):指定从哪个对象开始列出。marker是上一次请求的最后一个对象的名称,用于控制分页。marker用于标记分页的起始位置,初始值为空字符串表示从头开始。 - allObjects = append(allObjects, object.Key),生成需要返回的数据
r.GET("/list", func(c *gin.Context) { // 假设你已经设置好了 OSS 客户端和存储桶 var allObjects []string marker := "" for { lsRes, err := bucket.ListObjects(oss.Marker(marker)) if err != nil { log.Fatalf("Failed to list objects: %v", err) c.JSON(500, gin.H{ "status": "error", "message": fmt.Sprintf("Failed to list objects: %s", err.Error()), }) } // 打印列举结果。默认情况下,一次返回100条记录。 for _, object := range lsRes.Objects { allObjects = append(allObjects, object.Key) } // 如果还有更多对象需要列举,则更新marker并继续循环。 if lsRes.IsTruncated { marker = lsRes.NextMarker } else { break } } log.Println("All objects have been listed.") c.JSON(200, gin.H{ "status": "success", "message": "All objects have been listed", "objects": allObjects, }) -
其他:
- 加载env配置
- 使用
os.Stat函数检查.env文件是否存在。如果文件不存在,os.IsNotExist(err)会返回true。 - 如果文件不存在,函数会创建一个包含默认配置信息的字符串
envContent。这些默认配置信息包括阿里云OSS的访问端点(endpoint)、访问密钥ID(access key ID)、访问密钥密码(access key secret)以及存储桶名称(bucket name)。 - 使用
os.WriteFile函数创建新的.env文件,并将envContent写入该文件。0644是文件的权限模式,表示文件所有者有读写权限,而其他用户只有读权限。 - 如果文件创建成功,函数会打印一条信息,提示
.env文件已创建并包含了默认值。 - 如果文件已经存在,函数会打印一条信息,提示
.env文件已经存在。
func createEnvFileIfNotExist() {
// 检查 .env 文件是否存在
if _, err := os.Stat(".env"); os.IsNotExist(err) {
// 如果文件不存在,创建并写入默认值
envContent := `OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
OSS_ACCESS_KEY_ID=your-access-key-id
OSS_ACCESS_KEY_SECRET=your-access-key-secret
OSS_BUCKET_NAME=your-bucket-name`
// 创建文件
err := os.WriteFile(".env", []byte(envContent), 0644)
if err != nil {
log.Fatalf("Failed to create .env file: %v", err)
}
fmt.Println(".env file created with default values.")
} else {
fmt.Println(".env file already exists.")
}
}
- 生成随机文件名:
- 使用
time.Now().UnixNano()作为随机数生成器的种子。这样可以确保每次调用函数时,生成的随机数序列都是不同的。 - 定义了一个字符集
charset,包含了大小写字母和数字。 - 生成一个指定长度(这里是10)的随机字符串。通过遍历文件名长度,每次从字符集中随机选择一个字符并添加到文件名切片
filename中。 - 获取当前时间戳
time.Now().Unix(),并将其转换为字符串。 - 将时间戳和随机字符串组合起来,生成最终的文件名。文件名的格式是
时间戳_随机字符串.扩展名。
func generateRandomFilename(ext string) string {
// 设置随机数种子为当前时间戳
rand.Seed(time.Now().UnixNano())
// 生成一个随机字符串
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
length := 10 // 文件名长度
var filename []byte
for i := 0; i < length; i++ {
filename = append(filename, charset[rand.Intn(len(charset))])
}
// 使用时间戳作为文件名的一部分
timestamp := time.Now().Unix()
// 组合随机字符串和时间戳来生成文件名
return fmt.Sprintf("%d_%s%s", timestamp, string(filename), ext)
}