github.com/keysonZZZ/kmg@v0.0.0-20151121023212-05317bfd7d39/third/upyun/upyun.go (about)

     1  package upyun
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net"
    10  	"net/http"
    11  	"net/http/httputil"
    12  	"os"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  )
    17  
    18  type UpYun struct {
    19  	httpClient *http.Client
    20  	trans      *http.Transport
    21  	bucketName string
    22  	userName   string
    23  	passWord   string
    24  	apiDomain  string
    25  	contentMd5 string
    26  	fileSecret string
    27  	tmpHeaders map[string]string
    28  
    29  	TimeOut int
    30  	Debug   bool
    31  }
    32  
    33  /**
    34   * 初始化 UpYun 存储接口
    35   * @param bucketName 空间名称
    36   * @param userName 操作员名称
    37   * @param passWord 密码
    38   * return UpYun object
    39   */
    40  func NewUpYun(bucketName, userName, passWord string) *UpYun {
    41  	u := new(UpYun)
    42  	u.TimeOut = 300
    43  	u.httpClient = &http.Client{}
    44  	u.httpClient.Transport = &http.Transport{
    45  		Dial: timeoutDialer(u.TimeOut),
    46  	}
    47  	u.bucketName = bucketName
    48  	u.userName = userName
    49  	u.passWord = StringMd5(passWord)
    50  	u.apiDomain = "v0.api.upyun.com"
    51  	u.Debug = false
    52  	return u
    53  }
    54  
    55  func (u *UpYun) Version() string {
    56  	return "1.0.1"
    57  }
    58  
    59  /**
    60  * 切换 API 接口的域名
    61  * @param domain {
    62  默认 v0.api.upyun.com 自动识别,
    63      v1.api.upyun.com 电信,
    64      v2.api.upyun.com 联通,
    65      v3.api.upyun.com 移动
    66  }
    67  * return 无
    68  */
    69  func (u *UpYun) SetApiDomain(domain string) {
    70  	u.apiDomain = domain
    71  }
    72  
    73  /**
    74   * 设置连接超时时间
    75   * @param time 秒
    76   * return 无
    77   */
    78  func (u *UpYun) SetTimeout(time int) {
    79  	u.TimeOut = time
    80  	u.httpClient.Transport = &http.Transport{Dial: timeoutDialer(u.TimeOut)}
    81  }
    82  
    83  /**
    84   * 设置待上传文件的 Content-MD5 值(如又拍云服务端收到的文件MD5值与用户设置的不一致,
    85   * 将回报 406 Not Acceptable 错误)
    86   * @param str (文件 MD5 校验码)
    87   * return 无
    88   */
    89  func (u *UpYun) SetContentMD5(str string) {
    90  	u.contentMd5 = str
    91  }
    92  
    93  /**
    94   * 连接签名方法
    95   * @param method 请求方式 {GET, POST, PUT, DELETE}
    96   * return 签名字符串
    97   */
    98  func (u *UpYun) sign(method, uri, date string, length int64) string {
    99  	var bufSign bytes.Buffer
   100  	bufSign.WriteString(method)
   101  	bufSign.WriteString("&")
   102  	bufSign.WriteString(uri)
   103  	bufSign.WriteString("&")
   104  	bufSign.WriteString(date)
   105  	bufSign.WriteString("&")
   106  	bufSign.WriteString(strconv.FormatInt(length, 10))
   107  	bufSign.WriteString("&")
   108  	bufSign.WriteString(u.passWord)
   109  
   110  	var buf bytes.Buffer
   111  	buf.WriteString("UpYun ")
   112  	buf.WriteString(u.userName)
   113  	buf.WriteString(":")
   114  	buf.WriteString(StringMd5(bufSign.String()))
   115  	return buf.String()
   116  }
   117  
   118  /**
   119   * 连接处理逻辑
   120   * @param method 请求方式 {GET, POST, PUT, DELETE}
   121   * @param uri 请求地址
   122   * @param inFile 如果是POST上传文件,传递文件IO数据流
   123   * @param outFile 如果是GET下载文件,可传递文件IO数据流,这种情况函数也返回""
   124   * return 请求返回字符串,失败返回""(打开debug状态下遇到错误将中止程序执行)
   125   */
   126  func (u *UpYun) httpAction(method, uri string, headers map[string]string,
   127  	inFile, outFile *os.File) (string, error) {
   128  	uri = "/" + u.bucketName + uri
   129  	url := "http://" + u.apiDomain + uri
   130  	req, err := http.NewRequest(method, url, nil)
   131  	if err != nil {
   132  		if u.Debug {
   133  			fmt.Println("%v", err)
   134  			panic("http.NewRequest failed: " + err.Error())
   135  		}
   136  		return "", err
   137  	}
   138  
   139  	for k, v := range headers {
   140  		req.Header.Add(k, v)
   141  	}
   142  
   143  	length := FileSize(inFile)
   144  	if u.Debug {
   145  		fmt.Println("inFileSize: ", length)
   146  	}
   147  
   148  	if method == "PUT" || method == "POST" {
   149  		method = "POST"
   150  		if inFile != nil {
   151  			md5, err := IoMd5(inFile)
   152  			if err != nil {
   153  				if u.Debug {
   154  					panic("IoMd5 failed: " + err.Error())
   155  				}
   156  				return "", err
   157  			}
   158  			_, err = inFile.Seek(0, 0)
   159  			if err != nil {
   160  				if u.Debug {
   161  					panic("inFile.Seek failed: " + err.Error())
   162  				}
   163  				return "", err
   164  			}
   165  			req.Header.Add("Content-MD5", md5)
   166  
   167  			if u.fileSecret != "" {
   168  				req.Header.Add("Content-Secret", u.fileSecret)
   169  				u.fileSecret = ""
   170  			}
   171  			req.Header.Add("Content-Length", strconv.FormatInt(length, 10))
   172  			req.Body = inFile
   173  			req.ContentLength = length
   174  		}
   175  	}
   176  	req.Method = method
   177  
   178  	date := time.Now().UTC().Format(time.RFC1123)
   179  	req.Header.Add("Date", date)
   180  	req.Header.Add("Authorization", u.sign(method, uri, date, length))
   181  	if method == "HEAD" {
   182  		req.Body = nil
   183  	}
   184  
   185  	if u.Debug {
   186  		s, err := httputil.DumpRequestOut(req, true)
   187  		if err != nil {
   188  			panic("httputil.DumpRequestOut failed: " + err.Error())
   189  		}
   190  		fmt.Println(string(s))
   191  	}
   192  	var resp *http.Response
   193  	for i := 0; i < 2; i++ {
   194  		resp, err = u.httpClient.Do(req)
   195  		if err == nil {
   196  			break
   197  		}
   198  		u.SetTimeout(u.TimeOut)
   199  	}
   200  	if err != nil {
   201  		if u.Debug {
   202  			panic("httpClient.Do failed: " + err.Error())
   203  		}
   204  		return "", err
   205  	}
   206  
   207  	defer func() {
   208  		resp.Body.Close()
   209  	}()
   210  
   211  	rc := resp.StatusCode
   212  	if rc == 200 {
   213  		u.tmpHeaders = make(map[string]string)
   214  		for k, v := range resp.Header {
   215  			if strings.Contains(k, "X-Upyun") {
   216  				u.tmpHeaders[k] = v[0]
   217  			}
   218  		}
   219  
   220  		if method == "GET" && outFile != nil {
   221  			_, err := io.Copy(outFile, resp.Body)
   222  			if err != nil {
   223  				if u.Debug {
   224  					fmt.Printf("%v %v\n", rc, err)
   225  					panic("write output file failed: ")
   226  				}
   227  				return "", err
   228  			}
   229  			return "", nil
   230  		}
   231  
   232  		buf := bytes.NewBuffer(make([]byte, 0, 8192))
   233  		buf.ReadFrom(resp.Body)
   234  		return buf.String(), nil
   235  	}
   236  
   237  	return "", errors.New(resp.Status)
   238  }
   239  
   240  /**
   241   * 获取总体空间的占用信息
   242   * return 空间占用量,失败返回0.0
   243   */
   244  func (u *UpYun) GetBucketUsage() (float64, error) {
   245  	return u.GetFolderUsage("/")
   246  }
   247  
   248  /**
   249   * 获取某个子目录的占用信息
   250   * @param $path 目标路径
   251   * return 空间占用量和error,失败空间占用量返回0.0
   252   */
   253  func (u *UpYun) GetFolderUsage(path string) (float64, error) {
   254  	r, err := u.httpAction("GET", path+"?usage", nil, nil, nil)
   255  	if err != nil {
   256  		return 0.0, err
   257  	}
   258  	v, _ := strconv.ParseFloat(r, 64)
   259  	return v, nil
   260  }
   261  
   262  /**
   263   * 设置待上传文件的 访问密钥(注意:仅支持图片空!,设置密钥后,无法根据原文件URL直接访问,需带 URL 后面加上 (缩略图间隔标志符+密钥) 进行访问)
   264   * 如缩略图间隔标志符为 ! ,密钥为 bac,上传文件路径为 /folder/test.jpg ,那么该图片的对外访问地址为: http://空间域名/folder/test.jpg!bac
   265   * @param $str (文件 MD5 校验码)
   266   * return null;
   267   */
   268  func (u *UpYun) SetFileSecret(str string) {
   269  	u.fileSecret = str
   270  }
   271  
   272  /**
   273   * 上传文件
   274   * @param filePath 文件路径(包含文件名)
   275   * @param inFile 文件IO数据流
   276   * @param autoMkdir 是否自动创建父级目录(最深10级目录)
   277   * return error
   278   */
   279  func (u *UpYun) WriteFile(filePath string, inFile *os.File, autoMkdir bool) error {
   280  	var headers map[string]string
   281  	if autoMkdir {
   282  		headers = make(map[string]string)
   283  		headers["Mkdir"] = "true"
   284  	}
   285  	_, err := u.httpAction("PUT", filePath, headers, inFile, nil)
   286  	return err
   287  }
   288  
   289  /**
   290   * 获取上传文件后的信息(仅图片空间有返回数据)
   291   * @param key 信息字段名(x-upyun-width、x-upyun-height、x-upyun-frames、x-upyun-file-type)
   292   * return string or ""
   293   */
   294  func (u *UpYun) GetWritedFileInfo(key string) string {
   295  	if u.tmpHeaders == nil {
   296  		return ""
   297  	}
   298  	return u.tmpHeaders[strings.ToLower(key)]
   299  }
   300  
   301  /**
   302   * 读取文件
   303   * @param file 文件路径(包含文件名)
   304   * @param outFile 可传递文件IO数据流(结果返回true or false)
   305   * return error
   306   */
   307  func (u *UpYun) ReadFile(file string, outFile *os.File) error {
   308  	_, err := u.httpAction("GET", file, nil, nil, outFile)
   309  	return err
   310  }
   311  
   312  /**
   313   * 获取文件信息
   314   * @param file 文件路径(包含文件名)
   315   * return array('type': file | folder, 'size': file size, 'date': unix time) 或 nil
   316   */
   317  func (u *UpYun) GetFileInfo(file string) (m map[string]string, err error) {
   318  	_, err = u.httpAction("HEAD", file, nil, nil, nil)
   319  	if err != nil {
   320  		return
   321  	}
   322  	if u.tmpHeaders == nil {
   323  		return
   324  	}
   325  	m = make(map[string]string)
   326  	if v, ok := u.tmpHeaders["X-Upyun-File-Type"]; ok {
   327  		m["type"] = v
   328  	}
   329  	if v, ok := u.tmpHeaders["X-Upyun-File-Size"]; ok {
   330  		m["size"] = v
   331  	}
   332  	if v, ok := u.tmpHeaders["X-Upyun-File-Date"]; ok {
   333  		m["date"] = v
   334  	}
   335  	return
   336  }
   337  
   338  type DirInfo struct {
   339  	Name string
   340  	Type string
   341  	Size int64
   342  	Time int64
   343  }
   344  
   345  /**
   346   * 读取目录列表
   347   * @param path 目录路径
   348   * return DirInfo数组 或 nil
   349   */
   350  func (u *UpYun) ReadDir(path string) ([]*DirInfo, error) {
   351  	r, err := u.httpAction("GET", path, nil, nil, nil)
   352  	if err != nil {
   353  		return nil, err
   354  	}
   355  
   356  	dirs := make([]*DirInfo, 0, 8)
   357  	rs := strings.Split(r, "\n")
   358  	for i := 0; i < len(rs); i++ {
   359  		ri := strings.TrimSpace(rs[i])
   360  		if len(ri) == 0 {
   361  			continue
   362  		}
   363  		rid := strings.Split(ri, "\t")
   364  		d := new(DirInfo)
   365  		d.Name = rid[0]
   366  		if len(rid) > 3 && rid[3] != "" {
   367  			if rid[1] == "N" {
   368  				d.Type = "file"
   369  			} else {
   370  				d.Type = "folder"
   371  			}
   372  			d.Time, _ = strconv.ParseInt(rid[3], 10, 64)
   373  		}
   374  		if len(rid) > 2 {
   375  			d.Size, _ = strconv.ParseInt(rid[2], 10, 64)
   376  		}
   377  		dirs = append(dirs, d)
   378  	}
   379  	return dirs, nil
   380  }
   381  
   382  /**
   383   * 删除文件
   384   * @param file 文件路径(包含文件名)
   385   * return error
   386   */
   387  func (u *UpYun) DeleteFile(file string) error {
   388  	_, err := u.httpAction("DELETE", file, nil, nil, nil)
   389  	return err
   390  }
   391  
   392  /**
   393   * 创建目录
   394   * @param path 目录路径
   395   * @param auto_mkdir=false 是否自动创建父级目录
   396   * return error
   397   */
   398  func (u *UpYun) MkDir(path string, autoMkdir bool) error {
   399  	var headers map[string]string
   400  	headers = make(map[string]string)
   401  	headers["Folder"] = "true"
   402  	if autoMkdir {
   403  		headers["Mkdir"] = "true"
   404  	}
   405  	_, err := u.httpAction("PUT", path, headers, nil, nil)
   406  	return err
   407  }
   408  
   409  /**
   410   * 删除目录
   411   * @param path 目录路径
   412   * return error
   413   */
   414  func (u *UpYun) RmDir(dir string) error {
   415  	_, err := u.httpAction("DELETE", dir, nil, nil, nil)
   416  	return err
   417  }
   418  
   419  func FileSize(f *os.File) int64 {
   420  	if f == nil {
   421  		return 0
   422  	}
   423  	if fi, err := f.Stat(); err == nil {
   424  		return fi.Size()
   425  	}
   426  	return 0
   427  }
   428  
   429  func StringMd5(s string) string {
   430  	h := md5.New()
   431  	io.WriteString(h, s)
   432  	return fmt.Sprintf("%x", h.Sum(nil))
   433  }
   434  
   435  func IoMd5(r io.Reader) (sum string, err error) {
   436  	h := md5.New()
   437  	_, err = io.Copy(h, r)
   438  	if err != nil {
   439  		return
   440  	}
   441  	sum = fmt.Sprintf("%x", h.Sum(nil))
   442  	return
   443  }
   444  
   445  func timeoutDialer(timeout int) func(string, string) (net.Conn, error) {
   446  	return func(netw, addr string) (c net.Conn, err error) {
   447  		delta := time.Duration(timeout) * time.Second
   448  		c, err = net.DialTimeout(netw, addr, delta)
   449  		if err != nil {
   450  			return nil, err
   451  		}
   452  		c.SetDeadline(time.Now().Add(delta))
   453  		return c, nil
   454  	}
   455  }