初次提交
This commit is contained in:
85
src/file.ts
Normal file
85
src/file.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import crypto from 'crypto'
|
||||
|
||||
interface IFileConstructorOption {
|
||||
name: string
|
||||
type: string
|
||||
saveDir: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单提交的文件
|
||||
*/
|
||||
export class File {
|
||||
/** 文件名 */
|
||||
public readonly name: string
|
||||
/** mime类型 */
|
||||
public readonly type: string
|
||||
/** 文件保存路径 */
|
||||
public path: string
|
||||
|
||||
#ws: fs.WriteStream
|
||||
#hash: crypto.Hash
|
||||
#hashstr!: string
|
||||
#finished = false
|
||||
#size = 0
|
||||
#top64 = Buffer.from('')
|
||||
|
||||
constructor(option: IFileConstructorOption) {
|
||||
this.name = option.name
|
||||
this.type = option.type
|
||||
const randomText = parseInt(Math.random() * 10000 as any).toString().padStart(4, '0')
|
||||
this.path = path.join(option.saveDir, `${Date.now()}${randomText}`)
|
||||
this.#ws = fs.createWriteStream(this.path)
|
||||
this.#hash = crypto.createHash('md5')
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入数据到文件
|
||||
* @param data 写入的数据
|
||||
*/
|
||||
public async write(data: Buffer) {
|
||||
if (this.#finished || !this.#ws.writable) return
|
||||
//开始64字节
|
||||
if (this.#top64.length < 64) {
|
||||
this.#top64 = Buffer.concat([
|
||||
this.#top64,
|
||||
data.slice(0, 64 - this.#top64.length)
|
||||
])
|
||||
}
|
||||
|
||||
this.#size += data.length
|
||||
|
||||
//写入
|
||||
await Promise.all([
|
||||
new Promise<void>((resolve, reject) => this.#ws.write(data, err => err ? reject(err) : resolve())),
|
||||
new Promise<void>((resolve, reject) => this.#hash.write(data, err => err ? reject(err) : resolve())),
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束
|
||||
*/
|
||||
public finish() {
|
||||
this.#ws.destroy()
|
||||
this.#hashstr = this.#hash.digest('hex')
|
||||
this.#hash.destroy()
|
||||
this.#finished = true
|
||||
}
|
||||
|
||||
/** 文件hash */
|
||||
public get hash() {
|
||||
return this.#hashstr
|
||||
}
|
||||
|
||||
/** 文件大小 */
|
||||
public get size() {
|
||||
return this.#size
|
||||
}
|
||||
|
||||
/** 文件开始64字节 */
|
||||
public get top64() {
|
||||
return this.#top64
|
||||
}
|
||||
}
|
2
src/index.ts
Normal file
2
src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './file'
|
||||
export * from './reader'
|
356
src/reader.ts
Normal file
356
src/reader.ts
Normal file
@ -0,0 +1,356 @@
|
||||
import type http from 'http'
|
||||
import { File } from './file'
|
||||
|
||||
enum ReadState {
|
||||
boundary = 1, //需要读取boundary
|
||||
header = 2, //需要读取header
|
||||
content = 3, //需要读取内容
|
||||
finish = 4, //结束
|
||||
}
|
||||
|
||||
//字段头
|
||||
interface IFormDataHeaderField {
|
||||
/** 头类型 */
|
||||
type: 'field'
|
||||
/** 表单name */
|
||||
name: string
|
||||
/** 字段内容 */
|
||||
content: string
|
||||
}
|
||||
|
||||
//文件头
|
||||
interface IFormDataHeaderFile {
|
||||
/** 头类型 */
|
||||
type: 'file'
|
||||
/** 表单name */
|
||||
name: string
|
||||
/** 文件 */
|
||||
file: File
|
||||
}
|
||||
|
||||
//整合后的头
|
||||
type IFormDataHeader = IFormDataHeaderField | IFormDataHeaderFile
|
||||
|
||||
export interface IMultipartReaderResult {
|
||||
/** 文件列表 */
|
||||
files: { [k in string]: File | Array<File> }
|
||||
/** 表单字段列表 */
|
||||
fields: { [k in string]: string | Array<string> }
|
||||
}
|
||||
|
||||
export interface IMultipartReaderConstructorOption {
|
||||
/** 请求 */
|
||||
req: http.IncomingMessage
|
||||
/** 分隔符 */
|
||||
boundary: string
|
||||
/** 文件保存目录 */
|
||||
saveDir: string
|
||||
}
|
||||
|
||||
interface IReadResult {
|
||||
left: Buffer | null
|
||||
exit: boolean
|
||||
}
|
||||
|
||||
export class MultipartReader {
|
||||
/** 分隔符 */
|
||||
#boundary: Buffer
|
||||
/** 保存目录 */
|
||||
#saveDir: string
|
||||
/** 当前处理状态 */
|
||||
#state = ReadState.boundary
|
||||
/** 上一次没有处理完的头数据 */
|
||||
#headBuffer: Buffer | null = null
|
||||
/** 当前头信息 */
|
||||
#header: IFormDataHeader | undefined = undefined
|
||||
/** 处理到的字段信息 */
|
||||
#fields: IMultipartReaderResult['fields'] = {}
|
||||
/** 处理到的文件信息 */
|
||||
#files: IMultipartReaderResult['files'] = {}
|
||||
/** 已读取的长度 */
|
||||
#readed = 0
|
||||
/** 进度监听 */
|
||||
#onReadCallback?: (readed: number) => any
|
||||
|
||||
//wait回调
|
||||
#resolve?: (data: IMultipartReaderResult) => any
|
||||
#reject?: (err: Error) => any
|
||||
|
||||
//======================================================公共函数======================================================
|
||||
|
||||
/**
|
||||
* multipart读取工具
|
||||
* @param option 选项
|
||||
*/
|
||||
constructor(option: IMultipartReaderConstructorOption) {
|
||||
this.#boundary = Buffer.from(option.boundary)
|
||||
this.#saveDir = option.saveDir
|
||||
this.#handleRequest(option.req)
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待处理,处理完成得到内容
|
||||
*/
|
||||
public async wait() {
|
||||
return new Promise<IMultipartReaderResult>((resolve, reject) => {
|
||||
this.#resolve = resolve
|
||||
this.#reject = reject
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听读取过程
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
public onRead(callback: (readed: number) => any) {
|
||||
this.#onReadCallback = callback
|
||||
}
|
||||
|
||||
//======================================================私有函数======================================================
|
||||
|
||||
//处理请求
|
||||
#handleRequest(req: http.IncomingMessage) {
|
||||
let old: Buffer | null = null //处理剩下的Buffer
|
||||
const dataReader = async (_data: Buffer) => {
|
||||
//暂停读取
|
||||
req.pause()
|
||||
//数据内容
|
||||
const data: Buffer = old ? Buffer.concat([old, _data]) : _data
|
||||
old = await this.#parseData(data)
|
||||
//触发一下监听过程
|
||||
this.#onReadCallback?.(this.#readed += _data.length)
|
||||
//处理数据
|
||||
req.resume()
|
||||
if (this.#state == ReadState.finish) {
|
||||
this.#resolve?.({ files: this.#files, fields: this.#fields })
|
||||
}
|
||||
}
|
||||
const onError = (err: Error) => {
|
||||
this.#reject?.(err)
|
||||
}
|
||||
//事件处理
|
||||
req.on('error', onError)
|
||||
req.on('data', dataReader)
|
||||
req.once('end', () => {
|
||||
//删除监听器
|
||||
req.removeListener('error', onError)
|
||||
req.removeListener('data', dataReader)
|
||||
})
|
||||
}
|
||||
|
||||
//定义一个函数来处理data
|
||||
async #parseData(data: Buffer): Promise<Buffer | null> {
|
||||
let result: IReadResult = { exit: false, left: data }
|
||||
//使用循环,以避免使用递归调用
|
||||
while (result.left && !result.exit) {
|
||||
switch (this.#state) {
|
||||
//读取boundary
|
||||
//formdata中的每个数据是由 Content-Type中指定的Boundary进行分割的
|
||||
//内容的分隔符中,会在boundary前加上--,如果是结束,末尾还有--
|
||||
case ReadState.boundary:
|
||||
result = await this.#readBoundary(result.left)
|
||||
break
|
||||
//读取头
|
||||
//头部以两个连续的换行为结束
|
||||
case ReadState.header:
|
||||
result = await this.#readHeader(result.left)
|
||||
break
|
||||
//读取内容
|
||||
//内容可以一直读取,直到遇到下一个分隔符为止
|
||||
case ReadState.content:
|
||||
result = await this.#readContent(result.left)
|
||||
break
|
||||
//已经读完,其他的就不管了
|
||||
case ReadState.finish:
|
||||
result = { exit: true, left: null }
|
||||
break
|
||||
}
|
||||
}
|
||||
//返回剩余内容
|
||||
return result?.left ?? null
|
||||
}
|
||||
|
||||
//读取boundary(一开始会读取boundary,内容读取完后,也会回来继续读取boundary)
|
||||
async #readBoundary(data: Buffer): Promise<IReadResult> {
|
||||
//如果数据量不够,剩下的数据下次处理
|
||||
if (data.length <= this.#boundary.length + 2) return { left: data, exit: true } //+2是因为boundary开始有 --
|
||||
//开始有 --
|
||||
if (data[0] == 45 && data[1] == 45) data = data.slice(2)
|
||||
else throw new Error('multipart body error')
|
||||
//去除boundary
|
||||
if (this.#boundary.compare(data, 0, this.#boundary.length) == 0) data = data.slice(this.#boundary.length)
|
||||
else throw new Error('multipart body error')
|
||||
//去除\r\n
|
||||
if (data[0] == 13) {
|
||||
data = data.slice(1)
|
||||
if (data[0] == 10) data = data.slice(1) // \r
|
||||
else throw new Error('multipart body error') // \r后面必须时\n
|
||||
}
|
||||
//去除\n
|
||||
else if (data[0] == 10) data = data.slice(1)
|
||||
//遇到 -- , 可能就要结束了
|
||||
else if (data[0] == 45 && data[1] == 45) {
|
||||
//看看是否有换行,有换行表示结束
|
||||
if (data[2] == 10 || (data[2] == 13 && data[3] == 10)) {
|
||||
this.#saveHeader()
|
||||
this.#state = ReadState.finish
|
||||
return { left: null, exit: true }
|
||||
}
|
||||
//否则就当出错
|
||||
else throw new Error('multipart body error')
|
||||
}
|
||||
//上面的情况都不是,表示出错了
|
||||
else throw new Error('multipart body error')
|
||||
//开始读取头
|
||||
this.#state = ReadState.header
|
||||
//返回剩下的数据
|
||||
return { left: data, exit: false }
|
||||
}
|
||||
|
||||
//读取头信息(当度去玩boundary后就应该读取头)
|
||||
async #readHeader(data: Buffer): Promise<IReadResult> {
|
||||
//读取头结束位置
|
||||
let endAt = 0
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
// 检测是不是两个连续换行
|
||||
if (data[i] == 10) {
|
||||
if (data[i + 1] == 10) {
|
||||
endAt = i + 1
|
||||
break
|
||||
}
|
||||
else if (data[i + 1] == 13 && data[i + 2] == 10) {
|
||||
endAt = i + 2
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
//如果头读取完成,则读取body
|
||||
if (endAt) {
|
||||
//取得头
|
||||
const header = this.#headBuffer ? Buffer.concat([this.#headBuffer, data.slice(0, endAt + 1)]) : data.slice(0, endAt + 1)
|
||||
this.#headBuffer = null
|
||||
await this.#resolveHead((header + '').trim())
|
||||
//读取内容
|
||||
this.#state = ReadState.content
|
||||
return { left: data.slice(endAt + 1), exit: false }
|
||||
}
|
||||
//否则将内容缓存起来,下次处理
|
||||
else {
|
||||
this.#headBuffer = this.#headBuffer ? Buffer.concat([this.#headBuffer, data]) : data
|
||||
return { left: null, exit: true }
|
||||
}
|
||||
}
|
||||
|
||||
//读取内容(头读取完后,就要读取内容了)
|
||||
async #readContent(data: Buffer): Promise<IReadResult> {
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
//处理换行
|
||||
let gotbr = 0
|
||||
if (data[i] == 10) gotbr = 1
|
||||
else if (data[i] == 13 && data[i + 1] == 10) gotbr = 2
|
||||
|
||||
//遇到换行,那么很有可能就遇到了分隔符
|
||||
if (gotbr) {
|
||||
//看看内容够不够,不够啦?先保存起来,剩下的下次处理
|
||||
if (data.length - i - gotbr - 2 < this.#boundary.length) { //-gotbr表示减去换行符,-2表示减去分隔符开始的--
|
||||
// 保存数据
|
||||
await this.#resolveData(data.slice(0, i))
|
||||
//剩下的可能时分隔符的内容留着下次处理
|
||||
return { left: data.slice(i), exit: true }
|
||||
}
|
||||
//内容充足,处理内容
|
||||
else {
|
||||
//跳过换行符
|
||||
i += gotbr
|
||||
//遇到了 -- ,后面很有可能时分隔符,瞧一眼
|
||||
if (data[i] == 45 && data[i + 1] == 45) {
|
||||
//看看是不是遇到了分隔符
|
||||
if (this.#boundary.compare(data, i + 2, i + 2 + this.#boundary.length) == 0) {
|
||||
//保存数据
|
||||
await this.#resolveData(data.slice(0, i - gotbr))
|
||||
//接下来读取分隔符
|
||||
this.#state = ReadState.boundary
|
||||
return { left: data.slice(i), exit: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//没有遇到分隔符,将内容直接保存
|
||||
await this.#resolveData(data)
|
||||
return { left: null, exit: true }
|
||||
}
|
||||
|
||||
//收到数据后的处理
|
||||
async #resolveData(data: Buffer) {
|
||||
if (!this.#header) return
|
||||
//普通字段处理
|
||||
if (this.#header.type == 'field') this.#header.content += data
|
||||
//文件处理
|
||||
else await this.#header.file.write(data)
|
||||
}
|
||||
|
||||
//头部读取完成后的处理
|
||||
async #resolveHead(data: string) {
|
||||
this.#saveHeader()
|
||||
this.#header = undefined
|
||||
const body: { [i in string]: string } = {}
|
||||
//先按行分割
|
||||
const liens = data.split(/\r?\n/).map(s => s.trim()).filter(s => !!s)
|
||||
//处理每行的内容
|
||||
liens.forEach(line => {
|
||||
const match = line.match(/^([^:]+):([\s\S]+)$/)
|
||||
if (!match) return
|
||||
const key = match[1].trim().toLowerCase()
|
||||
const val = match[2].trim()
|
||||
if (key == 'content-disposition') {
|
||||
const items = val.split(/;/).map(s => s.trim()).filter(s => !!s)
|
||||
items.forEach(item => {
|
||||
let [k, v] = item.split(/=/)
|
||||
if (!v) return
|
||||
k = k.toLowerCase()
|
||||
//去除引号
|
||||
if (v[0] == '"' && v[v.length - 1] == '"') v = v.substring(1, v.length - 1)
|
||||
else if (v[0] == "'" && v[v.length - 1] == "'") v = v.substring(1, v.length - 1)
|
||||
//保存
|
||||
body[k] = v
|
||||
})
|
||||
}
|
||||
else body[key] = val
|
||||
})
|
||||
//类型处理
|
||||
if (body['content-type']) this.#header = {
|
||||
type: 'file',
|
||||
name: body.name,
|
||||
file: new File({ name: body.filename, type: body['content-type'], saveDir: this.#saveDir })
|
||||
}
|
||||
else this.#header = {
|
||||
type: 'field',
|
||||
name: body.name,
|
||||
content: '',
|
||||
}
|
||||
}
|
||||
|
||||
//保存头内容
|
||||
#saveHeader() {
|
||||
if (!this.#header) return
|
||||
if (this.#header.type == 'field') this.#putField(this.#header.name, this.#header.content)
|
||||
else if (this.#header.type == 'file') this.#putFile(this.#header.name, this.#header.file)
|
||||
}
|
||||
|
||||
//存入字段信息
|
||||
#putField(name: string, value: string) {
|
||||
if (!this.#fields[name]) this.#fields[name] = value
|
||||
else if (this.#fields[name] instanceof Array) (this.#fields as any)[name].push(value)
|
||||
else (this.#fields[name] = [this.#fields[name] as string]).push(value)
|
||||
}
|
||||
|
||||
//存入文件信息
|
||||
#putFile(name: string, file: File) {
|
||||
file.finish()
|
||||
if (!this.#files[name]) this.#files[name] = file
|
||||
else if (this.#files[name] instanceof Array) (this.#files as any)[name].push(file)
|
||||
else (this.#files[name] = [this.#files[name] as File]).push(file)
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user