Go實戰--golang資源管理七牛雲對象存儲(bucketManager)

生命不止,繼續 go go go !!!

之前學習了七牛與的golang SDK,我們主要介紹瞭如何通過golang上傳文件到七牛bucket:
Go實戰–golang上傳文件到七牛雲對象存儲(github.com/qiniu/api.v7)

今天,與大家一起學習bucket中資源管理。

bucket.go源碼

關於資源管理的方法都是位於bucket.go,大概五百多行的代碼,不算很長,這裏貼過來:

package storage

import (
    "context"
    "encoding/base64"
    "errors"
    "fmt"
    "net/url"
    "strconv"
    "strings"

    "github.com/qiniu/api.v7/auth/qbox"
    "github.com/qiniu/x/rpc.v7"
)

// 資源管理相關的默認域名
const (
    DefaultRsHost  = "rs.qiniu.com"
    DefaultRsfHost = "rsf.qiniu.com"
    DefaultAPIHost = "api.qiniu.com"
    DefaultPubHost = "pu.qbox.me:10200"
)

// FileInfo 文件基本信息
type FileInfo struct {
    Hash     string `json:"hash"`
    Fsize    int64  `json:"fsize"`
    PutTime  int64  `json:"putTime"`
    MimeType string `json:"mimeType"`
    Type     int    `json:"type"`
}

func (f *FileInfo) String() string {
    str := ""
    str += fmt.Sprintf("Hash:     %s\n", f.Hash)
    str += fmt.Sprintf("Fsize:    %d\n", f.Fsize)
    str += fmt.Sprintf("PutTime:  %d\n", f.PutTime)
    str += fmt.Sprintf("MimeType: %s\n", f.MimeType)
    str += fmt.Sprintf("Type:     %d\n", f.Type)
    return str
}

// FetchRet 資源抓取的返回值
type FetchRet struct {
    Hash     string `json:"hash"`
    Fsize    int64  `json:"fsize"`
    MimeType string `json:"mimeType"`
    Key      string `json:"key"`
}

func (r *FetchRet) String() string {
    str := ""
    str += fmt.Sprintf("Key:      %s\n", r.Key)
    str += fmt.Sprintf("Hash:     %s\n", r.Hash)
    str += fmt.Sprintf("Fsize:    %d\n", r.Fsize)
    str += fmt.Sprintf("MimeType: %s\n", r.MimeType)
    return str
}

// ListItem 爲文件列舉的返回值
type ListItem struct {
    Key      string `json:"key"`
    Hash     string `json:"hash"`
    Fsize    int64  `json:"fsize"`
    PutTime  int64  `json:"putTime"`
    MimeType string `json:"mimeType"`
    Type     int    `json:"type"`
    EndUser  string `json:"endUser"`
}

func (l *ListItem) String() string {
    str := ""
    str += fmt.Sprintf("Hash:     %s\n", l.Hash)
    str += fmt.Sprintf("Fsize:    %d\n", l.Fsize)
    str += fmt.Sprintf("PutTime:  %d\n", l.PutTime)
    str += fmt.Sprintf("MimeType: %s\n", l.MimeType)
    str += fmt.Sprintf("Type:     %d\n", l.Type)
    str += fmt.Sprintf("EndUser:  %s\n", l.EndUser)
    return str
}

// BatchOpRet 爲批量執行操作的返回值
// 批量操作支持 stat,copy,delete,move,chgm,chtype,deleteAfterDays幾個操作
// 其中 stat 爲獲取文件的基本信息,如果文件存在則返回基本信息,如果文件不存在返回 error 。
// 其他的操作,如果成功,則返回 code,不成功會同時返回 error 信息,可以根據 error 信息來判斷問題所在。
type BatchOpRet struct {
    Code int `json:"code,omitempty"`
    Data struct {
        Hash     string `json:"hash"`
        Fsize    int64  `json:"fsize"`
        PutTime  int64  `json:"putTime"`
        MimeType string `json:"mimeType"`
        Type     int    `json:"type"`
        Error    string `json:"error"`
    } `json:"data,omitempty"`
}

// BucketManager 提供了對資源進行管理的操作
type BucketManager struct {
    client *rpc.Client
    mac    *qbox.Mac
    cfg    *Config
}

// NewBucketManager 用來構建一個新的資源管理對象
func NewBucketManager(mac *qbox.Mac, cfg *Config) *BucketManager {
    if cfg == nil {
        cfg = &Config{}
    }

    return &BucketManager{
        client: NewClient(mac, nil),
        mac:    mac,
        cfg:    cfg,
    }
}

// NewBucketManagerEx 用來構建一個新的資源管理對象
func NewBucketManagerEx(mac *qbox.Mac, cfg *Config, client *rpc.Client) *BucketManager {
    if cfg == nil {
        cfg = &Config{}
    }

    if client == nil {
        client = NewClient(mac, nil)
    }

    return &BucketManager{
        client: client,
        mac:    mac,
        cfg:    cfg,
    }
}

// Buckets 用來獲取空間列表,如果指定了 shared 參數爲 true,那麼一同列表被授權訪問的空間
func (m *BucketManager) Buckets(shared bool) (buckets []string, err error) {
    ctx := context.TODO()
    var reqHost string

    scheme := "http://"
    if m.cfg.UseHTTPS {
        scheme = "https://"
    }

    reqHost = fmt.Sprintf("%s%s", scheme, DefaultRsHost)
    reqURL := fmt.Sprintf("%s/buckets?shared=%v", reqHost, shared)
    err = m.client.Call(ctx, &buckets, "POST", reqURL)
    return
}

// Stat 用來獲取一個文件的基本信息
func (m *BucketManager) Stat(bucket, key string) (info FileInfo, err error) {
    ctx := context.TODO()
    reqHost, reqErr := m.rsHost(bucket)
    if reqErr != nil {
        err = reqErr
        return
    }

    reqURL := fmt.Sprintf("%s%s", reqHost, URIStat(bucket, key))
    err = m.client.Call(ctx, &info, "POST", reqURL)
    return
}

// Delete 用來刪除空間中的一個文件
func (m *BucketManager) Delete(bucket, key string) (err error) {
    ctx := context.TODO()
    reqHost, reqErr := m.rsHost(bucket)
    if reqErr != nil {
        err = reqErr
        return
    }
    reqURL := fmt.Sprintf("%s%s", reqHost, URIDelete(bucket, key))
    err = m.client.Call(ctx, nil, "POST", reqURL)
    return
}

// Copy 用來創建已有空間中的文件的一個新的副本
func (m *BucketManager) Copy(srcBucket, srcKey, destBucket, destKey string, force bool) (err error) {
    ctx := context.TODO()
    reqHost, reqErr := m.rsHost(srcBucket)
    if reqErr != nil {
        err = reqErr
        return
    }

    reqURL := fmt.Sprintf("%s%s", reqHost, URICopy(srcBucket, srcKey, destBucket, destKey, force))
    err = m.client.Call(ctx, nil, "POST", reqURL)
    return
}

// Move 用來將空間中的一個文件移動到新的空間或者重命名
func (m *BucketManager) Move(srcBucket, srcKey, destBucket, destKey string, force bool) (err error) {
    ctx := context.TODO()
    reqHost, reqErr := m.rsHost(srcBucket)
    if reqErr != nil {
        err = reqErr
        return
    }

    reqURL := fmt.Sprintf("%s%s", reqHost, URIMove(srcBucket, srcKey, destBucket, destKey, force))
    err = m.client.Call(ctx, nil, "POST", reqURL)
    return
}

// ChangeMime 用來更新文件的MimeType
func (m *BucketManager) ChangeMime(bucket, key, newMime string) (err error) {
    ctx := context.TODO()
    reqHost, reqErr := m.rsHost(bucket)
    if reqErr != nil {
        err = reqErr
        return
    }
    reqURL := fmt.Sprintf("%s%s", reqHost, URIChangeMime(bucket, key, newMime))
    err = m.client.Call(ctx, nil, "POST", reqURL)
    return
}

// ChangeType 用來更新文件的存儲類型,0表示普通存儲,1表示低頻存儲
func (m *BucketManager) ChangeType(bucket, key string, fileType int) (err error) {
    ctx := context.TODO()
    reqHost, reqErr := m.rsHost(bucket)
    if reqErr != nil {
        err = reqErr
        return
    }
    reqURL := fmt.Sprintf("%s%s", reqHost, URIChangeType(bucket, key, fileType))
    err = m.client.Call(ctx, nil, "POST", reqURL)
    return
}

// DeleteAfterDays 用來更新文件生命週期,如果 days 設置爲0,則表示取消文件的定期刪除功能,永久存儲
func (m *BucketManager) DeleteAfterDays(bucket, key string, days int) (err error) {
    ctx := context.TODO()
    reqHost, reqErr := m.rsHost(bucket)
    if reqErr != nil {
        err = reqErr
        return
    }

    reqURL := fmt.Sprintf("%s%s", reqHost, URIDeleteAfterDays(bucket, key, days))
    err = m.client.Call(ctx, nil, "POST", reqURL)
    return
}

// Batch 接口提供了資源管理的批量操作,支持 stat,copy,move,delete,chgm,chtype,deleteAfterDays幾個接口
func (m *BucketManager) Batch(operations []string) (batchOpRet []BatchOpRet, err error) {
    if len(operations) > 1000 {
        err = errors.New("batch operation count exceeds the limit of 1000")
        return
    }
    ctx := context.TODO()
    scheme := "http://"
    if m.cfg.UseHTTPS {
        scheme = "https://"
    }
    reqURL := fmt.Sprintf("%s%s/batch", scheme, DefaultRsHost)
    params := map[string][]string{
        "op": operations,
    }
    err = m.client.CallWithForm(ctx, &batchOpRet, "POST", reqURL, params)
    return
}

// Fetch 根據提供的遠程資源鏈接來抓取一個文件到空間並已指定文件名保存
func (m *BucketManager) Fetch(resURL, bucket, key string) (fetchRet FetchRet, err error) {
    ctx := context.TODO()
    reqHost, reqErr := m.iovipHost(bucket)
    if reqErr != nil {
        err = reqErr
        return
    }
    reqURL := fmt.Sprintf("%s%s", reqHost, uriFetch(resURL, bucket, key))
    err = m.client.Call(ctx, &fetchRet, "POST", reqURL)
    return
}

// FetchWithoutKey 根據提供的遠程資源鏈接來抓取一個文件到空間並以文件的內容hash作爲文件名
func (m *BucketManager) FetchWithoutKey(resURL, bucket string) (fetchRet FetchRet, err error) {
    ctx := context.TODO()
    reqHost, reqErr := m.iovipHost(bucket)
    if reqErr != nil {
        err = reqErr
        return
    }
    reqURL := fmt.Sprintf("%s%s", reqHost, uriFetchWithoutKey(resURL, bucket))
    err = m.client.Call(ctx, &fetchRet, "POST", reqURL)
    return
}

// Prefetch 用來同步鏡像空間的資源和鏡像源資源內容
func (m *BucketManager) Prefetch(bucket, key string) (err error) {
    ctx := context.TODO()
    reqHost, reqErr := m.iovipHost(bucket)
    if reqErr != nil {
        err = reqErr
        return
    }
    reqURL := fmt.Sprintf("%s%s", reqHost, uriPrefetch(bucket, key))
    err = m.client.Call(ctx, nil, "POST", reqURL)
    return
}

// SetImage 用來設置空間鏡像源
func (m *BucketManager) SetImage(siteURL, bucket string) (err error) {
    ctx := context.TODO()
    reqURL := fmt.Sprintf("http://%s%s", DefaultPubHost, uriSetImage(siteURL, bucket))
    err = m.client.Call(ctx, nil, "POST", reqURL)
    return
}

// SetImageWithHost 用來設置空間鏡像源,額外添加回源Host頭部
func (m *BucketManager) SetImageWithHost(siteURL, bucket, host string) (err error) {
    ctx := context.TODO()
    reqURL := fmt.Sprintf("http://%s%s", DefaultPubHost,
        uriSetImageWithHost(siteURL, bucket, host))
    err = m.client.Call(ctx, nil, "POST", reqURL)
    return
}

// UnsetImage 用來取消空間鏡像源設置
func (m *BucketManager) UnsetImage(bucket string) (err error) {
    ctx := context.TODO()
    reqURL := fmt.Sprintf("http://%s%s", DefaultPubHost, uriUnsetImage(bucket))
    err = m.client.Call(ctx, nil, "POST", reqURL)
    return err
}

type listFilesRet struct {
    Marker         string     `json:"marker"`
    Items          []ListItem `json:"items"`
    CommonPrefixes []string   `json:"commonPrefixes"`
}

// ListFiles 用來獲取空間文件列表,可以根據需要指定文件的前綴 prefix,文件的目錄 delimiter,循環列舉的時候下次
// 列舉的位置 marker,以及每次返回的文件的最大數量limit,其中limit最大爲1000。
func (m *BucketManager) ListFiles(bucket, prefix, delimiter, marker string,
    limit int) (entries []ListItem, commonPrefixes []string, nextMarker string, hasNext bool, err error) {
    if limit <= 0 || limit > 1000 {
        err = errors.New("invalid list limit, only allow [1, 1000]")
        return
    }

    ctx := context.TODO()
    reqHost, reqErr := m.rsfHost(bucket)
    if reqErr != nil {
        err = reqErr
        return
    }

    ret := listFilesRet{}
    reqURL := fmt.Sprintf("%s%s", reqHost, uriListFiles(bucket, prefix, delimiter, marker, limit))
    err = m.client.Call(ctx, &ret, "POST", reqURL)
    if err != nil {
        return
    }

    commonPrefixes = ret.CommonPrefixes
    nextMarker = ret.Marker
    entries = ret.Items
    if ret.Marker != "" {
        hasNext = true
    }

    return
}

func (m *BucketManager) rsHost(bucket string) (rsHost string, err error) {
    var zone *Zone
    if m.cfg.Zone != nil {
        zone = m.cfg.Zone
    } else {
        if v, zoneErr := GetZone(m.mac.AccessKey, bucket); zoneErr != nil {
            err = zoneErr
            return
        } else {
            zone = v
        }
    }

    scheme := "http://"
    if m.cfg.UseHTTPS {
        scheme = "https://"
    }

    rsHost = fmt.Sprintf("%s%s", scheme, zone.RsHost)
    return
}

func (m *BucketManager) rsfHost(bucket string) (rsfHost string, err error) {
    var zone *Zone
    if m.cfg.Zone != nil {
        zone = m.cfg.Zone
    } else {
        if v, zoneErr := GetZone(m.mac.AccessKey, bucket); zoneErr != nil {
            err = zoneErr
            return
        } else {
            zone = v
        }
    }

    scheme := "http://"
    if m.cfg.UseHTTPS {
        scheme = "https://"
    }

    rsfHost = fmt.Sprintf("%s%s", scheme, zone.RsfHost)
    return
}

func (m *BucketManager) iovipHost(bucket string) (iovipHost string, err error) {
    var zone *Zone
    if m.cfg.Zone != nil {
        zone = m.cfg.Zone
    } else {
        if v, zoneErr := GetZone(m.mac.AccessKey, bucket); zoneErr != nil {
            err = zoneErr
            return
        } else {
            zone = v
        }
    }

    scheme := "http://"
    if m.cfg.UseHTTPS {
        scheme = "https://"
    }

    iovipHost = fmt.Sprintf("%s%s", scheme, zone.IovipHost)
    return
}

// 構建op的方法,導出的方法支持在Batch操作中使用

// URIStat 構建 stat 接口的請求命令
func URIStat(bucket, key string) string {
    return fmt.Sprintf("/stat/%s", EncodedEntry(bucket, key))
}

// URIDelete 構建 delete 接口的請求命令
func URIDelete(bucket, key string) string {
    return fmt.Sprintf("/delete/%s", EncodedEntry(bucket, key))
}

// URICopy 構建 copy 接口的請求命令
func URICopy(srcBucket, srcKey, destBucket, destKey string, force bool) string {
    return fmt.Sprintf("/copy/%s/%s/force/%v", EncodedEntry(srcBucket, srcKey),
        EncodedEntry(destBucket, destKey), force)
}

// URIMove 構建 move 接口的請求命令
func URIMove(srcBucket, srcKey, destBucket, destKey string, force bool) string {
    return fmt.Sprintf("/move/%s/%s/force/%v", EncodedEntry(srcBucket, srcKey),
        EncodedEntry(destBucket, destKey), force)
}

// URIDeleteAfterDays 構建 deleteAfterDays 接口的請求命令
func URIDeleteAfterDays(bucket, key string, days int) string {
    return fmt.Sprintf("/deleteAfterDays/%s/%d", EncodedEntry(bucket, key), days)
}

// URIChangeMime 構建 chgm 接口的請求命令
func URIChangeMime(bucket, key, newMime string) string {
    return fmt.Sprintf("/chgm/%s/mime/%s", EncodedEntry(bucket, key),
        base64.URLEncoding.EncodeToString([]byte(newMime)))
}

// URIChangeType 構建 chtype 接口的請求命令
func URIChangeType(bucket, key string, fileType int) string {
    return fmt.Sprintf("/chtype/%s/type/%d", EncodedEntry(bucket, key), fileType)
}

// 構建op的方法,非導出的方法無法用在Batch操作中
func uriFetch(resURL, bucket, key string) string {
    return fmt.Sprintf("/fetch/%s/to/%s",
        base64.URLEncoding.EncodeToString([]byte(resURL)), EncodedEntry(bucket, key))
}

func uriFetchWithoutKey(resURL, bucket string) string {
    return fmt.Sprintf("/fetch/%s/to/%s",
        base64.URLEncoding.EncodeToString([]byte(resURL)), EncodedEntryWithoutKey(bucket))
}

func uriPrefetch(bucket, key string) string {
    return fmt.Sprintf("/prefetch/%s", EncodedEntry(bucket, key))
}

func uriSetImage(siteURL, bucket string) string {
    return fmt.Sprintf("/image/%s/from/%s", bucket,
        base64.URLEncoding.EncodeToString([]byte(siteURL)))
}

func uriSetImageWithHost(siteURL, bucket, host string) string {
    return fmt.Sprintf("/image/%s/from/%s/host/%s", bucket,
        base64.URLEncoding.EncodeToString([]byte(siteURL)),
        base64.URLEncoding.EncodeToString([]byte(host)))
}

func uriUnsetImage(bucket string) string {
    return fmt.Sprintf("/unimage/%s", bucket)
}

func uriListFiles(bucket, prefix, delimiter, marker string, limit int) string {
    query := make(url.Values)
    query.Add("bucket", bucket)
    if prefix != "" {
        query.Add("prefix", prefix)
    }
    if delimiter != "" {
        query.Add("delimiter", delimiter)
    }
    if marker != "" {
        query.Add("marker", marker)
    }
    if limit > 0 {
        query.Add("limit", strconv.FormatInt(int64(limit), 10))
    }
    return fmt.Sprintf("/list?%s", query.Encode())
}

// EncodedEntry 生成URL Safe Base64編碼的 Entry
func EncodedEntry(bucket, key string) string {
    entry := fmt.Sprintf("%s:%s", bucket, key)
    return base64.URLEncoding.EncodeToString([]byte(entry))
}

// EncodedEntryWithoutKey 生成 key 爲null的情況下 URL Safe Base64編碼的Entry
func EncodedEntryWithoutKey(bucket string) string {
    return base64.URLEncoding.EncodeToString([]byte(bucket))
}

// MakePublicURL 用來生成公開空間資源下載鏈接
func MakePublicURL(domain, key string) (finalUrl string) {
    srcUrl := fmt.Sprintf("%s/%s", domain, key)
    srcUri, _ := url.Parse(srcUrl)
    finalUrl = srcUri.String()
    return
}

// MakePrivateURL 用來生成私有空間資源下載鏈接
func MakePrivateURL(mac *qbox.Mac, domain, key string, deadline int64) (privateURL string) {
    publicURL := MakePublicURL(domain, key)
    urlToSign := publicURL
    if strings.Contains(publicURL, "?") {
        urlToSign = fmt.Sprintf("%s&e=%d", urlToSign, deadline)
    } else {
        urlToSign = fmt.Sprintf("%s?e=%d", urlToSign, deadline)
    }
    token := mac.Sign([]byte(urlToSign))
    privateURL = fmt.Sprintf("%s&token=%s", urlToSign, token)
    return
}

資源管理–實戰

獲取文件類型
更改文件類型
更改文件mime
重命名文件
創建文件副本
刪除文件

package main

import (
    "fmt"

    "github.com/qiniu/api.v7/auth/qbox"
    "github.com/qiniu/api.v7/storage"
)

func main() {
    accessKey := "TgVGKnpCMLDI6hSS4rSWE3g-FZjMPf6ZbcX0Kd7c"
    secretKey := "zqZvH3fNVaggw00oc9wCrcWKgeeiV7WITFTFds7H"
    bucket := "wangshubotest"
    key := "2.log"

    mac := qbox.NewMac(accessKey, secretKey)

    cfg := storage.Config{}
    cfg.Zone = &storage.ZoneHuadong
    cfg.UseHTTPS = false
    cfg.UseCdnDomains = false

    bucketManager := storage.NewBucketManager(mac, &cfg)

    // Get file info
    fmt.Println("------Get file info------")
    fileInfo, sErr := bucketManager.Stat(bucket, key)
    if sErr != nil {
        fmt.Println(sErr)
        return
    }
    fmt.Println(fileInfo.String())
    fmt.Println(storage.ParsePutTime(fileInfo.PutTime))

    // Change the mime of the file
    fmt.Println("------Change the mime of the file------")
    newMime := "image/x-png"
    err := bucketManager.ChangeMime(bucket, key, newMime)
    if err != nil {
        fmt.Println(err)
        return
    }
    fileInfo, sErr = bucketManager.Stat(bucket, key)
    if sErr != nil {
        fmt.Println(sErr)
        return
    }
    fmt.Println(fileInfo.String())
    fmt.Println(storage.ParsePutTime(fileInfo.PutTime))

    // Change filetype
    fmt.Println("------Change filetype------")
    fileType := 1
    err = bucketManager.ChangeType(bucket, key, fileType)
    if err != nil {
        fmt.Println(err)
        return
    }
    fileInfo, sErr = bucketManager.Stat(bucket, key)
    if sErr != nil {
        fmt.Println(sErr)
        return
    }
    fmt.Println(fileInfo.String())
    fmt.Println(storage.ParsePutTime(fileInfo.PutTime))

    //Copy file
    fmt.Println("------Copy file------")
    destBucket := bucket
    destKey := "2_copy.log"
    force := false
    err = bucketManager.Copy(bucket, key, destBucket, destKey, force)
    if err != nil {
        fmt.Println(err)
        return
    }
    fileInfo, sErr = bucketManager.Stat(bucket, destKey)
    if sErr != nil {
        fmt.Println(sErr)
        return
    }
    fmt.Println(fileInfo.String())
    fmt.Println(storage.ParsePutTime(fileInfo.PutTime))

    //Rename file
    fmt.Println("------Rename file------")
    destKey = "2_new.log"
    force = false
    err = bucketManager.Move(bucket, key, destBucket, destKey, force)
    if err != nil {
        fmt.Println(err)
        return
    }
    fileInfo, sErr = bucketManager.Stat(bucket, destKey)
    if sErr != nil {
        fmt.Println(sErr)
        return
    }
    fmt.Println(fileInfo.String())
    fmt.Println(storage.ParsePutTime(fileInfo.PutTime))

    // Delete file
    fmt.Println("------Delete file------")
    err = bucketManager.Delete(bucket, key)
    if err != nil {
        fmt.Println(err)
        return
    }
    fileInfo, sErr = bucketManager.Stat(bucket, key)
    if sErr != nil {
        fmt.Println(sErr)
        return
    }
    fmt.Println(fileInfo.String())
    fmt.Println(storage.ParsePutTime(fileInfo.PutTime))
}

輸出:

------Get file info------
Hash:     Fh3tNUEoiaj-qkNcP915nRuiAm4-
Fsize:    20
PutTime:  15083847282270721
MimeType: text/plain
Type:     0

2017-10-19 11:45:28.2270721 +0800 CST
------Change the mime of the file------
Hash:     Fh3tNUEoiaj-qkNcP915nRuiAm4-
Fsize:    20
PutTime:  15083847282270721
MimeType: image/x-png
Type:     0

2017-10-19 11:45:28.2270721 +0800 CST
------Change filetype------
Hash:     Fh3tNUEoiaj-qkNcP915nRuiAm4-
Fsize:    20
PutTime:  15083847282270721
MimeType: image/x-png
Type:     1

2017-10-19 11:45:28.2270721 +0800 CST
------Copy file------
Hash:     Fh3tNUEoiaj-qkNcP915nRuiAm4-
Fsize:    20
PutTime:  15083847373416725
MimeType: image/x-png
Type:     1

2017-10-19 11:45:37.3416725 +0800 CST
------Rename file------
Hash:     Fh3tNUEoiaj-qkNcP915nRuiAm4-
Fsize:    20
PutTime:  15083847376868294
MimeType: image/x-png
Type:     1

2017-10-19 11:45:37.6868294 +0800 CST
------Delete file------
no such file or directory

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章