How to write a safe rename in Go? (Or, how to write this Python in Go?)

huangapple go评论90阅读模式
英文:

How to write a safe rename in Go? (Or, how to write this Python in Go?)

问题

我在Python中有以下代码:

if not os.path.exists(src): sys.exit("Does not exist: %s" % src)
if os.path.exists(dst): sys.exit("Already exists: %s" % dst)
os.rename(src, dst)

根据这个问题,我了解到在Go中没有直接的方法来测试文件是否存在或不存在。

请问在Go中如何正确编写上述代码,包括打印出正确的错误字符串

以下是我最接近的尝试:

package main

import "fmt"
import "os"

func main() {
    src := "a"
    dst := "b"
    e := os.Rename(src, dst)
    if e != nil {
        fmt.Println(e.(*os.LinkError).Op)
        fmt.Println(e.(*os.LinkError).Old)
        fmt.Println(e.(*os.LinkError).New)
        fmt.Println(e.(*os.LinkError).Err)
    }
}

根据错误信息的可用性,它实际上没有告诉你问题是什么,除非你解析一个自由格式的英文字符串。在我看来,似乎不可能在Go中编写等效的代码。

英文:

I've got the following code in Python:

    if not os.path.exists(src): sys.exit("Does not exist: %s" % src)
    if os.path.exists(dst): sys.exit("Already exists: %s" % dst)
    os.rename(src, dst)

From this question, I understand that there is no direct method to test if a file exists or doesn't exist.

What is the proper way to write the above in Go, including printing out the correct error strings?

Here is the closest I've gotten:

package main

import "fmt"
import "os"

func main() {
	src := "a"
    dst := "b"
	e := os.Rename(src, dst)
	if e != nil {
		fmt.Println(e.(*os.LinkError).Op)
		fmt.Println(e.(*os.LinkError).Old)
		fmt.Println(e.(*os.LinkError).New)
		fmt.Println(e.(*os.LinkError).Err)
	}
}

From the availability of information from the error, where it effectively doesn't tell you what the problem is without you parsing an English freeformat string, it seems to me that it is not possible to write the equivalent in Go.

答案1

得分: 9

你提供的代码存在竞态条件:在你检查dst不存在并将某些内容复制到dst之间,第三方可能已经创建了文件dst,导致你覆盖了一个文件。要么删除os.path.exists(dst)的检查,因为它无法可靠地检测到在你尝试删除它时目标是否存在,要么使用以下算法:

  1. src创建一个硬链接到dst。如果名为dst的文件已经存在,操作将失败,你可以退出。如果src不存在,操作也会失败。
  2. 删除src

下面的代码在Go中实现了上述的两步算法。

import "os"

func renameAndCheck(src, dst string) error {
    err := os.Link(src, dst)
    if err != nil {
        return err
    }

    return os.Remove(src)
}

你可以检查调用os.Link()失败的原因:

  • 如果错误满足os.IsNotExist(),则调用失败是因为在调用os.Link()src不存在。
  • 如果错误满足os.IsExist(),则调用失败是因为在调用os.Link()dst存在。
  • 如果错误满足os.IsPermission(),则调用失败是因为你没有足够的权限创建硬链接。

据我所知,其他原因(如文件系统不支持创建硬链接或srcdst位于不同的文件系统上)无法以可移植的方式进行测试。

英文:

The code you provide contains a race condition: Between you checking for dst to not exist and copying something into dst, a third party could have created the file dst, causing you to overwrite a file. Either remove the os.path.exists(dst) check because it cannot reliably detect if the target exists at the time you try to remove it, or employ the following algorithm instead:

  1. Create a hardlink from src to dst. If a file named dst exists, the operation will fail and you can bail out. If src does not exist, the operation will fail, too.
  2. Remove src.

The following code implements the two-step algorithm outlined above in Go.

import "os"

func renameAndCheck(src, dst string) error {
    err := os.Link(src, dst)
    if err != nil {
        return err
    }

    return os.Remove(src)
}

You can check for which reason the call to os.Link() failed:

  • If the error satisfies os.IsNotExist(), the call failed because src did not exist at the time os.Link() was called
  • If the error satisfies os.IsExist(), the call failed because dst exists at the time os.Link() is called
  • If the error satisfies os.IsPermission(), the call failed because you don't have sufficient permissions to create a hard link

As far as I know, other reasons (like the file system not supporting the creation of hard links or src and dst being on different file systems) cannot be tested portably.

答案2

得分: 2

你的Python代码翻译成Go语言的代码如下:

if _, err := os.Stat(src); err != nil {
    // 源文件不存在或访问源文件时出现其他错误
    log.Fatal("source:", err)
}
if _, err := os.Stat(dst); !os.IsNotExist(err) {
    // 目标文件已存在或访问目标文件时出现其他错误
    log.Fatal("dest:", err)
}
if err := os.Rename(src, dst); err != nil {
    log.Fatal(err)
}

这三个函数调用的顺序是不安全的(我指的是原始的Python版本和我在这里复制的版本)。在检查之后,源文件可能被删除,或者目标文件可能在重命名之前被创建。

安全地移动文件的方法取决于操作系统。在Windows上,你可以直接调用os.Rename()函数。在Windows上,如果目标文件存在或源文件不存在,该函数将失败。在Posix系统上,你应该像另一个答案中描述的那样进行链接和删除操作。

英文:

The translation if your Python code to Go is:

if _, err := os.Stat(src); err != nil {
    // The source does not exist or some other error accessing the source
    log.Fatal("source:", err)
}
if _, err := os.Stat(dst); !os.IsNotExists(dst) {
    // The destination exists or some other error accessing the destination
    log.Fatal("dest:", err)
}
if err := os.Rename(src, dst); err != nil {
    log.Fatal(err)
}

The three function call sequence is not safe (I am referring to both the original Python version and my replication of it here). The source can be removed or the destination can be created after the checks, but before the rename.

The safe way to move a file is OS dependent. On Windows, you can just call os.Rename(). On Windows, this function will fail if the destination exists or the source does not. On Posix systems, you should link and remove as described in another answer.

huangapple
  • 本文由 发表于 2014年9月20日 02:09:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/25939965.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定