内存网盘 - Go语言实现的WebDAV内存文件系统
这个 Go 小玩具,让内存秒变网盘!关机就消失,重命名随便改,还能自动挂成 Z 盘——这不是魔法,是 Go 写的 WebDAV 内存文件系统!
你有没有想过:如果有一个网盘,不用注册、不占硬盘、速度飞快,但只要一关程序,所有数据就"人间蒸发"?听起来像间谍工具?其实它只是个用 Go 写的小玩具——而且现在,它还能让你在 Windows 资源管理器里直接重命名文件,就像操作本地文件一样丝滑!
今天,我们就来揭开这个"内存网盘"的神秘面纱。放心,代码不多,笑点管够,原理讲透,连你家猫都能看懂(大概)。
🎯 功能特性
- ✅ 所有文件存在内存里(RAM),关进程就清零,干净得像没来过
- ✅ 支持完整的 WebDAV 协议:新建、删除、读写、建文件夹……
- ✅ 新增重磅功能:支持重命名文件/文件夹!拖拽、右键改名统统 OK
- ✅ 启动时自动在 Windows 上映射为 Z: 盘(可自定义),像本地磁盘一样用!
- ✅ 自带 README.txt 彩蛋,内含作者签名(不是病毒,真的!)
适用场景
- 临时共享
- 快速测试
- 演示环境
🛠️ 技术原理
1. 内存文件系统 = 一棵树
我们用 Go 的 map[string]*File 模拟文件目录结构,每个 File 可以是普通文件或目录。整棵树从根节点 / 开始,长得像这样:
/
└── README.txt
所有操作(创建、读取、删除、重命名)都是在这棵树上"修枝剪叶"。
2. WebDAV = HTTP 的"文件操作扩展包"
普通 HTTP 只能 GET/POST,而 WebDAV 增加了 MKCOL(建目录)、PUT(上传)、MOVE(重命名)等方法。Go 的 golang.org/x/net/webdav 包帮我们实现了协议解析,我们只需提供底层文件系统的实现。
3. 重命名 = “搬家+改名”
重命名的本质是:
- 从旧父目录中删掉条目
- 在新父目录中加上新名字
- 如果跨目录,还要更新父指针
关键是要同时锁住两个父目录,防止并发时"文件失踪"。我们的代码做到了这一点,安全又高效。
4. Windows 映射 = net use 命令自动化
程序启动后,偷偷执行:
net use Z: http://localhost:8080 /y
于是你的资源管理器就多了一个"网络驱动器"。退出时再执行 /delete,不留痕迹。
📦 使用方法
第一步:准备环境
确保已安装 Go(1.16+),并启用 Go Modules。
第二步:保存以下三个文件
注意:三个文件需放在同一模块下,例如项目结构如下:
your-project/
├── go.mod
├── main.go
└── memdisk/├── memfs.go└── webdav.go
第三步:运行!
go run main.go -port=8080 -drive=Z:
然后打开"此电脑",看看是不是多了个 Z: 盘?双击进去,试试新建文件、重命名——是不是和本地磁盘一模一样?


📄 源码展示
main.go
package mainimport ("context""flag""fmt""log""net/http""os""os/exec""os/signal""sync""syscall""time""golang.org/x/net/webdav""memdisk/memdisk"
)// 使用Windows net use命令映射网络驱动器
func mapNetworkDrive(serverURL, drive string) {// 首先,如果已经连接则断开连接exec.Command("net", "use", drive, "/delete", "/y").Run()// 然后映射网络驱动器cmd := exec.Command("net", "use", drive, serverURL)if err := cmd.Run(); err != nil {log.Printf("映射网络驱动器失败: %v", err)// 尝试不同的方法log.Printf("您可以手动映射驱动器: net use %s %s", drive, serverURL)} else {log.Printf("成功将 %s 映射到 %s", drive, serverURL)}
}// 使用Windows net use命令取消映射网络驱动器
func unmapNetworkDrive(drive string) {log.Printf("正在取消映射网络驱动器: %s", drive)cmd := exec.Command("net", "use", drive, "/delete", "/y")if err := cmd.Run(); err != nil {log.Printf("取消映射网络驱动器失败: %v", err)} else {log.Printf("成功取消映射 %s", drive)}
}func main() {// 解析命令行参数var port stringvar drive stringflag.StringVar(&port, "port", "8080", "服务器端口")flag.StringVar(&drive, "drive", "Z:", "映射的网络驱动器盘符")flag.Parse()// 如果使用旧的参数方式,仍然支持if flag.NArg() > 0 {port = flag.Arg(0)}if flag.NArg() > 1 {drive = flag.Arg(1)}// 创建一个新的内存文件系统fs := memdisk.NewMemFS()// 只创建一个README文件fs.WriteFile("/README.txt", []byte("内存文件系统\r\n\r\n这是一个运行在内存中的文件服务器。\r\n所有文件都存储在RAM中,服务器停止时将丢失。\r\nJjMgx"))// 用于网络驱动器映射的服务器URLserverURL := fmt.Sprintf("http://localhost:%s", port)// 启动WebDAV服务器startWebDAVServer(fs, port, drive, serverURL)}// 启动WebDAV服务器
func startWebDAVServer(fs *memdisk.MemFS, port, drive, serverURL string) {// 创建WebDAV文件系统wdFs := memdisk.NewWebDAVFS(fs)// 创建WebDAV处理器handler := &webdav.Handler{FileSystem: wdFs,LockSystem: webdav.NewMemLS(),Logger: func(r *http.Request, err error) {if err != nil {log.Printf("WebDAV错误: %v %v", r.Method, err)} else {log.Printf("WebDAV请求: %v %v", r.Method, r.URL)}},}// 创建服务器多路复用器mux := http.NewServeMux()// 为所有路径注册WebDAV处理器mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 添加CORS头部w.Header().Set("Access-Control-Allow-Origin", "*")w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, MKCOL, COPY, MOVE, OPTIONS")w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Depth, Overwrite, Destination, Authorization")// 添加可能影响显示名称的自定义头部w.Header().Set("Server", "CustomWebDAVServer")w.Header().Set("X-Server-Name", "MemoryFileSystem")// 处理OPTIONS请求if r.Method == "OPTIONS" {w.WriteHeader(http.StatusOK)return}// 调用WebDAV处理器handler.ServeHTTP(w, r)})// 监听中断信号的通道sigChan := make(chan os.Signal, 1)signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)// 确保清理完成的等待组var wg sync.WaitGroup// 启动服务器goroutine并在准备好时映射网络驱动器log.Printf("正在启动WebDAV服务器于 :%s", port)log.Printf("访问服务器地址 http://localhost:%s", port)log.Printf("尝试映射为网络驱动器: %s", drive)// 服务器goroutineserver := &http.Server{Addr: fmt.Sprintf(":%s", port),Handler: mux,}go func() {wg.Add(1)defer wg.Done()if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {log.Fatalf("服务器启动失败: %v", err)}}()// 给服务器一点时间启动,然后映射网络驱动器go func() {time.Sleep(500 * time.Millisecond)log.Printf("正在映射网络驱动器: %s 到 %s", drive, serverURL)mapNetworkDrive(serverURL, drive)}()// 等待中断信号<-sigChanlog.Println("正在关闭服务器...")// 取消映射网络驱动器unmapNetworkDrive(drive)// 优雅地关闭服务器if err := server.Shutdown(context.Background()); err != nil {log.Printf("服务器关闭错误: %v", err)}// 等待所有goroutine完成wg.Wait()log.Println("服务器已优雅退出")
}
memdisk/memfs.go
package memdiskimport ("os""sync""time"
)// File 表示内存文件系统中的一个文件
type File struct {Filename stringContent []byteFileSize int64CreatedAt time.TimeModifiedAt time.TimeIsDirectory boolChildren map[string]*FileParent *Filemu sync.RWMutex
}// MemFS 表示内存文件系统
type MemFS struct {Root *Filemu sync.RWMutex
}// NewMemFS 创建一个新的MemFS实例
func NewMemFS() *MemFS {root := &File{Filename: "",IsDirectory: true,Children: make(map[string]*File),CreatedAt: time.Now(),ModifiedAt: time.Now(),}return &MemFS{Root: root,}
}// splitPath 将路径分割为其组成部分
func splitPath(path string) []string {if path == "/" {return []string{}}components := []string{}current := ""for _, c := range path {if c == '/' {if current != "" {components = append(components, current)current = ""}} else {current += string(c)}}if current != "" {components = append(components, current)}return components
}// GetFile 根据路径获取文件或目录
func (fs *MemFS) GetFile(path string) (*File, error) {if path == "" {return nil, os.ErrInvalid}fs.mu.RLock()defer fs.mu.RUnlock()components := splitPath(path)current := fs.Rootfor _, comp := range components {current.mu.RLock()child, exists := current.Children[comp]current.mu.RUnlock()if !exists {return nil, os.ErrNotExist}current = child}return current, nil
}// CreateDir 在给定路径中创建新目录
func (fs *MemFS) CreateDir(path string) (*File, error) {if path == "" {return nil, os.ErrInvalid}components := splitPath(path)if len(components) == 0 {return fs.Root, nil // 根目录已存在}// 在不持有全局锁的情况下获取父级parentPathComponents := components[:len(components)-1]parentPath := "/" + joinPath(parentPathComponents)parent, err := fs.GetFile(parentPath)if err != nil {return nil, err}fs.mu.Lock()defer fs.mu.Unlock()parent.mu.Lock()defer parent.mu.Unlock()newDirName := components[len(components)-1]if _, exists := parent.Children[newDirName]; exists {return nil, os.ErrExist}newDir := &File{Filename: newDirName,IsDirectory: true,Children: make(map[string]*File),CreatedAt: time.Now(),ModifiedAt: time.Now(),Parent: parent,}parent.Children[newDirName] = newDirreturn newDir, nil
}// CreateFile 在给定路径中创建新文件
func (fs *MemFS) CreateFile(path string) (*File, error) {if path == "" {return nil, os.ErrInvalid}components := splitPath(path)if len(components) == 0 {return nil, os.ErrInvalid}// 在不持有全局锁的情况下获取父级parentPathComponents := components[:len(components)-1]parentPath := "/" + joinPath(parentPathComponents)parent, err := fs.GetFile(parentPath)if err != nil {return nil, err}fs.mu.Lock()defer fs.mu.Unlock()parent.mu.Lock()defer parent.mu.Unlock()newFileName := components[len(components)-1]if _, exists := parent.Children[newFileName]; exists {return nil, os.ErrExist}newFile := &File{Filename: newFileName,IsDirectory: false,Content: []byte{},FileSize: 0,CreatedAt: time.Now(),ModifiedAt: time.Now(),Parent: parent,}parent.Children[newFileName] = newFilereturn newFile, nil
}// ReadFile 读取文件内容
func (fs *MemFS) ReadFile(path string) ([]byte, error) {file, err := fs.GetFile(path)if err != nil {return nil, err}file.mu.RLock()defer file.mu.RUnlock()if file.IsDirectory {return nil, os.ErrInvalid}content := make([]byte, len(file.Content))copy(content, file.Content)return content, nil
}// WriteFile 将内容写入文件
func (fs *MemFS) WriteFile(path string, content []byte) error {file, err := fs.GetFile(path)if err != nil {// 如果文件不存在则尝试创建file, err = fs.CreateFile(path)if err != nil {return err}}file.mu.Lock()defer file.mu.Unlock()if file.IsDirectory {return os.ErrInvalid}file.Content = make([]byte, len(content))copy(file.Content, content)file.FileSize = int64(len(content))file.ModifiedAt = time.Now()return nil
}// Delete 删除文件或目录
func (fs *MemFS) Delete(path string) error {if path == "/" {return os.ErrPermission}components := splitPath(path)if len(components) == 0 {return os.ErrInvalid}// 在不持有全局锁的情况下获取父级parentPathComponents := components[:len(components)-1]parentPath := "/" + joinPath(parentPathComponents)parent, err := fs.GetFile(parentPath)if err != nil {return err}fs.mu.Lock()defer fs.mu.Unlock()parent.mu.Lock()defer parent.mu.Unlock()fileName := components[len(components)-1]if _, exists := parent.Children[fileName]; !exists {return os.ErrNotExist}delete(parent.Children, fileName)return nil
}// Rename 重命名文件或目录
func (fs *MemFS) Rename(oldPath, newPath string) error {if oldPath == "/" || newPath == "/" {return os.ErrPermission}oldComponents := splitPath(oldPath)newComponents := splitPath(newPath)if len(oldComponents) == 0 || len(newComponents) == 0 {return os.ErrInvalid}// 获取旧文件的父目录和文件名oldParentPathComponents := oldComponents[:len(oldComponents)-1]oldParentPath := "/" + joinPath(oldParentPathComponents)oldParent, err := fs.GetFile(oldParentPath)if err != nil {return err}// 获取新文件的父目录和文件名newParentPathComponents := newComponents[:len(newComponents)-1]newParentPath := "/" + joinPath(newParentPathComponents)newParent, err := fs.GetFile(newParentPath)if err != nil {return err}// 对相同对象只锁定一次sameParent := oldParent == newParentfs.mu.Lock()defer fs.mu.Unlock()oldParent.mu.Lock()defer oldParent.mu.Unlock()// 如果不是同一父目录,则锁定新父目录if !sameParent {newParent.mu.Lock()defer newParent.mu.Unlock()}oldFileName := oldComponents[len(oldComponents)-1]newFileName := newComponents[len(newComponents)-1]// 检查旧文件是否存在oldFile, exists := oldParent.Children[oldFileName]if !exists {return os.ErrNotExist}// 检查新文件是否已存在if _, exists := newParent.Children[newFileName]; exists {return os.ErrExist}// 从旧位置移除delete(oldParent.Children, oldFileName)// 更新文件名oldFile.Filename = newFileName// 更新父指针(如果不是同一父目录)if !sameParent {oldFile.Parent = newParent}// 添加到新位置newParent.Children[newFileName] = oldFile// 更新修改时间oldFile.ModifiedAt = time.Now()return nil
}// ListDir 列出目录内容
func (fs *MemFS) ListDir(path string) ([]*File, error) {dir, err := fs.GetFile(path)if err != nil {return nil, err}dir.mu.RLock()defer dir.mu.RUnlock()if !dir.IsDirectory {return nil, os.ErrInvalid}children := make([]*File, 0, len(dir.Children))for _, child := range dir.Children {child.mu.RLock()// 创建副本以避免竞态条件copyChild := &File{Filename: child.Filename,FileSize: child.FileSize,CreatedAt: child.CreatedAt,ModifiedAt: child.ModifiedAt,IsDirectory: child.IsDirectory,}child.mu.RUnlock()children = append(children, copyChild)}return children, nil
}// joinPath 将路径组件连接成单个路径字符串
func joinPath(components []string) string {result := ""for i, comp := range components {if i > 0 {result += "/"}result += comp}return result
}
memdisk/webdav.go
package memdiskimport ("context""io""os""time""golang.org/x/net/webdav"
)// 为File实现os.FileInfo接口
func (f *File) Name() string {return f.Filename
}func (f *File) Size() int64 {return f.FileSize
}func (f *File) Mode() os.FileMode {if f.IsDirectory {return 0755 | os.ModeDir}return 0644
}func (f *File) ModTime() time.Time {return f.ModifiedAt
}func (f *File) IsDir() bool {return f.IsDirectory
}func (f *File) Sys() interface{} {return nil
}// ... 移除兼容性层,现在直接实现Name()和Size() ...// WebDAVFS实现了webdav.FileSystem接口type WebDAVFS struct {fs *MemFS
}// NewWebDAVFS创建一个新的WebDAVFS实例
func NewWebDAVFS(fs *MemFS) *WebDAVFS {return &WebDAVFS{fs: fs,}
}// Mkdir创建一个新目录
func (wfs *WebDAVFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {_, err := wfs.fs.CreateDir(name)return err
}// OpenFile打开一个文件
func (wfs *WebDAVFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {// 检查文件是否存在file, err := wfs.fs.GetFile(name)if err != nil {if os.IsNotExist(err) && (flag&os.O_CREATE != 0) {// 如果文件不存在且指定了O_CREATE标志,则创建文件file, err = wfs.fs.CreateFile(name)if err != nil {return nil, err}} else {return nil, err}}// 返回WebDAVFile包装器return &WebDAVFile{file: file,pos: 0,}, nil
}// RemoveAll删除文件或目录
func (wfs *WebDAVFS) RemoveAll(ctx context.Context, name string) error {return wfs.fs.Delete(name)
}// Rename重命名文件或目录
func (wfs *WebDAVFS) Rename(ctx context.Context, oldName, newName string) error {return wfs.fs.Rename(oldName, newName)
}// Stat返回文件信息
func (wfs *WebDAVFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {file, err := wfs.fs.GetFile(name)if err != nil {return nil, err}return file, nil
}// WebDAVFile实现了webdav.File接口
type WebDAVFile struct {file *Filepos int64
}// Close关闭文件
func (wf *WebDAVFile) Close() error {return nil // 内存文件无需操作
}// Read从文件读取
func (wf *WebDAVFile) Read(p []byte) (n int, err error) {wf.file.mu.RLock()defer wf.file.mu.RUnlock()if wf.pos >= int64(len(wf.file.Content)) {return 0, io.EOF}n = copy(p, wf.file.Content[wf.pos:])wf.pos += int64(n)return n, nil
}// ReadAt在指定偏移位置从文件读取
func (wf *WebDAVFile) ReadAt(p []byte, off int64) (n int, err error) {wf.file.mu.RLock()defer wf.file.mu.RUnlock()if off >= int64(len(wf.file.Content)) {return 0, io.EOF}n = copy(p, wf.file.Content[off:])if n < len(p) {err = io.EOF}return n, err
}// Seek定位到指定位置
func (wf *WebDAVFile) Seek(offset int64, whence int) (int64, error) {wf.file.mu.RLock()defer wf.file.mu.RUnlock()var newPos int64switch whence {case io.SeekStart:newPos = offsetcase io.SeekCurrent:newPos = wf.pos + offsetcase io.SeekEnd:newPos = int64(len(wf.file.Content)) + offsetdefault:return 0, os.ErrInvalid}if newPos < 0 {return 0, os.ErrInvalid}wf.pos = newPosreturn newPos, nil
}// Write向文件写入
func (wf *WebDAVFile) Write(p []byte) (n int, err error) {wf.file.mu.Lock()defer wf.file.mu.Unlock()// 计算写入后的新位置newPos := wf.pos + int64(len(p))// 如需要则调整内容大小if newPos > int64(len(wf.file.Content)) {// 扩展内容newContent := make([]byte, newPos)copy(newContent, wf.file.Content)wf.file.Content = newContent}// 将p写入当前位置copy(wf.file.Content[wf.pos:newPos], p)// 更新位置和大小wf.pos = newPoswf.file.FileSize = newPoswf.file.ModifiedAt = time.Now()return len(p), nil
}// WriteAt在指定偏移位置向文件写入
func (wf *WebDAVFile) WriteAt(p []byte, off int64) (n int, err error) {wf.file.mu.Lock()defer wf.file.mu.Unlock()newPos := off + int64(len(p))if newPos > int64(len(wf.file.Content)) {// 扩展内容newContent := make([]byte, newPos)copy(newContent, wf.file.Content)wf.file.Content = newContent}copy(wf.file.Content[off:newPos], p)// 如需要则更新大小if newPos > wf.file.FileSize {wf.file.FileSize = newPos}wf.file.ModifiedAt = time.Now()return len(p), nil
}// Readdir读取目录条目
func (wf *WebDAVFile) Readdir(count int) ([]os.FileInfo, error) {wf.file.mu.RLock()defer wf.file.mu.RUnlock()if !wf.file.IsDirectory {return nil, os.ErrInvalid}// 将map值转换为切片children := make([]os.FileInfo, 0, len(wf.file.Children))for _, child := range wf.file.Children {children = append(children, child)}// 处理count参数if count <= 0 {return children, nil}if count > len(children) {return children, io.EOF}return children[:count], nil
}// Readdirnames读取目录条目名称
func (wf *WebDAVFile) Readdirnames(n int) ([]string, error) {wf.file.mu.RLock()defer wf.file.mu.RUnlock()if !wf.file.IsDirectory {return nil, os.ErrInvalid}// 将map键转换为切片names := make([]string, 0, len(wf.file.Children))for name := range wf.file.Children {names = append(names, name)}// 处理n参数if n <= 0 {return names, nil}if n > len(names) {return names, io.EOF}return names[:n], nil
}// Stat返回文件信息
func (wf *WebDAVFile) Stat() (os.FileInfo, error) {return wf.file, nil
}
🧼 注意事项
- 此项目仅限本地测试,无任何身份验证,请勿暴露到公网!
- Windows 映射功能仅在 Windows 上有效(废话文学+1)
- 数据随进程消亡——所以别把情书存进去,除非你想制造"数字失忆"浪漫
🌈 彩蛋
README.txt 末尾的 JjMgx 是作者签名,不是乱码,也不是病毒哈 😃
往期部分文章列表
- 远程桌面管理神器:go程序员的「服务器后宫」管理器
- FileSync:Go开发一个“佛系“文件同步小工具(附源码)
- 用 Go 写个"端口扫描器",100 行代码扫描你家路由器?
- 从"双击打不开"到"管理员都服了":用 Go 打造你的专属 .mgx 编辑器
- 震惊!Go语言居然可以这样玩Windows窗口,告别臃肿GUI库
- 剪贴板监控记:用 Go 写一个 Windows 剪贴板监控器
- 一文讲透 Go 的 defer:你的"善后管家",别让他变成"背锅侠"!
- 你知道程序怎样优雅退出吗?—— Go 开发中的"体面告别"全指南
- 用golang解救PDF文件中的图片只要200行代码!
- 200KB 的烦恼,Go 语言 20 分钟搞定!—— 一个程序员的图片压缩自救指南
- 从"CPU 烧开水"到优雅暂停:Go 里 sync.Cond 的正确打开方式
- 时移世易,篡改天机:吾以 Go 语令 Windows 文件"返老还童"记
- golang圆阵列图记:天灵灵地灵灵图标排圆形
- golang解图记
- 从 4.8 秒到 0.25 秒:我是如何把 Go 正则匹配提速 19 倍的?
- 用 Go 手搓一个内网 DNS 服务器:从此告别 IP 地址,用域名畅游家庭网络!
- 我用Go写了个华容道游戏,曹操终于不用再求关羽了!
- 用 Go 接口把 Excel 变成数据库:一个疯狂但可行的想法
- 穿墙术大揭秘:用 Go 手搓一个"内网穿透"神器!
- 布隆过滤器(go):一个可能犯错但从不撒谎的内存大师
- 自由通讯的魔法:Go从零实现UDP/P2P 聊天工具
- Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来
- 当你的程序学会了"诈尸":Go 实现 Windows 进程守护术
- 验证码识别API:告别收费接口,迎接免费午餐
- 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
- 无奈!我用go写了个MySQL服务
