获取 `panic: os: invalid use of WriteAt on file opened with O_APPEND` 错误。

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

Getting `panic: os: invalid use of WriteAt on file opened with O_APPEND`

问题

我是Go的新手。开始编写我的第一个代码,其中我需要从AWS下载一堆CSV文件。我不明白为什么在使用O_APPEND模式时会出现下面的错误。如果我移除os.O_APPEND,我只能得到最后一个文件的数据,这不是我的目标。

我的目标是将所有的CSV文件下载到一个本地文件中。我想了解我做错了什么。

package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
)

const (
	AccessKeyId     = "xxxxxxxxx"
	SecretAccessKey = "xxxxxxxxxxxxxxxxxxxx"
	Region          = "eu-central-1"
	Bucket          = "dexter-reports"
	bucketKey       = "Jenkins/pluginVersions/"
)

func main() {
	// 加载共享的AWS配置
	os.Setenv("AWS_ACCESS_KEY_ID", AccessKeyId)
	os.Setenv("AWS_SECRET_ACCESS_KEY", SecretAccessKey)
	filename := "JenkinsPluginDetais.txt"

	cred := credentials.NewStaticCredentials(AccessKeyId, SecretAccessKey, "")
	config := aws.Config{Credentials: cred, Region: aws.String(Region), Endpoint: aws.String("s3.amazonaws.com")}

	file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666)

	if err != nil {
		panic(err)
	}

	defer file.Close()

	sess, err := session.NewSession(&config)

	if err != nil {
		fmt.Println(err)
	}

	// 列出存储桶中的对象
	ObjectList := listBucketObjects(sess)

	// 遍历对象列表。首先通过s3manager初始化s3下载器
	downloader := s3manager.NewDownloader(sess)

	for _, item := range ObjectList.Contents {
		csvFile := filepath.Base(*item.Key)

		if csvFile != "pluginVersions" {

			downloadBucketObjects(downloader, file, csvFile)

		}

	}

}

func listBucketObjects(sess *session.Session) *s3.ListObjectsV2Output {

	// 创建一个新的s3客户端
	svc := s3.New(sess)
	resp, err := svc.ListObjectsV2(&s3.ListObjectsV2Input{
		Bucket: aws.String(Bucket),
		Prefix: aws.String(bucketKey),
	})

	if err != nil {
		panic(err)
	}

	return resp

}

func downloadBucketObjects(downloader *s3manager.Downloader, file *os.File, keyobj string) {

	fileToDownload := bucketKey + keyobj

	numBytes, err := downloader.Download(file,
		&s3.GetObjectInput{
			Bucket: aws.String(Bucket),
			Key:    aws.String(fileToDownload),
		})

	if err != nil {
		panic(err)
	}

	fmt.Println("Downloaded", file.Name(), numBytes, "bytes")

}

希望对你有帮助!

英文:

I am a newbie to Go. Was starting to write my first code in which I have to download a bunch of CSV's from AWS. I don't understand why it is giving me the below error with O_APPEND mode. If I remove os.O_APPEND, I only get the last file data which is not the objective.

The objective is to download all CSV files into one file locally. I'd like to understand what I'm doing incorrectly.

package main
import (
"fmt"
"os"
"path/filepath"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
)
const (
AccessKeyId     = "xxxxxxxxx"
SecretAccessKey = "xxxxxxxxxxxxxxxxxxxx"
Region          = "eu-central-1"
Bucket          = "dexter-reports"
bucketKey       = "Jenkins/pluginVersions/"
)
func main() {
// Load the Shared AWS Configuration
os.Setenv("AWS_ACCESS_KEY_ID", AccessKeyId)
os.Setenv("AWS_SECRET_ACCESS_KEY", SecretAccessKey)
filename := "JenkinsPluginDetais.txt"
cred := credentials.NewStaticCredentials(AccessKeyId, SecretAccessKey, "")
config := aws.Config{Credentials: cred, Region: aws.String(Region), Endpoint: aws.String("s3.amazonaws.com")}
file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
panic(err)
}
defer file.Close()
sess, err := session.NewSession(&config)
if err != nil {
fmt.Println(err)
}
//list Buckets
ObjectList := listBucketObjects(sess)
//loop over the obectlist. First initialize the s3 downloader via s3manager
downloader := s3manager.NewDownloader(sess)
for _, item := range ObjectList.Contents {
csvFile := filepath.Base(*item.Key)
if csvFile != "pluginVersions" {
downloadBucketObjects(downloader, file, csvFile)
}
}
}
func listBucketObjects(sess *session.Session) *s3.ListObjectsV2Output {
//create a new s3 client
svc := s3.New(sess)
resp, err := svc.ListObjectsV2(&s3.ListObjectsV2Input{
Bucket: aws.String(Bucket),
Prefix: aws.String(bucketKey),
})
if err != nil {
panic(err)
}
return resp
}
func downloadBucketObjects(downloader *s3manager.Downloader, file *os.File, keyobj string) {
fileToDownload := bucketKey + keyobj
numBytes, err := downloader.Download(file,
&s3.GetObjectInput{
Bucket: aws.String(Bucket),
Key:    aws.String(fileToDownload),
})
if err != nil {
panic(err)
}
fmt.Println("Downloaded", file.Name(), numBytes, "bytes")
}

答案1

得分: 3

首先,我不明白为什么你在第一次调用中需要os.O_APPEND标志。根据我的理解,你可以省略os.O_APPEND

现在,让我们来看看为什么会出现实际的问题:

O_APPEND的文档(参考:https://man7.org/linux/man-pages/man2/open.2.html):

O_APPEND
文件以追加模式打开。在每次写入(write(2))之前,文件偏移量都会定位在文件末尾,就像使用lseek(2)一样。文件偏移量的修改和写操作将作为一个单一的原子步骤执行。

因此,对于每次调用write,文件偏移量都会定位在文件末尾。

但是,(*s3Manager.Download).Download应该使用WriteAt方法,即:

WriteAt的文档:

$ go doc os WriteAt
package os // import "os"
func (f *File) WriteAt(b []byte, off int64) (n int, err error)
WriteAt在文件中的字节偏移量off处开始写入长度为len(b)的字节。它返回写入的字节数和错误(如果有)。当n != len(b)时,WriteAt返回非nil的错误。
如果文件使用了O_APPEND标志打开,WriteAt将返回一个错误。

注意最后一行,如果文件使用了O_APPEND标志,将会导致错误。这是正确的,因为WriteAt的第二个参数是一个偏移量,但是混合使用O_APPEND的行为和WriteAt的偏移量寻址可能会导致问题,从而产生意外的结果并引发错误。

英文:

Firstly, I don't get it why do you even need os.O_APPEND flag in the first place. As per my understanding, you can omit os.O_APPEND.

Now, let's come to the actual problem of why it's happening:

Doc for O_APPEND (Ref: https://man7.org/linux/man-pages/man2/open.2.html):

O_APPEND
The file is opened in append mode.  Before each write(2),
the file offset is positioned at the end of the file, as
if with lseek(2).  The modification of the file offset and
the write operation are performed as a single atomic step.

So for every call to write the file offset is positioned at the end of the file.

But (*s3Manager.Download).Download supposedly be using WriteAt method, i.e.,

Doc for WriteAt:

$ go doc os WriteAt
package os // import "os"
func (f *File) WriteAt(b []byte, off int64) (n int, err error)
WriteAt writes len(b) bytes to the File starting at byte offset off. It
returns the number of bytes written and an error, if any. WriteAt returns a
non-nil error when n != len(b).
If file was opened with the O_APPEND flag, WriteAt returns an error.

Notice the last line, that if the file's opened with O_APPEND flag it will result in an error and it's even right because WriteAt's second argument is an offset but mixing O_APPEND's behaviour and WriteAt offset seeking might create problem resulting in unexpected results and it errors out.

答案2

得分: 2

考虑一下s3manager.Downloader的定义:

func (d Downloader) Download(w io.WriterAt, input *s3.GetObjectInput, options ...func(*Downloader)) (n int64, err error)

第一个参数是io.WriterAt类型的;这个接口是:

type WriterAt interface {
	WriteAt(p []byte, off int64) (n int, err error)
}

这意味着Download函数将调用你传递给它的FileWriteAt方法。根据File.WriteAt的文档:

>如果文件是以O_APPEND标志打开的,则WriteAt会返回一个错误。

所以这解释了为什么你会得到这个错误,但也引出了一个问题:“为什么Download使用WriteAt而不接受一个io.Writer(并调用Write)?”答案可以在文档中找到:

>w io.WriterAt可以通过os.File来满足,以进行多部分并发下载,或者使用aws.WriteAtBuffer的内存[]byte包装器。

因此,为了提高性能,Downloader可能会对文件的多个部分发出多个同时请求,然后在接收到它们时将其写入(这意味着它可能不按顺序写入数据)。这也解释了为什么多次使用相同的File调用该函数会导致数据被覆盖(当Downloader检索文件的每个块时,它会将其写入输出文件的适当位置;这会覆盖已经存在的任何数据)。

文档中的上述引用还指出了一个可能的解决方案;使用aws.WriteAtBuffer,并在下载完成后将数据写入文件(然后可以使用O_APPEND打开该文件)- 类似于以下代码:

buf := aws.NewWriteAtBuffer([]byte{})
numBytes, err := downloader.Download(buf,
        &s3.GetObjectInput{
            Bucket: aws.String(Bucket),
            Key:    aws.String(fileToDownload),
        })

if err != nil {
    panic(err)
}
_, err = file.Write(buf.Bytes())
if err != nil {
    panic(err)
}

另一种方法是将文件下载到临时文件,然后将其追加到输出文件中(如果文件很大,可能需要这样做)。

英文:

Consider the definition of s3manager.Downloader:

func (d Downloader) Download(w io.WriterAt, input *s3.GetObjectInput, options ...func(*Downloader)) (n int64, err error)

The first argument is an io.WriterAt; this interface is:

type WriterAt interface {
WriteAt(p []byte, off int64) (n int, err error)
}

This means that the Download function is going to call the WriteAt method in the File you are passing it. As per the documentation for File.WriteAt

>If file was opened with the O_APPEND flag, WriteAt returns an error.

So this explains why you are getting the error but raises the question "why is Download using WriteAt and not accepting an io.Writer (and calling Write)?"; the answer can be found in the documentation:

>The w io.WriterAt can be satisfied by an os.File to do multipart concurrent downloads, or in memory []byte wrapper using aws.WriteAtBuffer

So, to increase performance, Downloader might make multiple simultaneous requests for parts of the file and then write these out as they are received (meaning it may not write the data in order). This also explains why calling the function multiple times with the same File results in overwritten data (when Downloader retrieves the each chunk of the file it writes it out at the appropriate position in the output file; this overwrites any data already there).

The above quote from the documentation also points to a possible solution; use an aws.WriteAtBuffer and, once the download is finished, write the data to your file (which could then be opened with O_APPEND) - something like this:

buf := aws.NewWriteAtBuffer([]byte{})
numBytes, err := downloader.Download(buf,
&s3.GetObjectInput{
Bucket: aws.String(Bucket),
Key:    aws.String(fileToDownload),
})
if err != nil {
panic(err)
}
_, err = file.Write(buf.Bytes())
if err != nil {
panic(err)
}

An alternative would be to download into a temporary file and then append that to your output file (you may need to do this if the files are large).

huangapple
  • 本文由 发表于 2021年8月1日 14:47:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/68608085.html
匿名

发表评论

匿名网友

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

确定