s3cmd的addHeader功能Go实现

通过本篇文章将了解到: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对象也可以添加。两种机制需要比较。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容