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

[

tobychui

](/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上传(减少开销,最快)

  1. 否则,如果文件小于“tmp 上的剩余空间”/16 - 1KB,文件被缓冲到 tmp 文件夹中(tmp 文件夹应该在 NVME SSD 或 RAM Disk 等快速介质中,比 FORM POST 慢但仍然快速地)

  2. 否则,文件块直接缓冲到磁盘(最慢,但为我们提供最多的工作空间)

文件合并程序

在之前的实现中,文件合并过程是这样发生的

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 或任何提供大文件存储的云服务提供商。但是如果你觉得它有用或者你有更好的实现,请随时告诉我,以便我们进一步改进设计:)

GitHub 徽标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 上查看

Logo

更多推荐