UNPKG

12.4 kBJavaScriptView Raw
1const fs = require('fs')
2const crypto = require('crypto')
3const urlParser = require('url')
4const http = require('http')
5const https = require('https')
6const { Transform, Readable } = require('stream')
7const fileType = require('file-type')
8
9const Img = require('./img')
10const { getVal, compose } = require('./util')
11const { HTTPError } = require('./error')
12
13// 请求网络图片时使用的代理信息
14const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36(KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
15// 请求网络图片时使用的期望资源类型
16const ACCEPT = 'image/jpg,image/png,image/webp,image/gif,image/bmp,*/*;q=0.8'
17
18class AliImg {
19
20 constructor (options) {
21 this.options = options
22 return this.createImg.bind(this)
23 }
24
25 /**
26 * 将本地文件上传到OSS
27 * @param {string} fullpath 本地文件路径
28 * @param {string} objectName 上传到OSS的对象名
29 */
30 putObject (fullpath, objectName) {
31 return this.putObjectFromStream(fs.createReadStream(fullpath), objectName)
32 }
33
34 /**
35 * 将图片数据流上传到OSS
36 * @param {Readable} readable 源图片的数据流
37 * @param {string} objectName 上传到OSS的对象名
38 */
39 putObjectFromStream(readable, objectName) {
40 return new Promise((resolve, reject) => {
41 let writable = null
42 readable.on('error', reject).on('data', (chunk) => {
43 if (!writable) {
44 // 根据数据的头部判断文件的媒体类型
45 let headers = {'Content-Type': fileType(chunk).mime}
46 // 建立上传连接
47 writable = this.putObj(objectName, headers)
48 writable.on('error', reject).on('response', compose(resolve, reject))
49 }
50 // 将图片数据写入上传连接
51 writable.write(chunk)
52 }).on('end', function () {
53 writable.end()
54 })
55 })
56 }
57
58 /**
59 * 将网络图片上传到阿里云OSS
60 * @param {string} url 图片链接
61 * @param {string} objectName 上传的阿里云位置
62 */
63 putObjectFromUrl (url, objectName) {
64
65 let opt = urlParser.parse(url)
66 // 部分网络服务需要有浏览器的代理信息才正常返回图片数据
67 opt.headers = { Accept: ACCEPT, 'User-Agent': USER_AGENT }
68
69 // 根据协议选择代理
70 let agent = opt.protocol === 'https:' ? https : http
71
72 return new Promise((resolve, reject) => {
73 agent.get(opt).on('error', reject).on('response', (res) => {
74 let { statusCode } = res
75 // 如果遇到图片的重定向请求,则使用新的链接处理
76 if (statusCode === 301 || statusCode === 302 || statusCode === 307) {
77 let key = 'Location'
78 // 获取新的链接
79 let url = getVal(res.headers, key)
80 // 递归处理图片上传
81 this.putObjectFromUrl(url, objectName).then(resolve).catch(reject)
82 } else if (statusCode < 200 || statusCode >= 300) {
83 // 如果请求图片出错,则抛出错误
84 let buf = []
85 res.on('data', Array.prototype.push.bind(buf)).on('end', function () {
86 // 请求图片出错的主体数据
87 let body = Buffer.concat(buf).toString()
88 reject(new HTTPError(statusCode, body))
89 })
90 } else {
91 // 使用图片的响应头部,打开OSS的上传连接
92 let writable = this.putObj(objectName, res.headers)
93 writable.on('error', reject).on('response', compose(resolve, reject))
94 // 将图片的主体数据写入到上传连接
95 res.pipe(writable)
96 }
97 })
98 })
99 }
100
101 createImg (path) {
102 const img = new Img(path)
103 img.client = this
104 img.toBuffer = this.toBuffer
105 img.stream = this.stream
106 img.save = this.save
107 img.write = this.write
108 img.putObjects = this.putObjects
109 return img
110 }
111
112 /**
113 * 将处理后的图片数据保存到OSS
114 * @param {string} objectName OSS对象名
115 */
116 save (objectName) {
117 return new Promise((resolve, reject) => {
118 // 获取处理后的图片数据
119 let readable = this.stream()
120 readable.on('error', reject).on('headers', headers => {
121 // 打开OSS的上传连接
122 let writable = this.client.putObj(objectName, headers)
123 // 获取图片的OSS链接
124 let url = this.client.getUrl(objectName)
125 writable.on('error', reject).on('response', compose(resolve.bind(null, url), reject))
126 // 将处理后的图片数据写入上传连接
127 readable.pipe(writable)
128 })
129 })
130 }
131
132 /**
133 * 返回处理后图片的二进制数据
134 */
135 toBuffer () {
136 return new Promise((resolve, reject) => {
137 let buf = []
138 this.stream().on('error', reject).on('data', Array.prototype.push.bind(buf)).on('end', function() {
139 // 图片的二进制数据
140 let data = Buffer.concat(buf)
141 resolve(data)
142 })
143 })
144 }
145
146 /**
147 * 返回可读流,包含处理后的图片数据
148 */
149 stream () {
150 // 转换流不转换数据,仅用于传送数据
151 const transform = new Transform({
152 transform(chunk, encoding, callback) {
153 this.push(chunk)
154 callback()
155 }
156 })
157
158 // 上传图片依赖的资源
159 this.putObjects().then(() => {
160 // 处理图片
161 let readable = this.client.getObj(this.toString())
162
163 // 如果HTTP请求出错,在返回的流对象上触发错误事件
164 // 将获取到的图片数据写入到流对象上
165 readable.on('error', transform.emit.bind(transform, 'error')).on('headers', transform.emit.bind(transform, 'headers'))
166 readable.pipe(transform)
167 // 如果依赖资源上传时出错,在返回的流对象上触发错误事件
168 }).catch(transform.emit.bind(transform, 'error'))
169
170 return transform
171 }
172
173 /**
174 * 将处理好的图片写入本地文件
175 * @param {string} fullpath 本地文件路径
176 */
177 write (fullpath) {
178 return new Promise((resolve, reject) => {
179 // 通过流将数据写入本地文件
180 let writable = fs.createWriteStream(fullpath)
181 // 当写入时发生错误,将promise置为rejected
182 writable.on('error', reject).on('finish', resolve)
183 this.stream().on('error', reject).pipe(writable)
184 })
185 }
186
187 /**
188 * 并行上传图片的全部依赖资源
189 */
190 putObjects () {
191 let list = this.child.map((item) => {
192 if (item.path instanceof Readable) {
193 return this.client.putObjectFromStream(item.path, item.objectName)
194 } else if (/https?:\/\//i.test(item.path)) {
195 return this.client.putObjectFromUrl(item.path, item.objectName)
196 } else {
197 return this.client.putObject(item.path, item.objectName)
198 }
199 })
200
201 return Promise.all(list)
202 }
203
204 /**
205 * 获取一个OSS对象,返回一个可读流
206 * @param {string} objectName OSS对象名
207 */
208 getObj(objectName) {
209 let method = 'GET'
210 // 获取日期首部
211 let headers = { Date: this.getDate() }
212 // 获取授权首部
213 headers.Authorization = this.getAuthorization({method,headers,objectName})
214 // 获取OSS对象的链接
215 let url = this.getUrl(objectName)
216 let opt = urlParser.parse(url)
217 Object.assign(opt, { headers })
218 // 根据协议选择代理
219 let agent = opt.protocol === 'https:' ? https : http
220 // 转换流不做数据转换,仅用于传送数据
221 let transform = new Transform({
222 transform(chunk, encoding, cb) {
223 this.push(chunk)
224 cb()
225 }
226 })
227 // 请求OSS对象出错时,在流对象上抛出错误
228 agent.get(opt).on('error', transform.emit.bind(transform, 'error')).on('response', function (res) {
229 let { statusCode, headers } = res
230 // 请求OSS对象出错时,在流对象上抛出错误
231 if (statusCode < 200 || statusCode >= 300) {
232 let buf = []
233 res.on('data', Array.prototype.push.bind(buf)).on('end', function () {
234 // 响应的主体数据
235 let body = Buffer.concat(buf).toString()
236 // 将响应的主体数据用于错误信息,触发错误事件
237 transform.emit('error', new HTTPError(statusCode, body))
238 })
239 } else {
240 // 通过在流对象上触发 headers 事件,传递响应首部
241 transform.emit('headers', headers)
242 res.on('error', transform.emit.bind(transform, 'error')).pipe(transform)
243 }
244 })
245
246 return transform
247 }
248
249 /**
250 * 打开OSS的上传连接,返回一个可写流
251 * @param {string} objectName OSS对象名
252 * @param {Object} headers 请求首部
253 */
254 putObj(objectName, headers) {
255 let method = 'PUT'
256 let key = 'Date'
257 if (!getVal(headers, key)) {
258 headers.Date = this.getDate()
259 }
260 // 获取OSS授权首部
261 headers.Authorization = this.getAuthorization({method,headers,objectName})
262 // 获取OSS的对象链接
263 let url = this.getUrl(objectName)
264 let opt = urlParser.parse(url)
265 Object.assign(opt, { method, headers })
266 // 根据协议选择代理
267 let agent = opt.protocol === 'https:' ? https : http
268 // 发起请求,返回请求对象
269 return agent.request(opt)
270 }
271
272 // 获取OSS对象的HTTP链接
273 getUrl (objectName) {
274 return 'http://' + [this.options.bucket, this.options.region, 'aliyuncs.com'].join('.') + '/' + objectName
275 }
276
277 // 根据配置参数生成授权首部
278 getAuthorization (opt) {
279 return 'OSS ' + this.options.accessKeyId + ':' + this.getSignature(opt)
280 }
281
282 // 根据配置参数生成签名信息
283 getSignature (options) {
284 const method = options.method ? options.method.toUpperCase() : 'GET'
285 const type = getVal(options.headers, 'content-type') || options.type || ''
286 const headers = options.headers ? this.canonicalizedOssHeaders(options.headers) : ''
287 const md5str = getVal(options.headers, 'content-md5') || (options.body ? this.getMD5(options.body) : '')
288 const datestr = this.getDate()
289 const query = options.query || {}
290 const objectName = options.objectName || '/'
291 const resoure = this.canonicalizedResource(objectName, query)
292 const str = [method, md5str, type, datestr, headers].join('\n') + resoure
293 return this.getHmac(str)
294 }
295
296 getMD5 (text) {
297 return crypto.createHash('md5').update(text).digest('base64')
298 }
299
300 getHmac (text) {
301 return crypto.createHmac('sha1', Buffer.from(this.options.accessKeySecret)).update(text).digest('base64')
302 }
303
304 getDate () {
305 return new Date().toUTCString()
306 }
307
308 canonicalizedOssHeaders (obj) {
309 const arr = []
310 for (let key in obj) {
311 if (/^x-oss-/i.test(key)) {
312 arr.push(key.toLowerCase() + ':' + obj[key])
313 }
314 }
315 if (arr.length === 0) {
316 return ''
317 }
318 arr.sort()
319 return arr.join('\n') + '\n'
320 }
321
322 canonicalizedResource (objectName, query) {
323 const arr = []
324 for (let key in query) {
325 arr.push(key + '=' + query[key])
326 }
327 if (arr.length === 0) {
328 return `/${this.options.bucket}/${objectName}`
329 }
330 arr.sort()
331 return objectName + '?' + arr.join('&')
332 }
333}
334
335module.exports = AliImg
\No newline at end of file