Full Code of shengxinjing/file-upload for AI

master 216ac00c427c cached
21 files
30.2 KB
9.0k tokens
10 symbols
1 requests
Download .txt
Repository: shengxinjing/file-upload
Branch: master
Commit: 216ac00c427c
Files: 21
Total size: 30.2 KB

Directory structure:
gitextract_lxtez43b/

├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── client/
│   ├── .gitignore
│   ├── README.md
│   ├── babel.config.js
│   ├── package.json
│   ├── public/
│   │   ├── hash.js
│   │   └── index.html
│   ├── src/
│   │   ├── App.vue
│   │   ├── components/
│   │   │   └── HelloWorld.vue
│   │   ├── main.js
│   │   └── plugins/
│   │       └── element.js
│   └── vue.config.js
├── index.html
└── server/
    ├── app/
    │   ├── controller/
    │   │   └── home.js
    │   ├── router.js
    │   └── service/
    │       └── upload.js
    ├── config/
    │   └── config.default.js
    └── package.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true


================================================
FILE: .gitignore
================================================
# Logs
logs
app/run
server/app/public/
server/app/run/
server/run/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 woniuppp

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# file-upload
面试造火箭系列


 1. vue create client
 2. cd client && vue add element
 3. mkdir server
 4. npm install egg egg-bin --save

================================================
FILE: client/.gitignore
================================================
.DS_Store
node_modules
/dist

# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: client/README.md
================================================
# client

## Project setup
```
npm install
```

### Compiles and hot-reloads for development
```
npm run serve
```

### Compiles and minifies for production
```
npm run build
```

### Run your tests
```
npm run test
```

### Lints and fixes files
```
npm run lint
```

### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).


================================================
FILE: client/babel.config.js
================================================
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ]
}


================================================
FILE: client/package.json
================================================
{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "start": "npm run serve"
  },
  "dependencies": {
    "axios": "^0.19.2",
    "blessed": "^0.1.81",
    "blessed-contrib": "^4.8.18",
    "core-js": "^3.6.4",
    "element-ui": "^2.4.5",
    "marked": "^0.8.0",
    "spark-md5": "^3.0.0",
    "stylus": "^0.54.7",
    "stylus-loader": "^3.0.2",
    "vue": "^2.6.11"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "^4.2.0",
    "@vue/cli-plugin-eslint": "^4.2.0",
    "@vue/cli-service": "^4.2.0",
    "babel-eslint": "^10.0.3",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.1.2",
    "vue-cli-plugin-element": "^1.0.1",
    "vue-template-compiler": "^2.6.11"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions"
  ]
}


================================================
FILE: client/public/hash.js
================================================


// web-worker
self.importScripts('spark-md5.min.js')

self.onmessage = e=>{
    // 接受主线程的通知
    const {chunks} = e.data
    const spark = new self.SparkMD5.ArrayBuffer()
    let progress = 0
    let count = 0
    
    const loadNext = index=>{
        const reader = new FileReader()
        reader.readAsArrayBuffer(chunks[index].file)
        reader.onload = e=>{
            // 累加器 不能依赖index,
            count++
            // 增量计算md5
            spark.append(e.target.result)
            if(count===chunks.length){
                // 通知主线程,计算结束
                self.postMessage({
                    progress:100,
                    hash:spark.end()
                })
            }else{
                // 每个区块计算结束,通知进度即可
                progress += 100/chunks.length
                self.postMessage({
                    progress
                })
                // 计算下一个
                loadNext(count)
            }
        }
    }
    // 启动
    loadNext(0)


}

================================================
FILE: client/public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: client/src/App.vue
================================================
<template>
  <div class="app">

    <input type="text">
    <i class="el-icon-loading" style="color:#F56C6C;"></i>

    <!-- <form method="post" action="http://localhost:7001/upload" enctype="multipart/form-data"> -->
    <div ref="drag" id="drag">
      <input type="file" name="file" @change="handleFileChange" />
      <!-- <img :src="preview" alt=""> -->
    </div>
    <!-- <div v-loading="loading">
      <textarea ref="article" v-model="article" cols="30" rows="10"></textarea>
      <div class="output" v-html="articleHtml"></div>
    </div>
    <div v-if="file">{{file.name}}</div> -->

    <div>
      上传进度
    </div>
      <el-progress :text-inside="true" :stroke-width="20" :percentage="uploadProgress"></el-progress>

          <div>文件准备中</div>
          <div>
      <el-progress :text-inside="true" :stroke-width="20" :percentage="hashProgress"></el-progress>

          </div>
      {{hashProgress}}
    <div> 
      <el-button type="primary" @click="handleUpload">上 传</el-button>
    </div>
        <div> 
      <el-button type="primary" @click="handleUpload1">慢启动上传</el-button>
    </div>





    <!-- 方块进度条 -->

      <div class="cube-container" :style="{width:cubeWidth+'px'}">
        <div class="cube" 

          v-for="chunk in chunks" 
          :key="chunk.name">
          <div           
            :class="{
            'uploading':chunk.progress>0&&chunk.progress<100, 
            'success':chunk.progress==100,
            'error':chunk.progress<0,
            }" 
            :style="{height:chunk.progress+'%'}"
            >
            <i v-if="chunk.progress<100 &&chunk.progress>1" class="el-icon-loading" style="color:#F56C6C;"></i>
          </div>
        </div>
      </div>

  </div>
</template>

<script>
import marked from 'marked'
import sparkMd5 from 'spark-md5'
const CHUNK_SIZE = 1*1024*1024 // 1M
const IMG_WIDTH_LIMIT = 1000
const IMG_HEIGHT_LIMIT = 1000
export default {
  name: "app",
  data() {
    return {
      chunks:[],
      file: null,
      hash:null,
      preview:null,
      article:`# 蜗牛老湿开心的一天
      * 吃饭
      * 睡觉
      * 上王者
`,
      loading:false,
      hashProgress:0
    };
  },
  computed:{
    articleHtml(){
      return marked(this.article)
    },
    cubeWidth(){
      return Math.ceil(Math.sqrt(this.chunks.length))*16
    },
    uploadProgress() {
      if (!this.file || !this.chunks.length) return 0;
      const loaded = this.chunks
        .map(item => item.chunk.size * item.progress)
        .reduce((acc, cur) => acc + cur);
      return parseInt((loaded / this.file.size).toFixed(2));
    }
  },
  async mounted() {
    // this.bindDragEvent('drag',()=>{
    //   this.preview = window.URL.createObjectURL(this.file)

    // })
    // this.bindDragEvent('article',async ()=>{
    //     this.loading = true
    //     const ret = await this.handleUpload()
    //     this.article += `![${this.file.name}](/api${ret.url})`
    //     this.loading = false

    // })
    // this.bindPasteEvent()
  },
  methods: {
    bindPasteEvent(){
      this.$refs.article.addEventListener('paste',async e=>{
        const files = e.clipboardData.files
        if(!files.length) return 
        this.file = files[0]
        this.loading = true
        const ret = await this.handleUpload()
        this.article += `![${this.file.name}](/api${ret.url})`
        this.loading = false

        e.preventDefault()
      })
    },
    bindDragEvent(name,cb) {
      const drag = this.$refs[name]

      drag.addEventListener("dragover", e => {
        drag.style.borderColor = "red"
        e.preventDefault()
      })
      drag.addEventListener("dragleave", e => {
        drag.style.borderColor = "#eee"
        e.preventDefault()
      })
      drag.addEventListener("drop", e => {
          const fileList = e.dataTransfer.files
          drag.style.borderColor = "#eee"
          this.file = fileList[0]; // 先只考虑单文件
          cb && cb()
          e.preventDefault()
        },
        false)
    },

    handleFileChange(e) {
      const [file] = e.target.files;
      if (!file) return;
     
      // if(file.size>CHUNK_SIZE){
      //   this.$message.error("请选择小于2M的文件");
      //   return;
      // }
      // if(!this.isImage(file)){
      //   this.$message.error("请选择正确的图片格式");
      //   return 
      // }

      this.file = file;
    },
    async blobToData(blob){
      return new Promise(resolve=>{
        const reader = new FileReader()
        reader.onload = function () {
          resolve(reader.result)
        }
        reader.readAsBinaryString(blob)
      })
      // 二进制=》ascii码=》转成16进制字符串
    },
    async blobToString(blob){
      return new Promise(resolve=>{
        const reader = new FileReader()
        reader.onload = function () {
          const ret = reader.result.split('')
                        .map(v=>v.charCodeAt())
                        .map(v=>v.toString(16).toUpperCase())
                        .map(v=>v.padStart(2,'0'))
                        .join(' ')
          resolve(ret)
        }
        reader.readAsBinaryString(blob)
      })
      // 二进制=》ascii码=》转成16进制字符串
    },
    async getRectByOffset(file,widthOffset,heightOffset,reverse){
      let width = await this.blobToString(file.slice(...widthOffset))
      let height = await this.blobToString(file.slice(...heightOffset))

      if(reverse){
        // 比如gif 的宽,6和7 是反着排的 大小端存储
        // 比如6位仕89,7位仕02, gif就是 0289 而不是8920的值 切分后翻转一下
        width = [width.slice(3,5),width.slice(0,2)].join(' ')
        height = [height.slice(3,5),height.slice(0,2)].join(' ')
      }
      const w = parseInt(width.replace(' ', ''),16)
      const h = parseInt(height.replace(' ', ''),16)
      return {w,h}
    },
    async isGif(file){
      const ret = await this.blobToString(file.slice(0,6))
      const isgif = (ret==='47 49 46 38 39 61') || (ret==='47 49 46 38 37 61')
      if(isgif){
        console.log('文件是gif')

        const {w,h} = await this.getRectByOffset(file,[6,8],[8,10],true)
        console.log('gif宽高',w,h)
        if(w>IMG_WIDTH_LIMIT || h>IMG_HEIGHT_LIMIT){
          this.$message.error("gif图片宽高不得超过!"+IMG_WIDTH_LIMIT+'和'+IMG_HEIGHT_LIMIT);
          return false
        }

      }
      return isgif
      // 文件头16进制 47 49 46 38 39 61 或者47 49 46 38 37 61
      // 分别仕89年和87年的规范
      // const tmp = '47 49 46 38 39 61'.split(' ')
      //               .map(v=>parseInt(v,16))
      //               .map(v=>String.fromCharCode(v))
      // console.log('gif头信息',tmp)
      // // 或者把字符串转为16进制 两个方法用那个都行
      // const tmp1 = 'GIF89a'.split('')
      //                 .map(v=>v.charCodeAt())
      //                 .map(v=>v.toString(16))
      // console.log('gif头信息',tmp1)
      
      // return ret ==='GIF89a' || ret==='GIF87a'
      // 文件头标识 (6 bytes) 47 49 46 38 39(37) 61

    },
    async isPng(file){
      const ret = await this.blobToString(file.slice(0,8))
      const ispng = ret==='89 50 4E 47 0D 0A 1A 0A'
      if(ispng){
        console.log('png宽高',w,h)
        const {w,h} = await this.getRectByOffset(file,[18,20],[22,24])
        if(w>IMG_WIDTH_LIMIT || h>IMG_HEIGHT_LIMIT){
          this.$message.error("png图片宽高不得超过!"+IMG_WIDTH_LIMIT+'和'+IMG_HEIGHT_LIMIT);
          return false
        }
      }
      return ispng
    },
    async isJpg(file){
      // jpg开头两个仕 FF D8
      // 结尾两个仕 FF D9
      const len = file.size
      const start = await this.blobToString(file.slice(0,2))
      const tail = await this.blobToString(file.slice(-2,len))
      const isjpg = start==='FF D8' && tail==='FF D9'
      if(isjpg){
        const heightStart = parseInt('A3',16)
        const widthStart = parseInt('A5',16)
        const {w,h} = await this.getRectByOffset(file,[widthStart,widthStart+2],[heightStart,heightStart+2])
        console.log('jpg大小',w, h)
      }
      return isjpg

    },
    isImage(file){
      return this.isGif(file) && this.isPng(file) && this.isJpg(file)

    },
    createFileChunk(file,size=CHUNK_SIZE){
      // 生成文件块 Blob.slice语法
      const chunks = [];
      let cur = 0;
      while (cur < file.size) {
        chunks.push({index:cur, file: file.slice(cur, cur + size)});
        cur += size;
      }
      return chunks;
    },
    ext(filename){
      // 返回文件后缀名
      return filename.split('.').pop()
    },
    async calculateHash(file){
      // 直接计算md5 大文件会卡顿
      const ret = await this.blobToData(file)
      return sparkMd5.hash(ret)
    },
    // web-worker
    async calculateHashWorker(chunks) {
      return new Promise(resolve => {
        // web-worker 防止卡顿主线程
        this.worker = new Worker("/hash.js");
        this.worker.postMessage({ chunks });
        this.worker.onmessage = e => {
          const { progress, hash } = e.data;
          this.hashProgress = Number(progress.toFixed(2));
          if (hash) {
            resolve(hash);
          }
        };
      });
    },
    async calculateHashSample() {
      return new Promise(resolve => {
        const spark = new sparkMd5.ArrayBuffer();
        const reader = new FileReader();
        const file = this.file;
        // 文件大小
        const size = this.file.size;
        let offset = 2 * 1024 * 1024;

        let chunks = [file.slice(0, offset)];

        // 前面100K

        let cur = offset;
        while (cur < size) {
          // 最后一块全部加进来
          if (cur + offset >= size) {
            chunks.push(file.slice(cur, cur + offset));
          } else {
            // 中间的 前中后去两个字节
            const mid = cur + offset / 2;
            const end = cur + offset;
            chunks.push(file.slice(cur, cur + 2));
            chunks.push(file.slice(mid, mid + 2));
            chunks.push(file.slice(end - 2, end));
          }
          // 前取两个子杰
          cur += offset;
        }
        // 拼接
        reader.readAsArrayBuffer(new Blob(chunks));

        // 最后100K
        reader.onload = e => {
          spark.append(e.target.result);
          this.hashProgress = 100
          resolve(spark.end());
        };
      });
    },
    async calculateHashIdle(chunks) {
      return new Promise(resolve => {
        const spark = new sparkMd5.ArrayBuffer();
        let count = 0;
        const appendToSpark = async file => {
          return new Promise(resolve => {
            const reader = new FileReader();
            reader.readAsArrayBuffer(file);
            reader.onload = e => {
              spark.append(e.target.result);
              resolve();
            };
          });
        };
        const workLoop = async deadline => {
          // 有任务,并且当前帧还没结束
          while (count < chunks.length && deadline.timeRemaining() > 1) {
            await appendToSpark(chunks[count].file);
            count++;
            // 没有了 计算完毕
            if (count < chunks.length) {
              // 计算中
              this.hashProgress = Number(
                ((100 * count) / chunks.length).toFixed(2)
              );
              // console.log(this.hashProgress)
            } else {
              // 计算完毕
              this.hashProgress = 100;
              resolve(spark.end());
            }
          }
          console.log(`浏览器有任务拉,开始计算${count}个,等待下次浏览器空闲`)

          window.requestIdleCallback(workLoop);
        };
        window.requestIdleCallback(workLoop);
      });
    },
    async handleUpload1(){
      // @todo数据缩放的比率 可以更平缓  
      // @todo 并发+慢启动

      // 慢启动上传逻辑 
      const file = this.file
      if (!file) return;
      const fileSize = file.size
      let offset = 0.1*1024*1024 

      let cur = 0 
      let count =0
      this.hash = await this.calculateHashSample();

      while(cur<fileSize){
        const chunk = file.slice(cur, cur+offset)
        cur+=offset
        const chunkName = this.container.hash + "-" + count;
        const form = new FormData();

          form.append("chunkname", chunkName)
          form.append("ext", this.ext(this.file.name))
          form.append("hash", this.hash)
          // form.append("file", new File([chunk],name,{hash,type:'png'}))


        let start = new Date().getTime()
        await this.$axios.post( '/upload', form)
        const now = new Date().getTime()

        const time = ((now -start)/1000).toFixed(4)

        // 期望10秒一个切片
        let rate = time/10
        // 速率有最大和最小 可以考虑更平滑的过滤 比如1/tan 
        if(rate<0.5) rate=0.5
        if(rate>2) rate=2
        // 新的切片大小等比变化
        console.log(`切片${count}大小是${this.format(offset)},耗时${time}秒,是30秒的${rate}倍,修正大小为${this.format(offset/rate)}`)
        offset = parseInt(offset/rate)
        // if(time)
        count++
      }



    },

    // async calculateHash
    async handleUpload() {
      if (!this.file) {
        this.$message.info("请选择文件");
        return;
      }
      let chunks = this.createFileChunk(this.file);

      // 计算hash 文件指纹标识
      // this.hash = await this.calculateHash(this.file)
      // web-worker
      // this.hash = await this.calculateHashWorker(chunks)
      // requestIdleCallback
      // this.hash = await this.calculateHashIdle(chunks)
      
      // 抽样哈希,牺牲一定的准确率 换来效率,hash一样的不一定是同一个文件, 但是不一样的一定不是 
      // 所以可以考虑用来预判
      this.hash = await this.calculateHashSample()

      // 检查文件是否已经上传
      const { uploaded, uploadedList } = await this.$axios.post('/check',{
          ext:this.ext(this.file.name),
          hash:this.hash
        }
      )
      if(uploaded){
        return this.$message.success("秒传:上传成功")
      }
      // 切片

      this.chunks = chunks.map((chunk,index)=>{
        // 每一个切片的名字
        const chunkName = this.hash+'-'+index
        return {
          hash:this.hash,
          chunk:chunk.file,
          name:chunkName,
          index,
          // 设置进度条
          progress: uploadedList.indexOf(chunkName) > -1 ? 100 : 0,
        }
      })
      // 传入已经存在的切片清单
      await this.uploadChunks(uploadedList);

    },
    async mergeRequest(){
      await this.$axios.post("/merge", {
        ext: this.ext(this.file.name),
        size: CHUNK_SIZE,
        hash: this.hash
      });
    },
    sendRequest(chunks, limit=4){
      return new Promise((resolve,reject)=>{
        const len = chunks.length
        let counter = 0
        // 全局开关
        let isStop = false 


        const start = async ()=>{

          if(isStop){
            return 
          }
          const task = chunks.shift()
          if(task){
            const {form,index} = task
            try{
              await this.$axios.post('/upload',form, {
                onUploadProgress: progress => {
                  this.chunks[index].progress = Number(((progress.loaded / progress.total) * 100).toFixed(2));
                }
              })
              if(counter==len-1){
                // 最后一个
                resolve()
              }else{
                counter++
                start()
              }
            }catch(e){
              // 当前切片报错了
              // 尝试3次重试机制,重新push到数组中
              console.log('出错了')
              // 进度条改成红色
              this.chunks[index].progress = -1
              if(task.error<3){
                task.error++
                // 队首进去 准备重试
                chunks.unshift(task)
                start()
              }else{
                // 错误3次了 直接结束
                isStop=true
                reject()
              }
            }

          }
        }

        while(limit>0){
          setTimeout(()=>{
            // 模拟延迟
            start()
          }, Math.random()*2000)

          limit-=1
        }



      })

    },
    async uploadChunks(uploadedList=[]){
      const list = this.chunks
        .filter(chunk => uploadedList.indexOf(chunk.name) == -1)
        .map(({ chunk,name, hash, index }, i) => {
          const form = new FormData();
          form.append("chunkname", name)
          form.append("ext", this.ext(this.file.name))
          form.append("hash", hash)
          // form.append("file", new File([chunk],name,{hash,type:'png'}))
          form.append("file",chunk)

          return { form, index,error:0}
        })
      //   .map(({ form, index }) =>this.$axios.post('/upload',form, {
      //     onUploadProgress: progress => {
      //       this.chunks[index].progress = Number(((progress.loaded / progress.total) * 100).toFixed(2));
      //     }
      //   }))
      // await Promise.all(list);
      try{
        await this.sendRequest([...list],4)
        if(uploadedList.length + list.length === this.chunks.length){
          await this.mergeRequest()
        }
      }catch(e){
        this.$message.error('上传似乎除了点小问题,重试试试哈')
      }

    }
  }
};
</script>

<style lang="stylus">
.app>div 
  margin 50px
#drag 
  height 100px
  border 2px dashed #eee
  line-height 100px
  text-align center
  vertical-align middle
img
  width 50px
.output
  display inline-block
  vertical-align top
  margin-left 30px
  padding 10px
  width 300px
  background  #eee
  img 
    width 200px

.cube-container
  width 100px
  overflow hidden
.cube
  width 14px
  height 14px
  line-height 12px;
  border 1px solid black
  background  #eee
  float left
  >.success
    background #67C23A
  >.uploading
    background #409EFF
  >.error
    background #F56C6C

</style>


================================================
FILE: client/src/components/HelloWorld.vue
================================================
<template>
  <div class="hello">
    <h1>{{ name }}</h1>
    <hr>
    <h1>{{ type }}</h1>
        <hr>
    <h1>{{ age }}</h1>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props:['name','type','age','type:age'],
  mounted(){
    console.log(this['type:age'])
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>


================================================
FILE: client/src/main.js
================================================
import Vue from 'vue'
import App from './App.vue'
import axios from 'axios'
import './plugins/element.js'

let request = axios.create({
  baseURL:"/api"
})


request.interceptors.response.use(
  async response=>{
    // header config这里处理就可以了,应用层只需要数据data
    let {data} = response
    // if(dat)
    return data

  }
)
Vue.prototype.$axios = request
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')


================================================
FILE: client/src/plugins/element.js
================================================
import Vue from 'vue'
import Element from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.use(Element)


================================================
FILE: client/vue.config.js
================================================
module.exports = {
  devServer: {
    proxy:{
      '/api/':{
        target:'http://localhost:7001',
        secure:false,
        pathRewrite:{
          '^/api':''
        }
      }
    }
  }
}


================================================
FILE: index.html
================================================
<style>
    .water-waves {
      margin: 0 auto;
      overflow: hidden;
      position: relative;
      width: 100px;
      height: 100px;
      border-radius: 50%;
      border: 1px solid silver;
      text-align: center;
      line-height: 50px;
      animation: water-waves linear infinite;
    }
  
    .water-wave1 {
      position: absolute;
      top: 40%;
      left: -25%;
      background: #33cfff;
      opacity: 0.7;
      width: 200%;
      height: 200%;
      border-radius: 40%;
      animation: inherit;
      animation-duration: 5s;
    }
  
    .water-wave2 {
      position: absolute;
      top: 45%;
      left: -35%;
      background: #0eaffe;;
      opacity: 0.5;
      width: 200%;
      height: 200%;
      border-radius: 35%;
      animation: inherit;
      animation-duration: 7s;
    }
  
    .water-wave3 {
      position: absolute;
      top: 50%;
      left: -35%;
      background: #0f7ae4;
      opacity: 0.3;
      width: 200%;
      height: 200%;
      border-radius: 33%;
      animation: inherit;
      animation-duration: 11s;
    }
  
    @keyframes water-waves {
      0% {
        transform: rotate(0deg);
      }
      100% {
        transform: rotate(360deg);
      }
    }
  </style>
  
  <div class="water-waves">
    <div class="water-wave1"></div>
    <div class="water-wave2"></div>
    <div class="water-wave3"></div>
    水波效果
  </div>

================================================
FILE: server/app/controller/home.js
================================================
// app/controller/home.js
const path = require('path')
const fse = require("fs-extra")

const Controller = require('egg').Controller;

class HomeController extends Controller {
  async index() {
    this.ctx.body = {
      msg:'hello eggjs'
    }
  }
  async merge(){
    const {ext,size,hash} = this.ctx.request.body
    const filePath = path.resolve(this.config.UPLOAD_DIR, `${hash}.${ext}`)
    await this.ctx.service.upload.mergeFileChunk(filePath, hash, size)
    this.ctx.body = {
      code:0,
      msg:'合并成功'
    }

  }
  async getUploadedList(dirPath){
    return fse.existsSync(dirPath) 
      ? (await fse.readdir(dirPath)).filter(name=>name[0]!=='.') // 过滤诡异的隐藏文件 比如.DS_store
      : []
  }
  async check(){
    const { ext, hash } = this.ctx.request.body
    const filePath = path.resolve(this.config.UPLOAD_DIR, `${hash}.${ext}`)
    console.log(filePath)
    // 文件是否存在
    let uploaded = false
    let uploadedList = []
    if (fse.existsSync(filePath)) {
      // 存在文件,直接返回已上传
      uploaded = true
    }else{
      // 文件没有完全上传完毕,但是可能存在部分切片上传完毕了
      uploadedList = await this.getUploadedList(path.resolve(this.config.UPLOAD_DIR, hash))
    }

    this.ctx.body = {
      code:0,
      uploaded,
      uploadedList // 过滤诡异的隐藏文件
    }
  } 
  async upload(){
    const { ctx } = this
    if(Math.random()<0.5){
      // 随机报个错
      return ctx.status = 500;
    }
    const file = ctx.request.files[0]
    const {chunkname,ext,hash} = ctx.request.body
    console.log(file,hash,chunkname,ext)
    const filename = `${hash}.${ext}`
    // 最终文件存储位置 根据chunkname获取后缀,名字用hash
    const filePath = path.resolve(
      this.config.UPLOAD_DIR,
      filename
    )
    // 碎片文件夹,用hash命名
    const chunkPath = path.resolve(this.config.UPLOAD_DIR, hash )

    // 文件存在直接返回
    if (fse.existsSync(filePath)) {
      this.ctx.body = {
        code:-1,
        msg:'文件存在',
        url:`/public/${filename}`
      }
      return
    }
    if (!fse.existsSync(this.config.UPLOAD_DIR)) {
      await fse.mkdirs(this.config.UPLOAD_DIR)
    }
    await fse.move(file.filepath, `${chunkPath}/${chunkname}`)
    this.ctx.body={
      code:0,
      msg:'上传成功',
      url:`/public/${filename}`
    }


  }
}

module.exports = HomeController;

================================================
FILE: server/app/router.js
================================================
// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.get('/index', controller.home.index);
  router.post('/upload', controller.home.upload);
  router.post('/merge', controller.home.merge);
  router.post('/check', controller.home.check);
};

================================================
FILE: server/app/service/upload.js
================================================
// app/service/upload.js
const path = require('path')
const fse = require('fs-extra')
const Service = require('egg').Service;

class UploadService extends Service {

  extractExt(filename) {
    return filename.slice(filename.lastIndexOf("."), filename.length)
  }
  async mergeFiles(files, dest, size) {
    const pipeStream = (filePath, writeStream) =>
      new Promise(resolve => {
        const readStream = fse.createReadStream(filePath)
        readStream.on("end", () => {
          // 删除文件
          fse.unlinkSync(filePath)
          resolve()
        })
        readStream.pipe(writeStream)
      })

    await Promise.all(
      files.map((file, index) =>
        pipeStream(
          file,
          // 指定位置创建可写流 加一个put避免文件夹和文件重名
          // hash后不存在这个问题,因为文件夹没有后缀
          // fse.createWriteStream(path.resolve(dest, '../', 'out' + filename), {
          fse.createWriteStream(dest, {
            start: index * size,
            end: (index + 1) * size
          })
        )
      )
    )

  }

  async mergeFileChunk(filePath, fileHash, size){
    const chunkDir = path.resolve(this.config.UPLOAD_DIR, fileHash)
    let chunkPaths = await fse.readdir(chunkDir)
    // 根据切片下标进行排序
    // 否则直接读取目录的获得的顺序可能会错乱
    chunkPaths
      .sort((a, b) => a.split("-")[1] - b.split("-")[1])
    chunkPaths = chunkPaths.map(cp => path.resolve(chunkDir, cp)) // 转成文件路径
    await this.mergeFiles(chunkPaths, filePath, size)
  }
}
module.exports = UploadService;

================================================
FILE: server/config/config.default.js
================================================
// config/config.default.js
const path = require('path')
module.exports = appInfo => {
  const config = {};
  config.keys = 'shengxinjign@!#rocks!';
  // config.middleware = ['cors'];
  config.multipart = {
    mode: 'file',
    // multipart: {
    // },
      whitelist: ()=>true


  }
  config.security = {
    csrf: {
      enable: false
    },
  }
  config.UPLOAD_DIR = path.resolve(__dirname, "..", "app/public"); // 大文件存储目录

  return config;
};

================================================
FILE: server/package.json
================================================
{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "egg-bin dev",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "egg": "^2.26.0",
    "egg-bin": "^4.14.1",
    "egg-cors": "^2.2.3",
    "fs-extra": "^8.1.0"
  }
}
Download .txt
gitextract_lxtez43b/

├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── client/
│   ├── .gitignore
│   ├── README.md
│   ├── babel.config.js
│   ├── package.json
│   ├── public/
│   │   ├── hash.js
│   │   └── index.html
│   ├── src/
│   │   ├── App.vue
│   │   ├── components/
│   │   │   └── HelloWorld.vue
│   │   ├── main.js
│   │   └── plugins/
│   │       └── element.js
│   └── vue.config.js
├── index.html
└── server/
    ├── app/
    │   ├── controller/
    │   │   └── home.js
    │   ├── router.js
    │   └── service/
    │       └── upload.js
    ├── config/
    │   └── config.default.js
    └── package.json
Download .txt
SYMBOL INDEX (10 symbols across 2 files)

FILE: server/app/controller/home.js
  class HomeController (line 7) | class HomeController extends Controller {
    method index (line 8) | async index() {
    method merge (line 13) | async merge(){
    method getUploadedList (line 23) | async getUploadedList(dirPath){
    method check (line 28) | async check(){
    method upload (line 49) | async upload(){

FILE: server/app/service/upload.js
  class UploadService (line 6) | class UploadService extends Service {
    method extractExt (line 8) | extractExt(filename) {
    method mergeFiles (line 11) | async mergeFiles(files, dest, size) {
    method mergeFileChunk (line 40) | async mergeFileChunk(filePath, fileHash, size){
Condensed preview — 21 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (35K chars).
[
  {
    "path": ".editorconfig",
    "chars": 121,
    "preview": "[*.{js,jsx,ts,tsx,vue}]\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\ninsert_final_newline = true"
  },
  {
    "path": ".gitignore",
    "chars": 1665,
    "preview": "# Logs\nlogs\napp/run\nserver/app/public/\nserver/app/run/\nserver/run/\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2020 woniuppp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 130,
    "preview": "# file-upload\n面试造火箭系列\n\n\n 1. vue create client\n 2. cd client && vue add element\n 3. mkdir server\n 4. npm install egg egg-"
  },
  {
    "path": "client/.gitignore",
    "chars": 214,
    "preview": ".DS_Store\nnode_modules\n/dist\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn"
  },
  {
    "path": "client/README.md",
    "chars": 359,
    "preview": "# client\n\n## Project setup\n```\nnpm install\n```\n\n### Compiles and hot-reloads for development\n```\nnpm run serve\n```\n\n### "
  },
  {
    "path": "client/babel.config.js",
    "chars": 73,
    "preview": "module.exports = {\n  presets: [\n    '@vue/cli-plugin-babel/preset'\n  ]\n}\n"
  },
  {
    "path": "client/package.json",
    "chars": 1096,
    "preview": "{\n  \"name\": \"client\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vue-cli-service serve\",\n    "
  },
  {
    "path": "client/public/hash.js",
    "chars": 976,
    "preview": "\n\n// web-worker\nself.importScripts('spark-md5.min.js')\n\nself.onmessage = e=>{\n    // 接受主线程的通知\n    const {chunks} = e.dat"
  },
  {
    "path": "client/public/index.html",
    "chars": 613,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE="
  },
  {
    "path": "client/src/App.vue",
    "chars": 17100,
    "preview": "<template>\n  <div class=\"app\">\n\n    <input type=\"text\">\n    <i class=\"el-icon-loading\" style=\"color:#F56C6C;\"></i>\n\n    "
  },
  {
    "path": "client/src/components/HelloWorld.vue",
    "chars": 541,
    "preview": "<template>\n  <div class=\"hello\">\n    <h1>{{ name }}</h1>\n    <hr>\n    <h1>{{ type }}</h1>\n        <hr>\n    <h1>{{ age }}"
  },
  {
    "path": "client/src/main.js",
    "chars": 435,
    "preview": "import Vue from 'vue'\nimport App from './App.vue'\nimport axios from 'axios'\nimport './plugins/element.js'\n\nlet request ="
  },
  {
    "path": "client/src/plugins/element.js",
    "chars": 119,
    "preview": "import Vue from 'vue'\nimport Element from 'element-ui'\nimport 'element-ui/lib/theme-chalk/index.css'\n\nVue.use(Element)\n"
  },
  {
    "path": "client/vue.config.js",
    "chars": 197,
    "preview": "module.exports = {\n  devServer: {\n    proxy:{\n      '/api/':{\n        target:'http://localhost:7001',\n        secure:fal"
  },
  {
    "path": "index.html",
    "chars": 1384,
    "preview": "<style>\n    .water-waves {\n      margin: 0 auto;\n      overflow: hidden;\n      position: relative;\n      width: 100px;\n "
  },
  {
    "path": "server/app/controller/home.js",
    "chars": 2232,
    "preview": "// app/controller/home.js\nconst path = require('path')\nconst fse = require(\"fs-extra\")\n\nconst Controller = require('egg'"
  },
  {
    "path": "server/app/router.js",
    "chars": 276,
    "preview": "// app/router.js\nmodule.exports = app => {\n  const { router, controller } = app;\n  router.get('/index', controller.home."
  },
  {
    "path": "server/app/service/upload.js",
    "chars": 1465,
    "preview": "// app/service/upload.js\nconst path = require('path')\nconst fse = require('fs-extra')\nconst Service = require('egg').Ser"
  },
  {
    "path": "server/config/config.default.js",
    "chars": 450,
    "preview": "// config/config.default.js\nconst path = require('path')\nmodule.exports = appInfo => {\n  const config = {};\n  config.key"
  },
  {
    "path": "server/package.json",
    "chars": 372,
    "preview": "{\n  \"name\": \"server\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"egg-"
  }
]

About this extraction

This page contains the full source code of the shengxinjing/file-upload GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 21 files (30.2 KB), approximately 9.0k tokens, and a symbol index with 10 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!