在 Go 中上传一个 RAM 和空间很小的大文件
这是先前实现的更新版本,关于在 Go 中上传大于 RAM 大小的文件。如果您之前没有阅读过,可以在以下链接中查看。
[

](/tobychui)[
在 Go 中上传大于 RAM 大小的文件
Toby Chui ・ 21 年 1 月 15 日 ・ 5 分钟阅读
#go #raspberrypi #upload #filesystem
](/tobychui/upload-a-file-larger-than-ram-size-in-go-4m2i)
在上一篇博文中,我使用 websocket 文件分块实现解决了 Go 上传方法,以处理上传文件远大于设备上可用 RAM 的情况。当您为一些 RAM 低至 512MB 的廉价 SBC 开发应用程序时,此实现非常有用。
最近,我在尝试将整个 Google Drive 迁移到我自己的 ARM 驱动的 DIY NAS 时遇到了另一个问题。问题是我的 NAS 只有 512MB + 32GB(microSD 卡)作为操作系统驱动器,而我在 SBC 上连接了 2 个 512GB 硬盘来存储文件。上传大小 >32GB 的文件将导致系统空间不足并导致我的ArozOS NAS OS崩溃。
在之前的实现中,为了上传一个 1GB 的文件,你需要在你的 tmp 文件夹(即 SD 卡)中有 1GB 的空间来缓冲通过 websocket 接收到的文件块。在最新的实现中,添加了一个新的“大文件模式”来处理上传文件 > tmp 文件夹空间的情况,方法是直接将上传文件块写入目标磁盘,同时最大限度地减少所有系统磁盘上所需的最大空间。在我向您展示其工作原理的代码之前,这是我决定何时进入“大文件模式”的逻辑
优化上传空间和时间占用的逻辑
1.如果文件小于4MB,用FORM POST上传(减少开销,最快)
-
否则,如果文件小于“tmp 上的剩余空间”/16 - 1KB,文件被缓冲到 tmp 文件夹中(tmp 文件夹应该在 NVME SSD 或 RAM Disk 等快速介质中,比 FORM POST 慢但仍然快速地)
-
否则,文件块直接缓冲到磁盘(最慢,但为我们提供最多的工作空间)
文件合并程序
在之前的实现中,文件合并过程是这样发生的
1.创建目标文件并打开它
2.遍历每个块,将其附加到打开的目标文件
3.删除所有chunk文件
*但是,这将占用上传文件空间的 2 倍。 * 它适用于中等大小的文件,但不适用于大文件。为了解决这个问题,我将实现更改为以下内容。
1.创建定义文件并打开
2.遍历每个块,将每个块附加到打开的目标文件,确认复制成功并删除源块
简单来说,通过动态删除文件,新的上传逻辑只占用 (x + c) 个字节大小,其中 x 是文件大小,c 是块大小。在我的设计中,c 是 512KB。
代码
前端代码没有变化,只是在打开 websocket 时多了一个 GET 参数来定义当前上传是否为大文件上传。以下是 websocket 对象的示例实现
let hugeFileMode = "";
if (file.size > largeFileCutoffSize){
//Filesize over cutoff line. Use huge file mode
hugeFileMode = "&hugefile=true";
}
let socket = new WebSocket(protocol + window.location.hostname + ":" + port + "/system/file_system/lowmemUpload?filename=" + encodeURIComponent(filename) + "&path=" + encodeURIComponent(uploadDir) + hugeFileMode);
进入全屏模式 退出全屏模式
这是 Go 后端的实现。注意
isHugeFile flag and //合并文件部分。
targetUploadLocation := filepath.Join(uploadPath, filename)
if !fs.FileExists(uploadPath) {
os.MkdirAll(uploadPath, 0755)
}
//Generate an UUID for this upload
uploadUUID := uuid.NewV4().String()
uploadFolder := filepath.Join(*tmp_directory, "uploads", uploadUUID)
if isHugeFile {
//Upload to the same directory as the target location.
uploadFolder = filepath.Join(uploadPath, ".metadata/.upload", uploadUUID)
}
os.MkdirAll(uploadFolder, 0700)
//Start websocket connection
var upgrader = websocket.Upgrader{}
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Failed to upgrade websocket connection: ", err.Error())
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 WebSocket upgrade failed"))
return
}
defer c.Close()
//Handle WebSocket upload
blockCounter := 0
chunkName := []string{}
lastChunkArrivalTime := time.Now().Unix()
//Setup a timeout listener, check if connection still active every 1 minute
ticker := time.NewTicker(60 * time.Second)
done := make(chan bool)
go func() {
for {
select {
case <-done:
return
case <-ticker.C:
if time.Now().Unix()-lastChunkArrivalTime > 300 {
//Already 5 minutes without new data arraival. Stop connection
log.Println("Upload WebSocket connection timeout. Disconnecting.")
c.WriteControl(8, []byte{}, time.Now().Add(time.Second))
time.Sleep(1 * time.Second)
c.Close()
return
}
}
}
}()
totalFileSize := int64(0)
for {
mt, message, err := c.ReadMessage()
if err != nil {
//Connection closed by client. Clear the tmp folder and exit
log.Println("Upload terminated by client. Cleaning tmp folder.")
//Clear the tmp folder
time.Sleep(1 * time.Second)
os.RemoveAll(uploadFolder)
return
}
//The mt should be 2 = binary for file upload and 1 for control syntax
if mt == 1 {
msg := strings.TrimSpace(string(message))
if msg == "done" {
//Start the merging process
break
} else {
//Unknown operations
}
} else if mt == 2 {
//File block. Save it to tmp folder
chunkFilepath := filepath.Join(uploadFolder, "upld_"+strconv.Itoa(blockCounter))
chunkName = append(chunkName, chunkFilepath)
writeErr := ioutil.WriteFile(chunkFilepath, message, 0700)
if writeErr != nil {
//Unable to write block. Is the tmp folder fulled?
log.Println("[Upload] Upload chunk write failed: " + err.Error())
c.WriteMessage(1, []byte(`{\"error\":\"Write file chunk to disk failed\"}`))
//Close the connection
c.WriteControl(8, []byte{}, time.Now().Add(time.Second))
time.Sleep(1 * time.Second)
c.Close()
//Clear the tmp files
os.RemoveAll(uploadFolder)
return
}
//Update the last upload chunk time
lastChunkArrivalTime = time.Now().Unix()
//Check if the file size is too big
totalFileSize += fs.GetFileSize(chunkFilepath)
if totalFileSize > max_upload_size {
//File too big
c.WriteMessage(1, []byte(`{\"error\":\"File size too large\"}`))
//Close the connection
c.WriteControl(8, []byte{}, time.Now().Add(time.Second))
time.Sleep(1 * time.Second)
c.Close()
//Clear the tmp files
os.RemoveAll(uploadFolder)
return
}
blockCounter++
//Request client to send the next chunk
c.WriteMessage(1, []byte("next"))
}
}
//Try to decode the location if possible
decodedUploadLocation, err := url.QueryUnescape(targetUploadLocation)
if err != nil {
decodedUploadLocation = targetUploadLocation
}
//Do not allow % sign in filename. Replace all with underscore
decodedUploadLocation = strings.ReplaceAll(decodedUploadLocation, "%", "_")
//Merge the file
out, err := os.OpenFile(decodedUploadLocation, os.O_CREATE|os.O_WRONLY, 0755)
if err != nil {
log.Println("Failed to open file:", err)
c.WriteMessage(1, []byte(`{\"error\":\"Failed to open destination file\"}`))
c.WriteControl(8, []byte{}, time.Now().Add(time.Second))
time.Sleep(1 * time.Second)
c.Close()
return
}
for _, filesrc := range chunkName {
srcChunkReader, err := os.Open(filesrc)
if err != nil {
log.Println("Failed to open Source Chunk", filesrc, " with error ", err.Error())
c.WriteMessage(1, []byte(`{\"error\":\"Failed to open Source Chunk\"}`))
return
}
io.Copy(out, srcChunkReader)
srcChunkReader.Close()
//Delete file immediately to save space
os.Remove(filesrc)
}
out.Close()
//Return complete signal
c.WriteMessage(1, []byte("OK"))
//Stop the timeout listner
done <- true
//Clear the tmp folder
time.Sleep(300 * time.Millisecond)
err = os.RemoveAll(uploadFolder)
if err != nil {
log.Println(err)
}
//Close WebSocket connection after finished
c.WriteControl(8, []byte{}, time.Now().Add(time.Second))
time.Sleep(300 * time.Second)
c.Close()
进入全屏模式 退出全屏模式
现在你可以拥有无限的文件上传大小
所以你有它。现在,只要您有足够的磁盘空间来存储它,您就可以将无限大的文件上传到您的系统中。 注意,这种上传方式非常慢。由于读取文件块和写入目标文件都在同一个磁盘上,因此实际合并文件的速度是前一种方法的 2 倍以上。 但是对于我的用例,至少它对于文件来说足够好太大而无法放入系统 RAM 或 tmp/ 文件夹。
除了我在 ArozOS 项目上工作之外,我不知道还有谁会发现这很有用。现在有这些问题的人通常只是将文件转储到 AWS 或任何提供大文件存储的云服务提供商。但是如果你觉得它有用或者你有更好的实现,请随时告诉我,以便我们进一步改进设计:)
tobychui/arozos
用于 Raspberry Pis 的通用 Web 桌面操作平台/操作系统,现在用 Go 编写!

[
](https ://camo.githubusercontent.com/69366188f79ae03c7ff41b134a02cf48b9f73d23ba59e4335fd20a153318c201/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d616465253230496e253230486f6e672532304b6f6e672de9a699e6b8afe9968be799bc2d626c756576696f6c6574)
重要提示
目前的arozos仍在紧张开发中。系统结构随时可能发生变化。请仅在当前现有的 ArOZ 网关接口 (AGI) JavaScript 接口或带有 ao_module.js 端点的标准 HTML webapps 上进行开发。
特点
用户界面
-
Web 桌面界面(优于 Synology DSM)
-
Ubuntu remix Windows 风格的启动菜单和任务栏
-
干净易用的文件管理器(支持拖放、上传等)
-
简单系统设置菜单
-
没有废话的模块命名方案
联网
-
FTP 服务器
-
静态网页服务器
-
WebDAV 服务器
-
UPnP 端口转发
-
Samba(通过第 3 方子服务支持)
-
WiFi 管理(支持 Rpi 的 wpa_supplicant 或 Armbian 的 nmcli)
文件/磁盘管理
-
安装/格式化磁盘实用程序(支持 NTFS、EXT4 等!)
-
虚拟文件系统架构
-
文件共享(类似于 Google Drive)
-
具有实时进度的基本文件操作(复制/剪切/粘贴/新建文件或文件夹等)
可扩展性
-
ECMA5(类似 JavaScript)脚本接口
-
...
在 GitHub 上查看
更多推荐

所有评论(0)