初次提交

This commit is contained in:
2022-04-02 16:17:29 +08:00
commit 15e175f061
8 changed files with 589 additions and 0 deletions

85
src/file.ts Normal file
View 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
View File

@ -0,0 +1,2 @@
export * from './file'
export * from './reader'

356
src/reader.ts Normal file
View 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)
}
}