oss对象存储 go操作

252 阅读10分钟

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)
}

主要功能

  1. 环境变量管理

    • 自动创建 .env 文件以存储 OSS 配置信息。
    • 加载环境变量以初始化 OSS 客户端。
  2. 基本路由

    1. GET /:返回简单的 JSON 消息。
    2. 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),
    		})
    	}
    })
    
    1. 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):在日志中记录文件下载成功的信息。
  3. 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,
    	})
    })
    
    1. 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"})
    })
    
    1. 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),
    	})
    })
    
    1. 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,
    	})
    
    
  4. 其他

  • 加载env配置
  1. 使用 os.Stat 函数检查 .env 文件是否存在。如果文件不存在,os.IsNotExist(err) 会返回 true
  2. 如果文件不存在,函数会创建一个包含默认配置信息的字符串 envContent。这些默认配置信息包括阿里云OSS的访问端点(endpoint)、访问密钥ID(access key ID)、访问密钥密码(access key secret)以及存储桶名称(bucket name)。
  3. 使用 os.WriteFile 函数创建新的 .env 文件,并将 envContent 写入该文件。0644 是文件的权限模式,表示文件所有者有读写权限,而其他用户只有读权限。
  4. 如果文件创建成功,函数会打印一条信息,提示 .env 文件已创建并包含了默认值。
  5. 如果文件已经存在,函数会打印一条信息,提示 .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.")
	}
}
  • 生成随机文件名:
  1. 使用 time.Now().UnixNano() 作为随机数生成器的种子。这样可以确保每次调用函数时,生成的随机数序列都是不同的。
  2. 定义了一个字符集 charset,包含了大小写字母和数字。
  3. 生成一个指定长度(这里是10)的随机字符串。通过遍历文件名长度,每次从字符集中随机选择一个字符并添加到文件名切片 filename 中。
  4. 获取当前时间戳 time.Now().Unix(),并将其转换为字符串。
  5. 将时间戳和随机字符串组合起来,生成最终的文件名。文件名的格式是 时间戳_随机字符串.扩展名
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)
}