filestoraged/src/common/filestorage.cr

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