0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-30 18:42:58 +00:00
terraform-provider-proxmox/proxmox/nodes/storage/upload.go
Keith e8bd1fcda7
fix(storage): ignore os.ErrClosed from deferred fileReader.Close() in storage.APIUpload (#1468)
fix(provider): ignore `ErrClosed` for fileReader

Signed-off-by: Keith King <KingKeithC@gmail.com>
2024-08-06 22:36:24 -04:00

164 lines
3.7 KiB
Go

package storage
import (
"context"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// APIUpload uploads a file to a datastore using the Proxmox API.
func (c *Client) APIUpload(
ctx context.Context,
d *api.FileUploadRequest,
tempDir string,
) (*DatastoreUploadResponseBody, error) {
tflog.Debug(ctx, "uploading file to datastore using PVE API", map[string]interface{}{
"file_name": d.FileName,
"content_type": d.ContentType,
})
r, w := io.Pipe()
defer func(r *io.PipeReader) {
err := r.Close()
if err != nil {
tflog.Error(ctx, "failed to close pipe reader", map[string]interface{}{
"error": err,
})
}
}(r)
m := multipart.NewWriter(w)
go func() {
defer func(w *io.PipeWriter) {
err := w.Close()
if err != nil {
tflog.Error(ctx, "failed to close pipe writer", map[string]interface{}{
"error": err,
})
}
}(w)
defer func(m *multipart.Writer) {
err := m.Close()
if err != nil {
tflog.Error(ctx, "failed to close multipart writer", map[string]interface{}{
"error": err,
})
}
}(m)
err := m.WriteField("content", d.ContentType)
if err != nil {
tflog.Error(ctx, "failed to write 'content' field", map[string]interface{}{
"error": err,
})
return
}
part, err := m.CreateFormFile("filename", d.FileName)
if err != nil {
return
}
_, err = io.Copy(part, d.File)
if err != nil {
return
}
}()
// We need to store the multipart content in a temporary file to avoid using high amounts of memory.
// This is necessary due to Proxmox VE not supporting chunked transfers in v6.1 and earlier versions.
tempMultipartFile, err := os.CreateTemp(tempDir, "multipart")
if err != nil {
return nil, fmt.Errorf("failed to create temporary file: %w", err)
}
tempMultipartFileName := tempMultipartFile.Name()
_, err = io.Copy(tempMultipartFile, r)
if err != nil {
return nil, fmt.Errorf("failed to copy multipart data to temporary file: %w", err)
}
err = tempMultipartFile.Close()
if err != nil {
return nil, fmt.Errorf("failed to close temporary file: %w", err)
}
defer func(name string) {
e := os.Remove(name)
if e != nil {
tflog.Error(ctx, "failed to remove temporary file", map[string]interface{}{
"error": e,
})
}
}(tempMultipartFileName)
// Now that the multipart data is stored in a file, we can go ahead and do an HTTP POST request.
fileReader, err := os.Open(tempMultipartFileName)
if err != nil {
return nil, fmt.Errorf("failed to open temporary file: %w", err)
}
defer func(fileReader *os.File) {
e := fileReader.Close()
if e != nil {
if errors.Is(e, os.ErrClosed) {
// We can ignore the error in the case that the file was already closed.
return
}
tflog.Error(ctx, "failed to close file reader", map[string]interface{}{
"error": e,
})
}
}(fileReader)
fileInfo, err := fileReader.Stat()
if err != nil {
return nil, fmt.Errorf("failed to get file info: %w", err)
}
fileSize := fileInfo.Size()
reqBody := &api.MultiPartData{
Boundary: m.Boundary(),
Reader: fileReader,
Size: &fileSize,
}
resBody := &DatastoreUploadResponseBody{}
err = c.DoRequest(
ctx,
http.MethodPost,
c.ExpandPath("upload"),
reqBody,
resBody,
)
if err != nil {
return nil, fmt.Errorf("error uploading file to datastore %s: %w", c.StorageName, err)
}
if resBody.UploadID == nil {
return nil, fmt.Errorf("error uploading file to datastore %s: no uploadID", c.StorageName)
}
err = c.Tasks().WaitForTask(ctx, *resBody.UploadID)
if err != nil {
return nil, fmt.Errorf("error uploading file to datastore %s: failed waiting for upload - %w", c.StorageName, err)
}
return resBody, nil
}