美文网首页
s3cmd的addHeader功能Go实现

s3cmd的addHeader功能Go实现

作者: 要厉害的 | 来源:发表于2021-08-05 12:22 被阅读0次

    通过本篇文章将了解到:1、利用go语言实现s3cmd中addHeader的流程;2、利用aws-sdk-go和http库实现ceph HEAD和PUT请求。

    s3cmd

    s3cmd是一个操作对象存储的软件,使用python开发。其功能包括常见的S3操作,比如桶的创建、删除,对象的增删改查等操作。实验使用的是Ceph RGW提供S3服务,s3cmd同样可以适配使用。

    在CentOS 7上使用如下命令安装:

    yum install s3cmd 
    

    安装后使用s3cmd --configure 或者直接修改配置文件 vim ~/.s3cfg。完成access_key、secret_key、host_base、host_bucket、use_https等字段的配置可以访问Ceph RGW对象存储。

    s3cmd的modify命令可以通过--add-header选项给对象添加而外的头部信息。语法如下:

    s3cmd modify s3://{桶名}/{对象名}  --add-header={key}:{value}
    

    需求

    最近在项目上需要用到给对象数据添加自定义的元数据。目前Ceph官方支持的形式x-amz-meta-{key}:{value} 形式,称为Canonicalized Header。

    通过该方式可以给对象添加额外自定义的元数据,并且如果配置了复制zone,把复制zone和ElasticSearch的后端绑定,该自定义元数据可以在Elasticsearch中形成索引条目(亲测有效)。同时支持查询语句,在不暴露ElasticSearch服务的情况下,通过RGW网关高效检索。

    使用s3cmd可以添加自定义的元数据,命令如下。这里添加了一个key为city,value为Hangzhou的字段。

    s3cmd modify s3://{桶名}/{对象名}  --add-header=x-amz-meta-city:Hangzhou
    

    现在项目组用Go语言对接,为了添加自定义元数据,一种方式可以直接在程序里面调用命令,但这要求运行主机上已配置好s3cmd命令。另外,是否有Go语言库可以支持,可以利用Go的库支持该功能。查看ceph-go和aws-sdk-go两个项目没有直接的提供类似的功能。需要实现一个类似的功能。

    addHeader实现流程

    借鉴s3cmd的思路(阅读S3.py内object_modify/object_copy等函数代码)。

    1、通过HEAD接口获得当前对象的头部信息(只获得头部信息即可,因为不会去修改对象的数据);

    2、修改HEAD头部信息,并添加对应的自定义字段;

    3、结合PUT接口和x-amz-copy-src选项(即拷贝对象接口),复制对象并上传新的header信息。

    为什么这里要使用x-amz-copy-src?如果不使用,新的对象会没有数据;或者就需要在步骤1中使用GET接口获取完整对象至本地,再上传,造成资源的消耗。x-amz-copy-src是在服务端完成的。

    4、需要说明的是HEAD的作为源对象,PUT操作的目的对象是同一个对象。

    GO语言实现

    该功能的Go实现基于:
    1、利用ceph本身RESTful的接口;
    2、利用http包进行http消息的发送和接收;
    3、利用aws-sdk-go中的签名对消息进行签署。

    具体步骤

    首先利用HEAD接口仅仅获得对象的头信息,Ceph提供了HEAD的接口。

    HEAD /{bucket}/{object} HTTP/1.1
    

    Go语言的实现需要用到aws-sdk-go当中的签名函数。因为Ceph兼容S3接口,认证的方式也是和AWS S3一样,需要在发送请求前先给请求签名。accessKey和secretKey是S3创建用户时设定的相应值。

    cred := credentials.NewStaticCredentials(accessKey, secretKey, "")
    signer := v4.NewSigner(cred)
    _, err = signer.Sign(request, nil, service, authRegion, time.Now())
    

    发送请求

    resp, err := httpClient.Do(request)
    

    获得头部信息之后需要进行修改,否则在签名的时候会出现认证失败的问题。删除原先的头部信息会在请求发送的时候根据实际情况自动添加。

    h2 = resp.Header.Clone()
    
    to_remove := []string{"Date", "Content-Length", 
                          "Content-Md5", "Accept-Ranges", 
                          "X-Amz-Request-Id", "Last-Modified", 
                          "ETag", "Connection", "Server", 
                          "X-Amz-Version-Id", "X-Amz-Delete-Maker"}
    
    for _, k := range to_remove {
       h2.Del(k) 
    }
    

    设置复制的方式,复制的方式有两种一种是COPY,一种是REPLACE。使用COPY则新添加的元数据不会有效,使用REPLACE则需要将原先元数据保留、修改、添加新的自定义元数据。这里指定REPLACE。

    request.Header.Add("X-Amz-Metadata-Directive", "REPLACE")
    

    使用Set方法设置元数据。

    request.Header.Set("X-Amz-Meta-City", "Hangzhou")
    

    再发送PUT请求和x-amz-source-copy参数复制一份对象。Ceph同样提供了复制对象的接口。

    PUT /{dest-bucket}/{dest-object} HTTP/1.1
    x-amz-copy-source: {source-bucket}/{source-object}
    

    指定复制源

    request.Header.Add("X-Amz-Copy-Source", srcAddr)
    

    这样就不需要将数据GET请求保留在本地,然后再上传,节省部分资源开销。

    具体代码

    整体代码如下

    package main
    
    import (
    
        "net/http"
    
        "net/http/httputil"
    
        "fmt"
    
        "context"
    
        "time"
    
        "github.com/aws/aws-sdk-go/aws/credentials"
    
        v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
    
    )
    
    
    
    const (
    
        authRegion = "default"
    
        service = "s3"
    
        connectionTimeout = time.Second * 3
    
        bucketName = "cephgo"
    
        endPoint = "http://192.168.99.103:8080"
    
        accessKey = "654321"
    
        secretKey = "654321"
    
        srcFile = "file3"
    
        destFile = "file3"
    
    )
    
    
    
    func buildUrlPath(host, bucket, file string) string {
    
        return fmt.Sprintf("%s/%s/%s", host, bucket, file)
    
    }
    
    
    
    func main() {
       
        //build Head req
        httpMethod := http.MethodHead
    
        urlPath := buildUrlPath(endPoint, bucketName, srcFile) 
    
        httpClient := &http.Client{Timeout: connectionTimeout}
    
        request, err := http.NewRequestWithContext(context.Background(), httpMethod, urlPath ,nil)
    
        if err != nil {
    
            fmt.Println(err)
    
          return
    
        }
        
        //sign Head req
    
        cred := credentials.NewStaticCredentials(accessKey, secretKey, "")
    
        signer := v4.NewSigner(cred)
    
        _, err = signer.Sign(request, nil, service, authRegion, time.Now())
    
        if err != nil {
    
            fmt.Println(err)
    
          return
    
        }
    
        //send HEAD Quest
    
        resp, err := httpClient.Do(request)
    
        if err != nil {
    
            fmt.Println(err)
    
          return
    
        }
    
        //copy header 
    
        var h2 http.Header
    
        h2 = resp.Header.Clone()
    
        to_remove := []string{"Date", "Content-Length", 
    
                                     "Content-Md5", "Accept-Ranges", 
    
                                     "X-Amz-Request-Id", "Last-Modified", 
    
                                     "ETag", "Connection", "Server", 
    
                                     "X-Amz-Version-Id", "X-Amz-Delete-Maker"}
    
        for _, k := range to_remove {
    
           h2.Del(k) 
    
        }
    
        //build PUT req
    
        httpMethod = http.MethodPut
    
        urlPath =  buildUrlPath(endPoint, bucketName, destFile) 
    
        request, err = http.NewRequestWithContext(context.Background(), httpMethod, urlPath ,nil)
    
        if err != nil {
    
            fmt.Println(err)
    
          return
    
        }
    
        //edit header    
    
        srcAddr := "/" + bucketName + "/" + srcFile
    
        request.Header = h2 
    
        request.Header.Add("X-Amz-Copy-Source", srcAddr)
    
        request.Header.Add("X-Amz-Metadata-Directive", "REPLACE")
    
        request.Header.Set("X-Amz-Meta-City", "Hangzhou")
    
        //sign PUT req
    
        cred = credentials.NewStaticCredentials(accessKey, secretKey, "")
    
        signer = v4.NewSigner(cred)
    
        _, err = signer.Sign(request, nil, service, authRegion, time.Now())
    
        if err != nil {
    
            fmt.Println(err)
    
          return
    
        }
    
        requestDump, err := httputil.DumpRequest(request, true)
    
        if err != nil {
    
            fmt.Println(err)
    
          return
    
        }
    
        fmt.Println(string(requestDump))
    
        //send PUT Quest
    
        resp, err = httpClient.Do(request)
    
        if err != nil {
    
            fmt.Println(err)
    
          return
        }
        responseDump, err := httputil.DumpResponse(resp, true)
    
        if err != nil {
    
            fmt.Println(err)
    
          return
    
        }
    
        fmt.Println(string(responseDump))
    }
    

    执行效果

    执行前对象无自定义元数据信息


    添加自定义元数据前.png

    执行后对象存在自定义元数据信息,且数据完整。

    添加自定义元数据后.png

    写到最后

    1、如何删除自定义的元数据?
    s3cmd的modify命令可以通过--remove-header选项给对象添加而外的头部信息。语法如下

    s3cmd modify s3://{桶名}/{对象名} --remove-header={key}
    

    依据之前的代码,用go可以实现,即利用http中Header的Del功能。添加如下语句即可。

    request.Header.Del(key)
    

    2、文章里描述了大体的流程,没有考虑分段上传等情况,距离实际封装成接口还有一些工作。
    3、实际上S3本身支持Tag方式,Ceph对象也可以添加。两种机制需要比较。

    相关文章

      网友评论

          本文标题:s3cmd的addHeader功能Go实现

          本文链接:https://www.haomeiwen.com/subject/ymdmvltx.html