require "uuid" require "openssl" require "json" require "base64" module FileStorage extend self # 1 MB read buffer, on-disk def file_reading_buffer_size 1_000_000 end # 1 KB message data buffer, on-network def message_buffer_size 1_000 end class Exception < ::Exception end enum MessageType Error Authentication UploadRequest DownloadRequest Response Responses Transfer end class Chunk include JSON::Serializable property n : Int32 # chunk's number property on : Int32 # number of chunks property digest : String # digest of the current chunk def initialize(@n, @on, data) @digest = FileStorage.data_digest data.to_slice end end # For now, upload and download are sequentials. # In a future version, we will be able to send # arbitrary parts of each file. class Token include JSON::Serializable property uid : Int32 property login : String def initialize(@uid, @login) end end # Who knows, maybe someday we will be on UDP, too. #class SHA256 # JSON.mapping({ # chunk: Slice(UInt8) # }) #end # A file has a name, a size and tags. class FileInfo include JSON::Serializable property name : String property size : UInt64 property nb_chunks : Int32 property digest : String # list of SHA256, if we are on UDP # chunks: Array(SHA256), property tags : Array(String) def initialize(file : File, tags = nil) @name = File.basename file.path @size = file.size @digest = FileStorage.file_digest file @nb_chunks = (@size / FileStorage.message_buffer_size).ceil.to_i @tags = tags || [] of String end end class Message end alias Request = UploadRequest | DownloadRequest class UploadRequest < Message include JSON::Serializable property mid : String # autogenerated property file : FileInfo def initialize(@file : FileInfo) @mid = UUID.random.to_s end end # WIP class DownloadRequest < Message include JSON::Serializable property mid : String # autogenerated property filedigest : String? # SHA256 digest of the file, used as ID property name : String? property tags : Array(String)? def initialize(@filedigest = nil, @name = nil, @tags = nil) @mid = UUID.random.to_s end end class Authentication < Message include JSON::Serializable property mid : String # autogenerated property token : Token property uploads : Array(UploadRequest) property downloads : Array(DownloadRequest) def initialize(@token, @uploads = Array(UploadRequest).new, @downloads = Array(DownloadRequest).new) @mid = UUID.random.to_s end end class Response < Message include JSON::Serializable property mid : String property response : String property reason : String? def initialize(@mid, @response, @reason = nil) end end class Error < Message include JSON::Serializable property mid : String property response : String # a response for each request property reason : String? def initialize(@mid, @response, @reason = nil) end end class Responses < Message include JSON::Serializable property mid : String property responses : Array(Response) # a response for each request property response : String property reason : String? def initialize(@mid, @response, @responses, @reason = nil) end end class Transfer < Message include JSON::Serializable property mid : String # autogenerated property filedigest : String # SHA256 digest of the entire file property chunk : Chunk # For now, just the counter in a string property data : String # base64 slice def initialize(file_info : FileInfo, count, bindata) # count: chunk number @filedigest = file_info.digest @data = Base64.encode bindata @chunk = FileStorage::Chunk.new count, file_info.nb_chunks - 1, @data @mid = UUID.random.to_s end end # private function def data_digest(data : Bytes) iodata = IO::Memory.new data, false buffer = Bytes.new FileStorage.file_reading_buffer_size io = OpenSSL::DigestIO.new(iodata, "SHA256") while io.read(buffer) > 0; end io.digest.hexstring end # private function def file_digest(file : File) # 1M read buffer buffer = Bytes.new(1_000_000) io = OpenSSL::DigestIO.new(file, "SHA256") while io.read(buffer) > 0 ; end io.digest.hexstring end end