Add UploadAttachment, a unary RPC that accepts a file in bounded chunks and streams it to a temp file instead of holding the whole blob in one request body and in memory. A call carrying a spec starts an upload and returns an opaque upload_id; later calls carry that id with write_offset, data, and finish_write, following the Cloud Storage WriteObject shape. Retrying the last chunk after a lost response is idempotent, uploads are bound to their owner, and they expire after 30 minutes of inactivity. CreateAttachment and the chunked finalize now share one processing pipeline for motion-photo detection, EXIF stripping, and storage. The motion-photo detector works over an io.ReaderAt so large JPEGs are never loaded whole, and the request body cap is a single per-procedure lookup. The web editor uploads through the new RPC in 2 MiB chunks with bounded concurrency under the server's per-user limit.
313 lines
11 KiB
Protocol Buffer
313 lines
11 KiB
Protocol Buffer
syntax = "proto3";
|
|
|
|
package memos.api.v1;
|
|
|
|
import "google/api/annotations.proto";
|
|
import "google/api/client.proto";
|
|
import "google/api/field_behavior.proto";
|
|
import "google/api/resource.proto";
|
|
import "google/protobuf/empty.proto";
|
|
import "google/protobuf/field_mask.proto";
|
|
import "google/protobuf/timestamp.proto";
|
|
|
|
option go_package = "gen/api/v1";
|
|
|
|
service AttachmentService {
|
|
// CreateAttachment creates a new attachment.
|
|
rpc CreateAttachment(CreateAttachmentRequest) returns (Attachment) {
|
|
option (google.api.http) = {
|
|
post: "/api/v1/attachments"
|
|
body: "attachment"
|
|
};
|
|
option (google.api.method_signature) = "attachment";
|
|
}
|
|
// UploadAttachment uploads a file in bounded chunks. The first call carries
|
|
// the spec and returns an upload_id; later calls carry that upload_id.
|
|
// Uploads are bound to the authenticated user, expire after 30 minutes of
|
|
// inactivity, and do not survive a server restart.
|
|
rpc UploadAttachment(UploadAttachmentRequest) returns (UploadAttachmentResponse) {
|
|
option (google.api.http) = {
|
|
post: "/api/v1/attachments:upload"
|
|
body: "*"
|
|
};
|
|
}
|
|
// ListAttachments lists all attachments.
|
|
rpc ListAttachments(ListAttachmentsRequest) returns (ListAttachmentsResponse) {
|
|
option (google.api.http) = {get: "/api/v1/attachments"};
|
|
}
|
|
// GetAttachment returns an attachment by name.
|
|
rpc GetAttachment(GetAttachmentRequest) returns (Attachment) {
|
|
option (google.api.http) = {get: "/api/v1/{name=attachments/*}"};
|
|
option (google.api.method_signature) = "name";
|
|
}
|
|
// UpdateAttachment updates an attachment.
|
|
rpc UpdateAttachment(UpdateAttachmentRequest) returns (Attachment) {
|
|
option (google.api.http) = {
|
|
patch: "/api/v1/{attachment.name=attachments/*}"
|
|
body: "attachment"
|
|
};
|
|
option (google.api.method_signature) = "attachment,update_mask";
|
|
}
|
|
// DeleteAttachment deletes an attachment by name.
|
|
rpc DeleteAttachment(DeleteAttachmentRequest) returns (google.protobuf.Empty) {
|
|
option (google.api.http) = {delete: "/api/v1/{name=attachments/*}"};
|
|
option (google.api.method_signature) = "name";
|
|
}
|
|
// BatchDeleteAttachments deletes multiple attachments in one request.
|
|
rpc BatchDeleteAttachments(BatchDeleteAttachmentsRequest) returns (google.protobuf.Empty) {
|
|
option (google.api.http) = {
|
|
post: "/api/v1/attachments:batchDelete"
|
|
body: "*"
|
|
};
|
|
}
|
|
}
|
|
|
|
enum MotionMediaFamily {
|
|
MOTION_MEDIA_FAMILY_UNSPECIFIED = 0;
|
|
APPLE_LIVE_PHOTO = 1;
|
|
ANDROID_MOTION_PHOTO = 2;
|
|
}
|
|
|
|
enum MotionMediaRole {
|
|
MOTION_MEDIA_ROLE_UNSPECIFIED = 0;
|
|
STILL = 1;
|
|
VIDEO = 2;
|
|
CONTAINER = 3;
|
|
}
|
|
|
|
message MotionMedia {
|
|
MotionMediaFamily family = 1;
|
|
MotionMediaRole role = 2;
|
|
string group_id = 3;
|
|
int64 presentation_timestamp_us = 4;
|
|
bool has_embedded_video = 5;
|
|
}
|
|
|
|
// MediaMetadata contains normalized metadata explicitly supplied by a client.
|
|
// The server validates and stores this data but does not extract it from the media file.
|
|
message MediaMetadata {
|
|
// Optional. Display-oriented width in pixels.
|
|
optional int32 width = 1;
|
|
// Optional. Display-oriented height in pixels.
|
|
optional int32 height = 2;
|
|
|
|
oneof details {
|
|
PhotoMetadata photo = 3;
|
|
VideoMetadata video = 4;
|
|
}
|
|
}
|
|
|
|
message PhotoMetadata {
|
|
// Optional. Capture time as recorded by the source media.
|
|
MediaCaptureTime capture_time = 1;
|
|
// Optional. Geographic location recorded by the source media.
|
|
MediaLocation location = 2;
|
|
// Optional. EXIF orientation value from 1 through 8 as recorded by the source file.
|
|
// This value is informational and must not be reapplied to the stored attachment;
|
|
// width and height already describe its display-oriented dimensions.
|
|
optional int32 source_exif_orientation = 3;
|
|
string camera_make = 4;
|
|
string camera_model = 5;
|
|
string lens_model = 6;
|
|
optional double f_number = 7;
|
|
optional double exposure_time_seconds = 8;
|
|
optional int32 iso = 9;
|
|
optional double focal_length_mm = 10;
|
|
}
|
|
|
|
message MediaCaptureTime {
|
|
// Local date and time without a time zone, formatted as YYYY-MM-DDTHH:mm:ss[.fraction].
|
|
string local_date_time = 1;
|
|
// Optional. UTC offset formatted as Z or +/-HH:MM.
|
|
optional string utc_offset = 2;
|
|
}
|
|
|
|
message MediaLocation {
|
|
// Optional. WGS84 latitude in decimal degrees. Must be provided with longitude.
|
|
optional double latitude = 1;
|
|
// Optional. WGS84 longitude in decimal degrees. Must be provided with latitude.
|
|
optional double longitude = 2;
|
|
// Optional. Signed altitude in meters relative to sea level.
|
|
optional double altitude_meters = 3;
|
|
}
|
|
|
|
message VideoMetadata {
|
|
optional double duration_seconds = 1;
|
|
}
|
|
|
|
message Attachment {
|
|
option (google.api.resource) = {
|
|
type: "memos.api.v1/Attachment"
|
|
pattern: "attachments/{attachment}"
|
|
singular: "attachment"
|
|
plural: "attachments"
|
|
};
|
|
|
|
// The name of the attachment.
|
|
// Format: attachments/{attachment}
|
|
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
|
|
|
|
// Output only. The creation timestamp.
|
|
google.protobuf.Timestamp create_time = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
|
|
|
|
// The filename of the attachment.
|
|
string filename = 3 [(google.api.field_behavior) = REQUIRED];
|
|
|
|
// Input only. The content of the attachment.
|
|
bytes content = 4 [(google.api.field_behavior) = INPUT_ONLY];
|
|
|
|
// Optional. The external link of the attachment.
|
|
string external_link = 5 [(google.api.field_behavior) = OPTIONAL];
|
|
|
|
// The MIME type of the attachment.
|
|
string type = 6 [(google.api.field_behavior) = REQUIRED];
|
|
|
|
// Output only. The size of the attachment in bytes.
|
|
int64 size = 7 [(google.api.field_behavior) = OUTPUT_ONLY];
|
|
|
|
// Optional. The related memo. Refer to `Memo.name`.
|
|
// Format: memos/{memo}
|
|
optional string memo = 8 [(google.api.field_behavior) = OPTIONAL];
|
|
|
|
// Optional. Motion media metadata.
|
|
MotionMedia motion_media = 9 [(google.api.field_behavior) = OPTIONAL];
|
|
|
|
// Optional. Immutable normalized media metadata explicitly supplied by the client at creation time.
|
|
MediaMetadata media_metadata = 10 [
|
|
(google.api.field_behavior) = OPTIONAL,
|
|
(google.api.field_behavior) = IMMUTABLE
|
|
];
|
|
}
|
|
|
|
message CreateAttachmentRequest {
|
|
// Required. The attachment to create.
|
|
Attachment attachment = 1 [(google.api.field_behavior) = REQUIRED];
|
|
|
|
// Optional. The attachment ID to use for this attachment.
|
|
// If empty, a unique ID will be generated.
|
|
// Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
|
string attachment_id = 2 [(google.api.field_behavior) = OPTIONAL];
|
|
}
|
|
|
|
message UploadAttachmentRequest {
|
|
// Required. Start a new upload or continue an existing one.
|
|
oneof upload {
|
|
// Starts a new upload. The same call may also carry data and finish_write.
|
|
UploadAttachmentSpec spec = 1;
|
|
|
|
// Continues the upload identified by a previous response.
|
|
string upload_id = 2;
|
|
}
|
|
|
|
// Required. Zero-based byte offset at which data is written. Must equal the
|
|
// committed size, except when retrying the most recently accepted chunk
|
|
// with identical bytes and offset, which is accepted without writing again.
|
|
int64 write_offset = 3 [(google.api.field_behavior) = REQUIRED];
|
|
|
|
// Optional. File bytes, at most max_chunk_size long. With no data and
|
|
// finish_write false, the call reports progress without writing, and
|
|
// write_offset is ignored.
|
|
bytes data = 4 [(google.api.field_behavior) = OPTIONAL];
|
|
|
|
// Optional. Finalize the upload after writing data. The committed size must
|
|
// then equal total_size. Any later call for the same upload_id returns the
|
|
// created attachment.
|
|
bool finish_write = 5 [(google.api.field_behavior) = OPTIONAL];
|
|
}
|
|
|
|
message UploadAttachmentSpec {
|
|
// Required. Metadata for the attachment to create. content must be empty;
|
|
// file bytes are sent in data.
|
|
Attachment attachment = 1 [(google.api.field_behavior) = REQUIRED];
|
|
|
|
// Optional. The attachment ID to use for this attachment.
|
|
// If empty, a unique ID will be generated.
|
|
// Format: ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?$
|
|
string attachment_id = 2 [(google.api.field_behavior) = OPTIONAL];
|
|
|
|
// Optional. Total size of the file in bytes before media processing.
|
|
// Zero represents an empty file.
|
|
int64 total_size = 3 [(google.api.field_behavior) = OPTIONAL];
|
|
}
|
|
|
|
message UploadAttachmentResponse {
|
|
// Opaque ID for subsequent calls. This is not a resource name.
|
|
string upload_id = 1;
|
|
|
|
// Number of file bytes committed so far.
|
|
int64 committed_size = 2;
|
|
|
|
// Set once the upload has been finalized.
|
|
Attachment attachment = 3;
|
|
|
|
// Maximum number of data bytes accepted in one call.
|
|
int32 max_chunk_size = 4;
|
|
}
|
|
|
|
message ListAttachmentsRequest {
|
|
// Optional. The maximum number of attachments to return.
|
|
// The service may return fewer than this value.
|
|
// If unspecified, at most 50 attachments will be returned.
|
|
// The maximum value is 1000; values above 1000 will be coerced to 1000.
|
|
int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL];
|
|
|
|
// Optional. A page token, received from a previous `ListAttachments` call.
|
|
// Provide this to retrieve the subsequent page.
|
|
string page_token = 2 [(google.api.field_behavior) = OPTIONAL];
|
|
|
|
// Optional. Filter to apply to the list results.
|
|
// Example: "mime_type==\"image/png\"" or "filename.contains(\"test\")"
|
|
// Supported operators: =, !=, <, <=, >, >=, : (contains), in
|
|
// Supported fields: filename, mime_type, create_time, memo_id, space.
|
|
// `space` only supports a non-negated `==` comparison with a Space resource
|
|
// name or null.
|
|
// `space` is the linked memo's space resource name, or null when the
|
|
// attachment is unlinked or its linked memo has no space.
|
|
string filter = 3 [(google.api.field_behavior) = OPTIONAL];
|
|
|
|
// Optional. The order to sort results by.
|
|
// Example: "create_time desc" or "filename asc"
|
|
string order_by = 4 [(google.api.field_behavior) = OPTIONAL];
|
|
|
|
reserved 5, 6;
|
|
reserved "space", "unassigned";
|
|
}
|
|
|
|
message ListAttachmentsResponse {
|
|
// The list of attachments.
|
|
repeated Attachment attachments = 1;
|
|
|
|
// A token that can be sent as `page_token` to retrieve the next page.
|
|
// If this field is omitted, there are no subsequent pages.
|
|
string next_page_token = 2;
|
|
}
|
|
|
|
message GetAttachmentRequest {
|
|
// Required. The attachment name of the attachment to retrieve.
|
|
// Format: attachments/{attachment}
|
|
string name = 1 [
|
|
(google.api.field_behavior) = REQUIRED,
|
|
(google.api.resource_reference) = {type: "memos.api.v1/Attachment"}
|
|
];
|
|
}
|
|
|
|
message UpdateAttachmentRequest {
|
|
// Required. The attachment which replaces the attachment on the server.
|
|
Attachment attachment = 1 [(google.api.field_behavior) = REQUIRED];
|
|
|
|
// Required. The list of fields to update.
|
|
google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED];
|
|
}
|
|
|
|
message DeleteAttachmentRequest {
|
|
// Required. The attachment name of the attachment to delete.
|
|
// Format: attachments/{attachment}
|
|
string name = 1 [
|
|
(google.api.field_behavior) = REQUIRED,
|
|
(google.api.resource_reference) = {type: "memos.api.v1/Attachment"}
|
|
];
|
|
}
|
|
|
|
message BatchDeleteAttachmentsRequest {
|
|
repeated string names = 1 [(google.api.field_behavior) = REQUIRED];
|
|
}
|