【问题标题】:Simple way to copy a file复制文件的简单方法
【发布时间】:2014-01-30 09:47:07
【问题描述】:

有没有什么简单/快速的方法可以在 Go 中复制文件?

我在 Doc 中找不到快速的方法,搜索互联网也无济于事。

【问题讨论】:

标签: file copy go


【解决方案1】:

警告:这个答案主要是关于向文件添加硬链接,而不是复制内容。

健壮高效副本在概念上很简单,但由于需要处理许多边缘情况和系统限制,因此实施起来并不简单。目标操作系统及其配置。

如果您只是想复制现有文件,您可以使用os.Link(srcName, dstName)。这避免了在应用程序中移动字节并节省磁盘空间。对于大文件,这可以节省大量时间和空间。

但是不同的操作系统对硬链接的工作方式有不同的限制。根据您的应用程序和目标系统配置,Link() 调用可能并非在所有情况下都有效。

如果您想要一个通用、强大且高效的复制功能,请将Copy() 更新为:

  1. 执行检查以确保至少某种形式的复制会成功(访问权限、目录存在等)
  2. 检查这两个文件是否已经存在并且相同 os.SameFile,如果相同则返回成功
  3. 尝试链接,成功则返回
  4. 复制字节(所有有效均失败),返回结果

一种优化是在 go 例程中复制字节,这样调用者就不会阻塞字节复制。这样做会增加调用者正确处理成功/错误情况的复杂性。

如果我想要两者,我将有两个不同的复制函数:CopyFile(src, dst string) (error) 用于阻塞复制,CopyFileAsync(src, dst string) (chan c, error) 将信号通道传回给异步情况的调用者。

package main

import (
    "fmt"
    "io"
    "os"
)

// CopyFile copies a file from src to dst. If src and dst files exist, and are
// the same, then return success. Otherise, attempt to create a hard link
// between the two files. If that fail, copy the file contents from src to dst.
func CopyFile(src, dst string) (err error) {
    sfi, err := os.Stat(src)
    if err != nil {
        return
    }
    if !sfi.Mode().IsRegular() {
        // cannot copy non-regular files (e.g., directories,
        // symlinks, devices, etc.)
        return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
    }
    dfi, err := os.Stat(dst)
    if err != nil {
        if !os.IsNotExist(err) {
            return
        }
    } else {
        if !(dfi.Mode().IsRegular()) {
            return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
        }
        if os.SameFile(sfi, dfi) {
            return
        }
    }
    if err = os.Link(src, dst); err == nil {
        return
    }
    err = copyFileContents(src, dst)
    return
}

// copyFileContents copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file.
func copyFileContents(src, dst string) (err error) {
    in, err := os.Open(src)
    if err != nil {
        return
    }
    defer in.Close()
    out, err := os.Create(dst)
    if err != nil {
        return
    }
    defer func() {
        cerr := out.Close()
        if err == nil {
            err = cerr
        }
    }()
    if _, err = io.Copy(out, in); err != nil {
        return
    }
    err = out.Sync()
    return
}

func main() {
    fmt.Printf("Copying %s to %s\n", os.Args[1], os.Args[2])
    err := CopyFile(os.Args[1], os.Args[2])
    if err != nil {
        fmt.Printf("CopyFile failed %q\n", err)
    } else {
        fmt.Printf("CopyFile succeeded\n")
    }
}

【讨论】:

  • 您应该添加一个重要的警告,即创建硬链接与创建副本相同。使用硬链接,您有一个文件,使用副本,您有两个不同的文件。使用副本时,对第一个文件的更改不会影响第二个文件。
  • 好点。我认为链接的定义是隐含的,但实际上只有在已知的情况下才清楚。
  • 问题是关于复制一个文件;不创建更多的分区链接。如果用户只是想从多个位置引用同一个文件,硬链接(或软链接)应该是一个替代答案。
  • 理论上也应该检查dst中是否有足够的空间。
  • 请记住,由于这部分:if err = os.Link(src, dst)... 此功能无法按原样用于备份目的。如果要复制文件以备份某些数据,则必须将数据本身复制到文件系统上
【解决方案2】:

您已经获得了在标准库中编写此类函数所需的所有内容。这是显而易见的代码。

// Copy the src file to dst. Any existing file will be overwritten and will not
// copy file attributes.
func Copy(src, dst string) error {
    in, err := os.Open(src)
    if err != nil {
        return err
    }
    defer in.Close()

    out, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer out.Close()

    _, err = io.Copy(out, in)
    if err != nil {
        return err
    }
    return out.Close()
}

【讨论】:

  • 根据您的应用程序,如果输出文件存在,您可能希望失败,否则您将覆盖文件的内容。您可以通过调用 os.OpenFile(dst, syscall.O_CREATE | syscall.O_EXCL, FileMode(0666)) 而不是 os.Create(...) 来做到这一点。如果目标文件已经存在,该调用将失败。如果两个文件已经相同(例如,如果它们被链接),另一个优化将是避免复制文件。你
  • 仅使用标准库无法实现的一个方面是对写时复制的透明支持,这在某些情况下可能是可取的。但是,c-o-w 仅在某些文件系统上受支持,据我所知,它没有系统调用(除了 ioctl)。
  • 延迟的out.Close() 不会总是失败吗?您没有检查错误,但文档说连续调用 Close() 将失败。
  • 为什么在return和defer中都是out.Close()?
  • 如果 io.Copy 错误,第一个 defer out.Close 将运行。第二个out.Close() 是不必要的,每次都会出错。
【解决方案3】:
import (
    "io/ioutil"
    "log"
)

func checkErr(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func copy(src string, dst string) {
    // Read all content of src to data
    data, err := ioutil.ReadFile(src)
    checkErr(err)
    // Write data to dst
    err = ioutil.WriteFile(dst, data, 0644)
    checkErr(err)
}

【讨论】:

  • 您能否在代码中添加几行解释或 cmets,以帮助 OP 理解它。
  • 如果文件夹中有几ggs大小的文件,这个程序会吃gigs内存。请改用 io.CopyN()。
  • @smile-on 是的,但是如果您要复制一个小文件以进行测试或其他用途,谁在乎?快速而肮脏的方法也值得在这里。
【解决方案4】:

如果你在linux/mac中运行代码,你可以直接执行系统的cp命令。

srcFolder := "copy/from/path"
destFolder := "copy/to/path"
cpCmd := exec.Command("cp", "-rf", srcFolder, destFolder)
err := cpCmd.Run()

它把 go 有点像一个脚本,但它完成了工作。另外,你需要导入“os/exec”

【讨论】:

  • 这是否可以保证如果 srcFolder 或 destFolder 无效甚至是用户恶意制作的会发生什么?说 destFolder := "copy/to/path; rm -rf /",SQL 注入风格。
  • 如果从用户那里指定源文件夹和目标文件夹,我建议使用不同的方法。此代码假定路径有效。
  • @user1047788 虽然来自用户的任何路径都需要清理/验证,以防万一你好奇,“;” os.Exec 不会将其评估为执行新命令。您的示例实际上会将确切的值“copy/to/path; rm -rf /”作为参数发送给 cp 命令(包括空格和其他字符)。
  • 这是一个适用于 Windows 的巧妙技巧!但是,go on windows 会在 srcFolder 路径中进行名称替换,go on linux 不会。 srcFolder := "copy/from/path/*" 在 win 上正常,在 Linux 上出错。
  • 我尝试使用您的代码复制文件--help,但没有任何反应。 ;)
【解决方案5】:

在这种情况下,需要验证几个条件,我更喜欢非嵌套代码

func Copy(src, dst string) (int64, error) {
  src_file, err := os.Open(src)
  if err != nil {
    return 0, err
  }
  defer src_file.Close()

  src_file_stat, err := src_file.Stat()
  if err != nil {
    return 0, err
  }

  if !src_file_stat.Mode().IsRegular() {
    return 0, fmt.Errorf("%s is not a regular file", src)
  }

  dst_file, err := os.Create(dst)
  if err != nil {
    return 0, err
  }
  defer dst_file.Close()
  return io.Copy(dst_file, src_file)
}

【讨论】:

    【解决方案6】:

    从 Go 1.15(2020 年 8 月)开始,您可以使用File.ReadFrom

    package main
    import "os"
    
    func main() {
       r, err := os.Open("in.txt")
       if err != nil {
          panic(err)
       }
       defer r.Close()
       w, err := os.Create("out.txt")
       if err != nil {
          panic(err)
       }
       defer w.Close()
       w.ReadFrom(r)
    }
    

    【讨论】:

      【解决方案7】:

      这是一个复制文件的明显方法:

      package main
      import (
          "os"
          "log"
          "io"
      )
      
      func main() {
          sFile, err := os.Open("test.txt")
          if err != nil {
              log.Fatal(err)
          }
          defer sFile.Close()
      
          eFile, err := os.Create("test_copy.txt")
          if err != nil {
              log.Fatal(err)
          }
          defer eFile.Close()
      
          _, err = io.Copy(eFile, sFile) // first var shows number of bytes
          if err != nil {
              log.Fatal(err)
          }
      
          err = eFile.Sync()
          if err != nil {
              log.Fatal(err)
          }
      }
      

      【讨论】:

      • 我试过这个方法,但是导致文件不能正常工作。
      • eFile 中的e 是什么意思?
      【解决方案8】:

      如果你在 windows 上,你可以像这样包装 CopyFileW:

      package utils
      
      import (
          "syscall"
          "unsafe"
      )
      
      var (
          modkernel32   = syscall.NewLazyDLL("kernel32.dll")
          procCopyFileW = modkernel32.NewProc("CopyFileW")
      )
      
      // CopyFile wraps windows function CopyFileW
      func CopyFile(src, dst string, failIfExists bool) error {
          lpExistingFileName, err := syscall.UTF16PtrFromString(src)
          if err != nil {
              return err
          }
      
          lpNewFileName, err := syscall.UTF16PtrFromString(dst)
          if err != nil {
              return err
          }
      
          var bFailIfExists uint32
          if failIfExists {
              bFailIfExists = 1
          } else {
              bFailIfExists = 0
          }
      
          r1, _, err := syscall.Syscall(
              procCopyFileW.Addr(),
              3,
              uintptr(unsafe.Pointer(lpExistingFileName)),
              uintptr(unsafe.Pointer(lpNewFileName)),
              uintptr(bFailIfExists))
      
          if r1 == 0 {
              return err
          }
          return nil
      }
      

      代码的灵感来自 C:\Go\src\syscall\zsyscall_windows.go 中的包装器

      【讨论】:

      • 当语言提供所有必要的原语时,编写如此复杂的东西是没有意义的
      • 当人们想要利用操作系统性能时,它解决了一个特定的用例
      【解决方案9】:

      您可以使用“执行”。 exec.Command("cmd","/c","copy","fileToBeCopied destinationDirectory") 用于 windows 我已经使用它并且它工作正常。有关 exec 的更多详细信息,您可以参考手册。

      【讨论】:

      • @mh-cbon :我可能是错的,只要我看到问题它只是询问复制文件的方法。当然可能还有其他方法,但我发现这种方法非常简单。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-09-24
      • 2010-09-11
      • 1970-01-01
      • 2013-05-18
      • 1970-01-01
      • 1970-01-01
      • 2018-01-25
      相关资源
      最近更新 更多