208 lines
4.3 KiB
Crystal
208 lines
4.3 KiB
Crystal
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
|