syntax = "proto3";

package protocol;

import "ext.proto";
import "repos/protobuf/gogoproto/gogo.proto";

// --- Pre-auth ---

message Hello {
    string device_name     = 1;
    string client_name     = 2;
    string client_version  = 3;
    int32  num_connections = 4;
    int64  timestamp       = 5;
}

// --- Header ---

message Header {
    MessageType        type        = 1;
    MessageCompression compression = 2;
}

enum MessageType {
    MESSAGE_TYPE_CLUSTER_CONFIG    = 0;
    MESSAGE_TYPE_INDEX             = 1;
    MESSAGE_TYPE_INDEX_UPDATE      = 2;
    MESSAGE_TYPE_REQUEST           = 3;
    MESSAGE_TYPE_RESPONSE          = 4;
    MESSAGE_TYPE_DOWNLOAD_PROGRESS = 5;
    MESSAGE_TYPE_PING              = 6;
    MESSAGE_TYPE_CLOSE             = 7;
}

enum MessageCompression {
    MESSAGE_COMPRESSION_NONE = 0;
    MESSAGE_COMPRESSION_LZ4  = 1 [(ext.enumgoname) = "MessageCompressionLZ4"];
}

// --- Actual messages ---

// Cluster Config

message ClusterConfig {
    repeated Folder folders   = 1;
    bool            secondary = 2;
}

message Folder {
    string id                   = 1 [(ext.goname) = "ID"];
    string label                = 2;
    bool   read_only            = 3;
    bool   ignore_permissions   = 4;
    bool   ignore_delete        = 5;
    bool   disable_temp_indexes = 6;
    bool   paused               = 7;

    repeated Device devices = 16;
}

message Device {
    bytes           id                         = 1 [(ext.goname) = "ID", (ext.device_id) = true];
    string          name                       = 2;
    repeated string addresses                  = 3;
    Compression     compression                = 4;
    string          cert_name                  = 5;
    int64           max_sequence               = 6;
    bool            introducer                 = 7;
    uint64          index_id                   = 8 [(ext.goname) = "IndexID", (ext.gotype) = "IndexID"];
    bool            skip_introduction_removals = 9;
    bytes           encryption_password_token  = 10;
}

enum Compression {
    COMPRESSION_METADATA = 0;
    COMPRESSION_NEVER    = 1;
    COMPRESSION_ALWAYS   = 2;
}

// Index and Index Update

message Index {
    string            folder = 1;
    repeated FileInfo files  = 2;
}

message IndexUpdate {
    string            folder = 1;
    repeated FileInfo files  = 2;
}

message FileInfo {
    option (gogoproto.goproto_stringer) = false;

    // The field ordering here optimizes for struct size / alignment --
    // large types come before smaller ones.

    string             name           = 1;
    int64              size           = 3;
    int64              modified_s     = 5;
    uint64             modified_by    = 12 [(ext.gotype) = "ShortID"];
    Vector             version        = 9;
    int64              sequence       = 10;
    repeated BlockInfo blocks         = 16;
    string             symlink_target = 17;
    bytes              blocks_hash    = 18;
    bytes              encrypted      = 19;
    FileInfoType       type           = 2;
    uint32             permissions    = 4;
    int32              modified_ns    = 11;
    int32              block_size     = 13 [(ext.goname) = "RawBlockSize"];
    PlatformData       platform       = 14;

    // The local_flags fields stores flags that are relevant to the local
    // host only. It is not part of the protocol, doesn't get sent or
    // received (we make sure to zero it), nonetheless we need it on our
    // struct and to be able to serialize it to/from the database.
    uint32 local_flags = 1000;

    // The version_hash is an implementation detail and not part of the wire
    // format.
    bytes version_hash = 1001;

    // The time when the inode was last changed (i.e., permissions, xattrs
    // etc changed). This is host-local, not sent over the wire.
    int64 inode_change_ns = 1002;

    // The size of the data appended to the encrypted file on disk. This is
    // host-local, not sent over the wire.
    int32 encryption_trailer_size = 1003;

    bool deleted        = 6;
    bool invalid        = 7 [(ext.goname) = "RawInvalid"];
    bool no_permissions = 8;
}

enum FileInfoType {
    FILE_INFO_TYPE_FILE              = 0;
    FILE_INFO_TYPE_DIRECTORY         = 1;
    FILE_INFO_TYPE_SYMLINK_FILE      = 2 [deprecated = true];
    FILE_INFO_TYPE_SYMLINK_DIRECTORY = 3 [deprecated = true];
    FILE_INFO_TYPE_SYMLINK           = 4;
}

message BlockInfo {
    option (gogoproto.goproto_stringer) = false;
    bytes  hash      = 3;
    int64  offset    = 1;
    int32  size      = 2;
    uint32 weak_hash = 4;
}

message Vector {
    repeated Counter counters = 1;
}

message Counter {
    uint64 id    = 1 [(ext.goname) = "ID", (ext.gotype) = "ShortID"];
    uint64 value = 2;
}

message PlatformData {
    UnixData    unix    = 1 [(gogoproto.nullable) = true];
    WindowsData windows = 2 [(gogoproto.nullable) = true];
    XattrData   linux   = 3 [(gogoproto.nullable) = true];
    XattrData   darwin  = 4 [(gogoproto.nullable) = true];
    XattrData   freebsd = 5 [(gogoproto.nullable) = true, (ext.goname) = "FreeBSD"];
    XattrData   netbsd  = 6 [(gogoproto.nullable) = true, (ext.goname) = "NetBSD"];
}

message UnixData {
    // The owner name and group name are set when known (i.e., could be
    // resolved on the source device), while the UID and GID are always set
    // as they come directly from the stat() call.
    string owner_name = 1;
    string group_name = 2;
    int32  uid        = 3 [(ext.goname) = "UID"];
    int32  gid        = 4 [(ext.goname) = "GID"];
}

message WindowsData {
    // Windows file objects have a single owner, which may be a user or a
    // group. We keep the name of that account, and a flag to indicate what
    // type it is.
    string owner_name     = 1;
    bool   owner_is_group = 2;
}

message XattrData {
    repeated Xattr xattrs = 1;
}

message Xattr {
    string name  = 1;
    bytes  value = 2;
}

// Request

message Request {
    int32  id             = 1 [(ext.goname) = "ID"];
    string folder         = 2;
    string name           = 3;
    int64  offset         = 4;
    int32  size           = 5;
    bytes  hash           = 6;
    bool   from_temporary = 7;
    uint32 weak_hash      = 8;
    int32  block_no       = 9;
}

// Response

message Response {
    int32     id   = 1 [(ext.goname) = "ID"];
    bytes     data = 2;
    ErrorCode code = 3;
}

enum ErrorCode {
    ERROR_CODE_NO_ERROR     = 0;
    ERROR_CODE_GENERIC      = 1;
    ERROR_CODE_NO_SUCH_FILE = 2;
    ERROR_CODE_INVALID_FILE = 3;
}

// DownloadProgress

message DownloadProgress {
    string                              folder  = 1;
    repeated FileDownloadProgressUpdate updates = 2;
}

message FileDownloadProgressUpdate {
    FileDownloadProgressUpdateType update_type   = 1;
    string                         name          = 2;
    Vector                         version       = 3;
    repeated int32                 block_indexes = 4 [packed=false];
    int32                          block_size    = 5;
}

enum FileDownloadProgressUpdateType {
    FILE_DOWNLOAD_PROGRESS_UPDATE_TYPE_APPEND = 0;
    FILE_DOWNLOAD_PROGRESS_UPDATE_TYPE_FORGET = 1;
}

// Ping

message Ping {
}

// Close

message Close {
    string reason = 1;
}

