Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions block.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package proton

import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"net/textproto"

"github.com/go-resty/resty/v2"
)
Expand Down Expand Up @@ -32,11 +36,40 @@ func (c *Client) RequestBlockUpload(ctx context.Context, req BlockUploadReq) ([]
return res.UploadLinks, nil
}

func (c *Client) UploadBlock(ctx context.Context, bareURL, token string, block io.Reader) error {
// UploadBlock uploads an encrypted block to Proton storage.
// The block data is accepted as []byte so that resty can replay it on retry
// without exhausting an io.Reader. (resty rebuilds the request body on each
// retry attempt; if an io.Reader is used it is consumed on the first attempt
// and subsequent retries send an empty body, causing Code=2003 errors.)
func (c *Client) UploadBlock(ctx context.Context, bareURL, token string, block []byte) error {
// Pre-build the multipart body into []byte. resty stores the raw []byte in
// r.Body and reconstructs r.bodyBuf from it on every retry attempt, so the
// storage server always receives the full block data.
var buf bytes.Buffer
w := multipart.NewWriter(&buf)

hdr := make(textproto.MIMEHeader)
hdr.Set("Content-Disposition", `form-data; name="Block"; filename="blob"`)
hdr.Set("Content-Type", "application/octet-stream")
part, err := w.CreatePart(hdr)
if err != nil {
return fmt.Errorf("creating multipart field: %w", err)
}
if _, err := part.Write(block); err != nil {
return fmt.Errorf("writing block data: %w", err)
}
if err := w.Close(); err != nil {
return fmt.Errorf("closing multipart writer: %w", err)
}

contentType := w.FormDataContentType()
bodyBytes := buf.Bytes()

return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.
SetHeader("pm-storage-token", token).
SetMultipartField("Block", "blob", "application/octet-stream", block).
SetHeader("Content-Type", contentType).
SetBody(bodyBytes).
Post(bareURL)
})
}
13 changes: 13 additions & 0 deletions block_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ type BlockUploadInfo struct {
Size int64
EncSignature string
Hash string
Verifier BlockUploadVerifier `json:",omitempty"`
}

// BlockUploadVerifier holds the per-block verification token required by the
// Proton storage backend to authenticate that the block was correctly encrypted.
type BlockUploadVerifier struct {
Token string `json:",omitempty"`
}

// RevisionVerification is the response from the block upload verification endpoint.
type RevisionVerification struct {
VerificationCode string // Base64-encoded verification code XOR'd with each block
ContentKeyPacket string // Encrypted content session key (for client-side integrity check)
}

type BlockUploadLink struct {
Expand Down
23 changes: 23 additions & 0 deletions link_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ import (
"github.com/go-resty/resty/v2"
)

// GetRevisionVerification fetches the verification code for a draft revision from the
// v2 volume-based API. The VerificationCode is XOR'd with each block's encrypted bytes
// to produce a per-block Verifier.Token that the storage backend requires.
func (c *Client) GetRevisionVerification(ctx context.Context, volumeID, linkID, revisionID string) (RevisionVerification, error) {
var res struct {
VerificationCode string
ContentKeyPacket string
}

if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get(
"/drive/v2/volumes/" + volumeID + "/links/" + linkID + "/revisions/" + revisionID + "/verification",
)
}); err != nil {
return RevisionVerification{}, err
}

return RevisionVerification{
VerificationCode: res.VerificationCode,
ContentKeyPacket: res.ContentKeyPacket,
}, nil
}

func (c *Client) ListRevisions(ctx context.Context, shareID, linkID string) ([]RevisionMetadata, error) {
var res struct {
Revisions []RevisionMetadata
Expand Down
12 changes: 7 additions & 5 deletions link_file_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ func GetNameHash(name string, hashKey []byte) (string, error) {
type MoveLinkReq struct {
ParentLinkID string

Name string // Encrypted File Name
OriginalHash string // Old Encrypted File Name Hash
Hash string // Encrypted File Name Hash by using parent's NodeHashKey
Name string // Encrypted File Name
OriginalHash string // Old Encrypted File Name Hash
Hash string // Encrypted File Name Hash by using parent's NodeHashKey

NodePassphrase string // The passphrase used to unlock the NodeKey, encrypted by the owning Link/Share keyring.
NodePassphraseSignature string // The signature of the NodePassphrase
NodePassphraseSignature string `json:",omitempty"` // The signature of the NodePassphrase; only set for anonymously created nodes.

SignatureAddress string // Signature email address used to sign passphrase and name
SignatureEmail string `json:"SignatureEmail,omitempty"` // Passphrase signature address; only set for anonymously created nodes.
NameSignatureEmail string `json:"NameSignatureEmail"` // Email address used to sign the encrypted name.
}

func (moveLinkReq *MoveLinkReq) SetName(name string, addrKR, nodeKR *crypto.KeyRing) error {
Expand Down
8 changes: 7 additions & 1 deletion response.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,13 @@ func catchRetryAfter(_ *resty.Client, res *resty.Response) (time.Duration, error
}

func catchTooManyRequests(res *resty.Response, _ error) bool {
return res.StatusCode() == http.StatusTooManyRequests || res.StatusCode() == http.StatusServiceUnavailable
switch res.StatusCode() {
case http.StatusTooManyRequests, // 429
http.StatusBadGateway, // 502 — storage servers return this transiently
http.StatusServiceUnavailable: // 503
return true
}
return false
}

func catchDialError(res *resty.Response, err error) bool {
Expand Down