From 16d78b86df99a7bccbf213847fbdea7fdb5eac21 Mon Sep 17 00:00:00 2001 From: Vladimir Senflok Date: Mon, 18 May 2026 22:47:23 +0200 Subject: [PATCH] fix: ensure rm-filename header always includes file extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reMarkable Cloud started rejecting rm-filename header values without a file extension on ~18 May 2026, returning HTTP 400 with {"message":"unexpected 'rm-filename' http header"} for bare UUIDs. This breaks all sync operations (rmapi ls, mkdir, put, mv, etc.) because BuildTree / Mirror fail on the very first child blob fetch. Several callers pass extensionless names through to the transport layer: - tree.BuildTree:377 - provider.GetReader(e.Hash, e.DocumentID) - blobdoc.Mirror:217 - r.GetReader(e.Hash, e.DocumentID) - tree.BuildTree:363 - provider.GetReader(rootHash, "roothash") (legacy) At the BlobStorage layer all such reads/writes represent doc-level docSchema blobs, so default to the .docSchema suffix when the supplied filename has no extension. Callers that already pass a full filename (e.g. addExt(name, MetadataExt) → '.metadata') are unaffected. Verified via curl against internal.cloud.remarkable.com with a valid bearer token: rm-filename: -> HTTP 400 (unexpected header) rm-filename: .docSchema -> HTTP 200 (real payload returned) After the patch, rmapi ls / works on accounts that were locked out since 18 May 2026. Fixes #62 Signed-off-by: Vladimir Senflok --- api/sync15/blobstorage.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/api/sync15/blobstorage.go b/api/sync15/blobstorage.go index ea95320e..28310fa7 100644 --- a/api/sync15/blobstorage.go +++ b/api/sync15/blobstorage.go @@ -3,7 +3,9 @@ package sync15 import ( "fmt" "io" + "strings" + "github.com/juruen/rmapi/archive" "github.com/juruen/rmapi/config" "github.com/juruen/rmapi/log" "github.com/juruen/rmapi/model" @@ -21,11 +23,24 @@ func NewBlobStorage(http *transport.HttpClientCtx) *BlobStorage { } } +// reMarkable Cloud (since ~2026-05-18) rejects rm-filename header values +// without a file extension (HTTP 400 "unexpected 'rm-filename' http header"). +// Several callers pass bare DocumentIDs (UUIDs) or literal "roothash" +// through to the transport layer; at this layer all such cases represent +// doc-level docSchema reads/writes, so default to the .docSchema suffix. +func ensureExtension(filename string) string { + if !strings.Contains(filename, ".") { + return filename + "." + string(archive.DocSchemaExt) + } + return filename +} + func (b *BlobStorage) GetReader(hash, filename string) (io.ReadCloser, error) { - return b.http.GetStream(transport.UserBearer, config.BlobUrl+hash, filename) + return b.http.GetStream(transport.UserBearer, config.BlobUrl+hash, ensureExtension(filename)) } func (b *BlobStorage) UploadBlob(hash, filename string, reader io.Reader) error { + filename = ensureExtension(filename) log.Trace.Println("uploading blob ", filename) headers := map[string]string{}