Skip to content

Commit 7589bfd

Browse files
author
ym
committed
实现分片上传功能,添加上传进度显示,优化文件路径处理
1 parent e6fa07a commit 7589bfd

3 files changed

Lines changed: 246 additions & 83 deletions

File tree

assets/templates/index.tmpl

Lines changed: 137 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -43,59 +43,149 @@
4343
<a href="#" onclick="createNewFile()">{{t "nFile"}}</a>
4444
<a href="#" onclick="createNewDir()">{{t "nDir"}}</a>
4545
<h3>{{t "cUpFile"}}</h3>
46-
<form id="uploadForm" action="/do/upload/{{.path}}" method="post" enctype="multipart/form-data">
46+
<form id="uploadForm" enctype="multipart/form-data">
4747
<input type="file" name="file" id="fileInput">
4848
</form>
49+
<div id="upload-progress" style="display:none;margin-top:10px;">
50+
<div style="font-size:13px;margin-bottom:4px;" id="upload-filename"></div>
51+
<div style="background:#ddd;border-radius:4px;height:16px;width:100%;overflow:hidden;">
52+
<div id="upload-bar" style="background:#337ab7;height:16px;width:0%;transition:width 0.15s;"></div>
53+
</div>
54+
<div style="font-size:12px;margin-top:4px;color:#555;" id="upload-percent">0%</div>
55+
</div>
4956
<script src="https://s3.pstatp.com/cdn/expire-1-M/jquery/3.2.1/jquery.min.js"></script>
5057
<script>
51-
//拖放上传文件 start
52-
// 获取 body 元素
53-
const body = document.querySelector('body');
58+
var CHUNK_SIZE = 2 * 1024 * 1024; // 2MB per chunk
5459

55-
// 监听文件拖放事件
56-
body.addEventListener('drop', (event) => {
57-
event.preventDefault();
58-
event.stopPropagation();
60+
function getFileId(file) {
61+
return file.name + '_' + file.size + '_' + file.lastModified;
62+
}
63+
64+
function showProgress(filename) {
65+
document.getElementById('upload-progress').style.display = 'block';
66+
document.getElementById('upload-filename').textContent = filename;
67+
updateProgress(0);
68+
}
69+
70+
function updateProgress(percent) {
71+
var p = Math.min(100, Math.round(percent));
72+
document.getElementById('upload-bar').style.width = p + '%';
73+
document.getElementById('upload-percent').textContent = p + '%';
74+
}
75+
76+
function hideProgress() {
77+
document.getElementById('upload-progress').style.display = 'none';
78+
}
79+
80+
function checkChunks(fileId, totalChunks) {
81+
return fetch('/do/chunk/check', {
82+
method: 'POST',
83+
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
84+
body: 'fileId=' + encodeURIComponent(fileId) + '&totalChunks=' + totalChunks
85+
}).then(function(r) { return r.json(); })
86+
.then(function(d) { return d.uploaded || []; })
87+
.catch(function() { return []; });
88+
}
5989

60-
// 获取拖放的文件
61-
const file = event.dataTransfer.files[0];
90+
function uploadChunk(fileId, chunkIndex, totalChunks, chunk, onProgress) {
91+
return new Promise(function(resolve, reject) {
92+
var formData = new FormData();
93+
formData.append('fileId', fileId);
94+
formData.append('chunkIndex', chunkIndex);
95+
formData.append('totalChunks', totalChunks);
96+
formData.append('file', chunk);
97+
var xhr = new XMLHttpRequest();
98+
xhr.upload.addEventListener('progress', function(e) {
99+
if (e.lengthComputable) onProgress(e.loaded / e.total);
100+
});
101+
xhr.onload = function() {
102+
try {
103+
var data = JSON.parse(xhr.responseText);
104+
data.stat ? resolve() : reject(new Error('chunk upload failed'));
105+
} catch(e) { reject(e); }
106+
};
107+
xhr.onerror = function() { reject(new Error('network error')); };
108+
xhr.open('POST', '/do/chunk/upload');
109+
xhr.send(formData);
110+
});
111+
}
112+
113+
function mergeChunks(fileId, totalChunks, fileName, path) {
114+
return fetch('/do/chunk/merge', {
115+
method: 'POST',
116+
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
117+
body: 'fileId=' + encodeURIComponent(fileId) +
118+
'&totalChunks=' + totalChunks +
119+
'&fileName=' + encodeURIComponent(fileName) +
120+
'&path=' + encodeURIComponent(path)
121+
}).then(function(r) { return r.json(); })
122+
.then(function(d) { return d.stat; });
123+
}
62124

63-
// 判断拖放的元素是否为文件,如果是,则模拟表单提交
64-
if (file) {
65-
// 创建 form 元素
66-
const form = document.createElement('form');
67-
form.setAttribute('method', 'post');
68-
form.setAttribute('enctype', 'multipart/form-data');
69-
form.setAttribute('action', '/do/upload/{{.path}}');
125+
async function uploadFile(file, uploadPath) {
126+
var fileId = getFileId(file);
127+
var totalChunks = Math.ceil(file.size / CHUNK_SIZE) || 1;
128+
showProgress(file.name);
70129

71-
// 创建 file input 元素
72-
const fileInput = document.createElement('input');
73-
fileInput.setAttribute('type', 'file');
74-
fileInput.setAttribute('name', 'file');
75-
fileInput.files = event.dataTransfer.files;
130+
var uploadedList = await checkChunks(fileId, totalChunks);
131+
var uploadedSet = new Set(uploadedList);
132+
var completedChunks = uploadedList.length;
76133

77-
// 创建 submit input 元素
78-
const submitInput = document.createElement('input');
79-
submitInput.setAttribute('type', 'submit');
80-
submitInput.setAttribute('value', '{{t "submit"}}');
81-
// 添加元素到 form 中
82-
form.appendChild(fileInput);
83-
form.appendChild(submitInput);
84-
// 添加 form 到 body 中
85-
body.appendChild(form);
86-
// 提交表单
87-
form.submit();
134+
for (var i = 0; i < totalChunks; i++) {
135+
if (uploadedSet.has(i)) {
136+
updateProgress(completedChunks / totalChunks * 100);
137+
continue;
138+
}
139+
var chunk = file.slice(i * CHUNK_SIZE, Math.min((i + 1) * CHUNK_SIZE, file.size));
140+
var capturedI = i;
141+
try {
142+
await uploadChunk(fileId, capturedI, totalChunks, chunk, function(chunkProg) {
143+
updateProgress((completedChunks + chunkProg) / totalChunks * 100);
144+
});
145+
completedChunks++;
146+
updateProgress(completedChunks / totalChunks * 100);
147+
} catch(e) {
148+
hideProgress();
149+
alert('上传失败:' + e.message);
150+
return;
151+
}
88152
}
89-
});
90153

91-
// 防止浏览器默认行为
92-
body.addEventListener('dragover', (event) => {
154+
updateProgress(100);
155+
try {
156+
var ok = await mergeChunks(fileId, totalChunks, file.name, uploadPath);
157+
hideProgress();
158+
if (ok) {
159+
if (confirm('上传成功,点击确认刷新')) location.reload();
160+
} else {
161+
alert('文件合并失败');
162+
}
163+
} catch(e) {
164+
hideProgress();
165+
alert('合并请求失败');
166+
}
167+
}
168+
169+
// 拖放上传
170+
var body = document.querySelector('body');
171+
body.addEventListener('drop', function(event) {
172+
event.preventDefault();
173+
event.stopPropagation();
174+
var file = event.dataTransfer.files[0];
175+
if (file) uploadFile(file, '{{.path}}');
176+
});
177+
body.addEventListener('dragover', function(event) {
93178
event.preventDefault();
94179
event.stopPropagation();
95180
});
96-
//拖放上传文件 end
181+
182+
// 文件选择上传
183+
document.getElementById('fileInput').addEventListener('change', function() {
184+
if (this.files.length > 0) uploadFile(this.files[0], '{{.path}}');
185+
});
186+
97187
function createNewDir() {
98-
const dirName = window.prompt("请输入文件夹名:");
188+
var dirName = window.prompt("请输入文件夹名:");
99189
if (dirName !== null) {
100190
$.post("/do/newdir",{path:{{.path}},dirname:dirName},function(data,status){
101191
if(status == "success"){
@@ -112,7 +202,7 @@
112202
}
113203
}
114204
function createNewFile() {
115-
const fileName = window.prompt("请输入文件名:");
205+
var fileName = window.prompt("请输入文件名:");
116206
if (fileName !== null) {
117207
$.post("/do/newfile",{path:{{.path}},filename:fileName},function(data,status){
118208
if(status == "success"){
@@ -161,48 +251,21 @@
161251
}
162252
}
163253
function edite(path) {
164-
var temp = document.createElement("form"); //创建form表单
254+
var temp = document.createElement("form");
165255
temp.action = "/edite";
166256
temp.method = "post";
167-
temp.target = "_blank"
168-
temp.style.display = "none";//表单样式为隐藏
169-
var opt =document.createElement("input"); //添加input标签
170-
opt.type="text"; //类型为text
171-
opt.name = "path"; //设置name属性
172-
opt.value = path; //设置value属性
257+
temp.target = "_blank";
258+
temp.style.display = "none";
259+
var opt = document.createElement("input");
260+
opt.type = "text";
261+
opt.name = "path";
262+
opt.value = path;
173263
temp.appendChild(opt);
174264
document.body.appendChild(temp);
175265
temp.submit();
176266
return temp;
177267
}
178-
document.getElementById('fileInput').addEventListener('change', function() {
179-
if (this.files.length > 0) {
180-
// 创建一个FormData对象
181-
var formData = new FormData();
182-
formData.append('file', this.files[0]);
183-
184-
// 创建一个XMLHttpRequest对象
185-
var xhr = new XMLHttpRequest();
186-
187-
// 设置请求方法和上传文件的URL
188-
xhr.open('POST', '/do/upload/{{.path}}', true);
189-
190-
// 设置请求完成的处理函数
191-
xhr.onload = function () {
192-
if (xhr.status === 200) {
193-
alert('文件上传成功');
194-
location.reload(); // 或者其他的逻辑,比如显示上传的文件
195-
} else {
196-
alert('文件上传失败');
197-
}
198-
};
199-
200-
// 发送FormData对象
201-
xhr.send(formData);
202-
}
203-
});
204-
205268
</script>
206269
{{ end }}
207270
</body>
208-
</html>
271+
</html>

main.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os"
1414
"os/exec"
1515
"path/filepath"
16+
"strconv"
1617
"strings"
1718

1819
"github.com/gin-gonic/gin"
@@ -76,6 +77,17 @@ func urlForPath(cPath string) string {
7677
return "/d/" + cPath
7778
}
7879

80+
// chunkDir returns the temp directory for storing upload chunks.
81+
// Returns empty string if fileId is invalid (path traversal attempt).
82+
func chunkDir(fileId string) string {
83+
base := filepath.Join(os.TempDir(), "goFile-chunks")
84+
dir := filepath.Clean(filepath.Join(base, fileId))
85+
if !strings.HasPrefix(dir, base+string(filepath.Separator)) {
86+
return ""
87+
}
88+
return dir
89+
}
90+
7991
// Web Serve
8092
func web() {
8193
initTemplates()
@@ -162,6 +174,97 @@ func web() {
162174
})
163175
})
164176

177+
// 分片上传 - 查询已上传分片
178+
r.POST("/do/chunk/check", func(c *gin.Context) {
179+
fileId := c.PostForm("fileId")
180+
dir := chunkDir(fileId)
181+
if dir == "" {
182+
c.JSON(http.StatusOK, gin.H{"uploaded": []int{}})
183+
return
184+
}
185+
totalChunks, _ := strconv.Atoi(c.PostForm("totalChunks"))
186+
uploaded := []int{}
187+
for i := 0; i < totalChunks; i++ {
188+
if utils.Exist(filepath.Join(dir, strconv.Itoa(i))) {
189+
uploaded = append(uploaded, i)
190+
}
191+
}
192+
c.JSON(http.StatusOK, gin.H{"uploaded": uploaded})
193+
})
194+
195+
// 分片上传 - 上传单个分片
196+
r.POST("/do/chunk/upload", func(c *gin.Context) {
197+
fileId := c.PostForm("fileId")
198+
dir := chunkDir(fileId)
199+
if dir == "" {
200+
c.JSON(http.StatusOK, gin.H{"stat": false})
201+
return
202+
}
203+
chunkIndex, err := strconv.Atoi(c.PostForm("chunkIndex"))
204+
if err != nil {
205+
c.JSON(http.StatusOK, gin.H{"stat": false})
206+
return
207+
}
208+
if err := os.MkdirAll(dir, 0755); err != nil {
209+
c.JSON(http.StatusOK, gin.H{"stat": false})
210+
return
211+
}
212+
file, err := c.FormFile("file")
213+
if err != nil {
214+
c.JSON(http.StatusOK, gin.H{"stat": false})
215+
return
216+
}
217+
if err := c.SaveUploadedFile(file, filepath.Join(dir, strconv.Itoa(chunkIndex))); err != nil {
218+
c.JSON(http.StatusOK, gin.H{"stat": false})
219+
return
220+
}
221+
c.JSON(http.StatusOK, gin.H{"stat": true})
222+
})
223+
224+
// 分片上传 - 合并分片为最终文件
225+
r.POST("/do/chunk/merge", func(c *gin.Context) {
226+
fileId := c.PostForm("fileId")
227+
dir := chunkDir(fileId)
228+
if dir == "" {
229+
c.JSON(http.StatusOK, gin.H{"stat": false})
230+
return
231+
}
232+
totalChunks, err := strconv.Atoi(c.PostForm("totalChunks"))
233+
if err != nil || totalChunks <= 0 {
234+
c.JSON(http.StatusOK, gin.H{"stat": false})
235+
return
236+
}
237+
destDir := filepath.Join(conf.GoFile, c.PostForm("path"))
238+
if !utils.IsPathSafe(destDir) {
239+
c.JSON(http.StatusOK, gin.H{"stat": false})
240+
return
241+
}
242+
destPath := filepath.Join(destDir, filepath.Base(c.PostForm("fileName")))
243+
out, err := os.Create(destPath)
244+
if err != nil {
245+
c.JSON(http.StatusOK, gin.H{"stat": false})
246+
return
247+
}
248+
defer func() {
249+
out.Close()
250+
os.RemoveAll(dir)
251+
}()
252+
for i := 0; i < totalChunks; i++ {
253+
chunk, err := os.Open(filepath.Join(dir, strconv.Itoa(i)))
254+
if err != nil {
255+
c.JSON(http.StatusOK, gin.H{"stat": false})
256+
return
257+
}
258+
_, copyErr := io.Copy(out, chunk)
259+
chunk.Close()
260+
if copyErr != nil {
261+
c.JSON(http.StatusOK, gin.H{"stat": false})
262+
return
263+
}
264+
}
265+
c.JSON(http.StatusOK, gin.H{"stat": true})
266+
})
267+
165268
// 新建文件
166269
r.POST("/do/newfile", func(c *gin.Context) {
167270
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")

0 commit comments

Comments
 (0)