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



我是围棋新手。我开始写我的第一个代码,我必须从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() {
// 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")
}

首先,我不明白为什么你一开始就需要os.O_APPEND标志。根据我的理解,你可以省略os.O_APPEND

现在,让我们来看看为什么会发生这种情况:

Doc forO_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.

所以对于每次调用write,文件偏移量定位在文件的末尾。

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

Doc forWriteAt:

$ 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.

注意最后一行,如果文件以O_APPEND标志打开,它将导致一个错误,它甚至是正确的,因为WriteAt的第二个参数是一个偏移量,但是混合O_APPEND的行为和WriteAt的偏移量查找可能会产生问题,导致意想不到的结果,它出错了。

考虑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函数将调用你传递给它的File中的WriteAt方法。根据File.WriteAt

文档

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

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

世界。操作系统可以满足WriterAt。文件做多部分并发下载,或在内存[]字节包装使用aws。WriteAtBuffer

因此,为了提高性能,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)
}

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

最新更新