From f87fd35a64bac2b39ac0fa229891ffe401e9330e Mon Sep 17 00:00:00 2001 From: Philippe PITTOLI Date: Thu, 2 Jan 2020 09:21:11 +0100 Subject: [PATCH] Sorting some files. --- shard.yml | 12 +- src/{client.cr => client/main.cr} | 66 +++++---- src/common.cr | 108 --------------- src/{ => common}/colors.cr | 0 src/common/filestorage.cr | 219 ++++++++++++++++++++++++++++++ src/json_tests.cr | 17 --- src/main.cr | 146 -------------------- src/server/cli.cr | 17 +++ src/server/context.cr | 26 ++++ src/server/handlers.cr | 132 ++++++++++++++++++ src/server/main-loop.cr | 142 +++++++++++++++++++ src/server/main.cr | 35 +++++ src/tests/common-tests.cr | 87 ++++++++++++ src/tests/json_tests.cr | 32 +++++ 14 files changed, 740 insertions(+), 299 deletions(-) rename src/{client.cr => client/main.cr} (57%) delete mode 100644 src/common.cr rename src/{ => common}/colors.cr (100%) create mode 100644 src/common/filestorage.cr delete mode 100644 src/json_tests.cr delete mode 100644 src/main.cr create mode 100644 src/server/cli.cr create mode 100644 src/server/context.cr create mode 100644 src/server/handlers.cr create mode 100644 src/server/main-loop.cr create mode 100644 src/server/main.cr create mode 100644 src/tests/common-tests.cr create mode 100644 src/tests/json_tests.cr diff --git a/shard.yml b/shard.yml index 2e9474f..f4df7ca 100644 --- a/shard.yml +++ b/shard.yml @@ -16,11 +16,13 @@ dependencies: git: https://github.com/Lukc/authd targets: + common-tests: + main: src/tests/common-tests.cr json-tests: - main: src/json_tests.cr - filestorage: - main: src/main.cr - filestorageclient: - main: src/client.cr + main: src/tests/json_tests.cr + server: + main: src/server/main.cr + client: + main: src/client/main.cr license: MIT diff --git a/src/client.cr b/src/client/main.cr similarity index 57% rename from src/client.cr rename to src/client/main.cr index 7e0052d..3612557 100644 --- a/src/client.cr +++ b/src/client/main.cr @@ -2,7 +2,11 @@ require "option_parser" require "ipc" require "json" -require "./common.cr" +require "base64" + +require "../common/filestorage.cr" + +alias FM = FileStorage::Message # TODO # For now, this example only upload files. @@ -13,7 +17,7 @@ service_name = "filestorage" files_and_directories_to_transfer = Array(String).new # This is the requests we will send to the server -requets = Array(Requets).new +requests = Array(FM::Request).new OptionParser.parse do |parser| @@ -29,6 +33,7 @@ OptionParser.parse do |parser| parser.on "-h", "--help", "Show this help" do puts parser + puts "program [OPTIONS] " exit 0 end end @@ -39,7 +44,8 @@ end # For now, we only want to upload files, so we create an UploadRequest # -files_info = Array(FileInfo).new +files_info = Hash(String, FileStorage::FileInfo).new + puts "files and directories to transfer" files_and_directories_to_transfer.each do |f| @@ -48,7 +54,7 @@ files_and_directories_to_transfer.each do |f| puts "Directories not supported, for now" elsif File.file?(f) && File.readable? f File.open(f) do |file| - files_info << FileInfo.new file + files_info[file.path] = FileStorage::FileInfo.new file end else if ! File.exists? f @@ -61,9 +67,11 @@ files_and_directories_to_transfer.each do |f| end end -pp! files_info +files_info.values.each do |file_info| + requests << FM::UploadRequest.new file_info +end -requests << UploadRequest.new files_info +pp! requests # # Connection to the service @@ -75,11 +83,9 @@ client = IPC::Client.new service_name # Sending the authentication message, including files info # -token = Token.new 1002, "karchnu" -authentication_message = AuthenticationMessage.new token, files_info - - -client.send(1.to_u8, authentication_message.to_json) +token = FileStorage::Token.new 1002, "karchnu" +authentication_message = FM::Authentication.new token, requests +client.send FileStorage::MessageType::Authentication.to_u8, authentication_message.to_json # # Receiving a response @@ -89,29 +95,43 @@ m = client.read # puts "message received: #{m.to_s}" # puts "message received payload: #{String.new m.payload}" -response = Response.from_json(String.new m.payload) +response = FM::Response.from_json(String.new m.payload) if response.mid == authentication_message.mid puts "This is a response for the authentication message" else - puts "Message IDs from authentication message and its response differ" + raise "Message IDs from authentication message and its response differ" end # # file transfer # -puts "transfer" -files_and_directories_to_transfer.each do |f| - puts "- #{f}" +def file_transfer(client : IPC::Client, file : File, file_info : FileStorage::FileInfo) + buffer_size = 1_000 - if File.directory? f - # TODO - elsif File.file?(f) && File.readable? f - File.open(f) do |file| - # TODO - # file - end + buffer = Bytes.new buffer_size + counter = 1 + size = 0 + + while (size = file.read(buffer)) > 0 + # transfer message = file_info, chunk count, data (will be base64'd) + transfer_message = FM::Transfer.new file_info, counter, buffer[0 ... size] + + client.send FileStorage::MessageType::Transfer.to_u8, transfer_message.to_json + counter += 1 + + buffer = Bytes.new buffer_size + end +end + +puts "transfer" + +files_info.keys.each do |file_path| + puts "- #{file_path}" + + File.open(file_path) do |file| + file_transfer client, file, files_info[file_path] end end diff --git a/src/common.cr b/src/common.cr deleted file mode 100644 index 2e810c9..0000000 --- a/src/common.cr +++ /dev/null @@ -1,108 +0,0 @@ -require "uuid" - -enum MessageType - Error - AuthenticationMessage - Response - Transfer -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 - JSON.mapping({ - uid: Int32, - 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 - JSON.mapping({ - name: String, - size: UInt64, - # list of SHA256, if we are on UDP - # chunks: Array(SHA256), - tags: Array(String)? - }) - - # debugging constructor - def initialize(@name, @size, @tags = nil) - # If on UDP - # @chunks = Array(SHA256).new - # arbitrary values here - end - - def initialize(file : File, @tags = nil) - @name = File.basename file.path - @size = file.size - end -end - -class Request -end - -class UploadRequest < Request - property files_to_upload : Array(FileInfo) - - def initialize(@files_to_upload) - end -end - - -# WIP -class DownloadRequest < Request - property names : Array(String)?, - property tags : Array(String)? - - def initialize(@names = nil, @tags = nil) - end -end - -class AuthenticationMessage - JSON.mapping({ - mid: String, - token: Token, - requests: Array(Requests) - }) - - def initialize(@token, @files, @tags = nil) - @mid = UUID.random.to_s - end -end - -class Response - JSON.mapping({ - mid: String, - response: String, - reason: String? - }) - - def initialize(@mid, @response, @reason = nil) - end -end - -class TransferMessage - JSON.mapping({ - mid: String, - chunk: String, - data: Slice(UInt8) - }) - - def initialize(@chunk, @data) - @mid = UUID.random.to_s - end -end diff --git a/src/colors.cr b/src/common/colors.cr similarity index 100% rename from src/colors.cr rename to src/common/colors.cr diff --git a/src/common/filestorage.cr b/src/common/filestorage.cr new file mode 100644 index 0000000..5281f55 --- /dev/null +++ b/src/common/filestorage.cr @@ -0,0 +1,219 @@ +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 + JSON.mapping({ + # chunk's number + n: Int32, + # number of chunks + on: Int32, + # digest of the current chunk + digest: String + }) + + 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 + JSON.mapping({ + uid: Int32, + 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 + JSON.mapping({ + name: String, + size: UInt64, + nb_chunks: Int32, + # SHA256 file digest + digest: String, + + # list of SHA256, if we are on UDP + # chunks: Array(SHA256), + 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 + end + end + + class Message + + alias Request = UploadRequest | DownloadRequest + + class UploadRequest + JSON.mapping({ + # autogenerated + mid: String, + file: FileInfo + }) + + def initialize(@file) + @mid = UUID.random.to_s + end + end + + + # WIP + class DownloadRequest + JSON.mapping({ + # autogenerated + mid: String, + # SHA256 digest of the file, used as ID + uuid: String?, + name: String?, + tags: Array(String)? + }) + + def initialize(@uuid = nil, @name = nil, @tags = nil) + @mid = UUID.random.to_s + end + end + + class Authentication + JSON.mapping({ + # autogenerated + mid: String, + token: Token, + requests: Array(Request) + }) + + def initialize(@token, @requests = Array(Request).new) + @mid = UUID.random.to_s + end + end + + class Response + JSON.mapping({ + mid: String, + response: String, + reason: String? + }) + + def initialize(@mid, @response, @reason = nil) + end + end + + class Error + JSON.mapping({ + mid: String, + # a response for each request + response: String, + reason: String? + }) + + def initialize(@mid, @response, @reason = nil) + end + end + + class Responses + JSON.mapping({ + mid: String, + # a response for each request + responses: Array(Response), + response: String, + reason: String? + }) + + def initialize(@mid, @response, @responses, @reason = nil) + end + end + + class Transfer + JSON.mapping({ + # autogenerated + mid: String, + # SHA256 digest of the entire file + filedigest: String, + # For now, just the counter in a string + chunk: Chunk, + # base64 slice + data: String, + }) + + 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, @data + @mid = UUID.random.to_s + end + 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 diff --git a/src/json_tests.cr b/src/json_tests.cr deleted file mode 100644 index 9060746..0000000 --- a/src/json_tests.cr +++ /dev/null @@ -1,17 +0,0 @@ -require "json" - -require "./common.cr" - -files_info = Array(FileInfo).new -files_info << FileInfo.new "file.txt", 4123.to_u64, %w(important truc machin) - -token = Token.new 1002, "karchnu" -authentication_message = AuthenticationMessage.new token, files_info - -# TODO, TEST, DEBUG, XXX, FIXME -pp! authentication_message.to_json - - -am_from_json = AuthenticationMessage.from_json authentication_message.to_json - -pp! am_from_json diff --git a/src/main.cr b/src/main.cr deleted file mode 100644 index d6f24a6..0000000 --- a/src/main.cr +++ /dev/null @@ -1,146 +0,0 @@ -require "option_parser" -require "ipc" -require "json" - -require "./colors" - -# require "dodb" - -require "./common.cr" - -storage_directory = "./storage" -service_name = "filestorage" - - -OptionParser.parse do |parser| - parser.on "-d storage-directory", - "--storage-directory storage-directory", - "The directory where to put uploaded files." do |opt| - storage_directory = opt - end - - parser.on "-s service-name", "--service-name service-name", "Service name." do |name| - service_name = name - end - - parser.on "-h", "--help", "Show this help" do - puts parser - exit 0 - end -end - - -# keep track of connected users and their requests -# TODO: requests should be handled concurrently -class User - property uid : Int32 - property token : Token - property requests : Array(Request) - - def initialize(@token) - @uid = token.uid - end -end - -# list of connected users -# fd => uid -connected_users = Hash(Int32, Int32).new -users_status = Hash(Int32, User).new - -service = IPC::SwitchingService.new service_name - -def receiving_files(user : User, event : IPC::Event::Message) -end - -# Could be the reception of a file or a file request -def request_handling(user : User, event : IPC::Event::Message) - puts "request handling" - - # - # Here we get requests from the message received - # - -end - -service.loop do |event| - case event - when IPC::Event::Timer - puts "#{CORANGE}IPC::Event::Timer#{CRESET}" - - when IPC::Event::Connection - puts "#{CBLUE}IPC::Event::Connection: #{event.connection.fd}#{CRESET}" - - when IPC::Event::Disconnection - puts "#{CBLUE}IPC::Event::Disconnection: #{event.connection.fd}#{CRESET}" - - connected_users.select! do |fd, uid| - fd != event.connection.fd - end - - when IPC::Event::ExtraSocket - puts "#{CRED}IPC::Event::ExtraSocket: should not happen in this service#{CRESET}" - - when IPC::Event::Switch - puts "#{CRED}IPC::Event::Switch: should not happen in this service#{CRESET}" - - # IPC::Event::Message has to be the last entry - # because ExtraSocket and Switch inherit from Message class - when IPC::Event::Message - puts "#{CBLUE}IPC::Event::Message#{CRESET}: #{event.connection.fd}" - - # 1. test if the client is already authenticated - if userid = connected_users[event.connection.fd]? - puts "User is connected: #{user.token.login}" - request_handling users_status[userid], event - else - puts "User is not currently connected" - - # The first message sent to the server has to be the AuthenticationMessage. - # Users sent their token (JWT) to authenticate themselves. - # The token contains the user id, its login and a few other parameters. - # (see the authd documentation). - authentication_message = - AuthenticationMessage.from_json( - String.new event.message.payload - ) - - # Is the user already recorded in users_status? - if users_status[authentication_message.token.uid]? - puts "We already knew the user #{authentication_message.token.uid}" - pp! users_status[authentication_message.token.uid] - else - # AuthenticationMessage includes requests. - new_user = - User.new authentication_message.token, - authentication_message.requests - - connected_users[event.connection.fd] = new_user.uid - - # record the new user in users_status - users_status[new_user.uid] = new_user - - puts "New user is: #{new_user.token.login}" - end - - # The user is now connected. - user = users_status[authentication_message.token.uid] - - # We verify the user's rights to upload files. - # TODO RIGHTS - # if user wants to upload but not allowed to: Response - # if user wants to get a file but not allowed to: Response - - # The user is authorized to upload files. - - # TODO: quotas - # Quotas are not defined yet. - - # Sending a response. - # The response is "Ok" when the message is well received and authorized. - response = Response.new authentication_message.mid, "Ok" - event.connection.send MessageType::Response.to_u8, response.to_json - end - else - raise "Event type not supported." - end -end diff --git a/src/server/cli.cr b/src/server/cli.cr new file mode 100644 index 0000000..83b3448 --- /dev/null +++ b/src/server/cli.cr @@ -0,0 +1,17 @@ + +OptionParser.parse do |parser| + parser.on "-d storage-directory", + "--storage-directory storage-directory", + "The directory where to put uploaded files." do |opt| + Context.storage_directory = opt + end + + parser.on "-s service-name", "--service-name service-name", "Service name." do |name| + Context.service_name = name + end + + parser.on "-h", "--help", "Show this help" do + puts parser + exit 0 + end +end diff --git a/src/server/context.cr b/src/server/context.cr new file mode 100644 index 0000000..111a060 --- /dev/null +++ b/src/server/context.cr @@ -0,0 +1,26 @@ + +# keep track of connected users and their requests +# TODO: requests should be handled concurrently +class User + property uid : Int32 + property token : FileStorage::Token + property requests : Array(FileStorage::Message::Request)? + + def initialize(@token, @requests = nil) + @uid = token.uid + end +end + +class Context + class_property service_name = "filestorage" + class_property storage_directory = "./storage" + + # list of connected users (fd => uid) + class_property connected_users = Hash(Int32, Int32).new + + # users_status: keep track of the users' status even if they are + # disconnected, allowing the application to handle connection problems + class_property users_status = Hash(Int32, User).new + + class_property service : IPC::Service? = nil +end diff --git a/src/server/handlers.cr b/src/server/handlers.cr new file mode 100644 index 0000000..ad5b629 --- /dev/null +++ b/src/server/handlers.cr @@ -0,0 +1,132 @@ + +require "dodb" +require "base64" + +# reception of a file chunk +def hdl_transfer(message : FileStorage::Message::Transfer, + user : User, + event : IPC::Event::Message) : FileStorage::Message::Response + puts "receiving a file" + + transfer_message = FileStorage::Message::Transfer.from_json( + String.new event.message.payload + ) + + pp! transfer_message + + puts "chunk: #{transfer_message.chunk}" + puts "data: #{Base64.decode transfer_message.data}" + + FileStorage::Message::Response.new message.mid, "Ok" +end + +# TODO +# the client sent an upload request +def hdl_upload(request : FileStorage::Message::UploadRequest, + user : User, + event : IPC::Event::Message) : FileStorage::Message::Response + + puts "hdl upload: mid=#{request.mid}" + pp! request + + FileStorage::Message::Response.new request.mid, "Upload OK" +end + +# TODO +# the client sent a download request +def hdl_download(request : FileStorage::Message::DownloadRequest, + user : User, + event : IPC::Event::Message) : FileStorage::Message::Response + + puts "hdl download: mid=#{request.mid}" + pp! request + + FileStorage::Message::Response.new request.mid, "Download OK" +end + + +# Entry point for request management +# Each request should have a response. +# Then, responses are sent in a single message. +def hdl_requests(requests : Array(FileStorage::Message::Request), + user : User, + event : IPC::Event::Message) : Array(FileStorage::Message::Response) + + puts "hdl request" + responses = Array(FileStorage::Message::Response).new + + requests.each do |request| + case request + when FileStorage::Message::DownloadRequest + responses << hdl_download request, user, event + when FileStorage::Message::UploadRequest + responses << hdl_upload request, user, event + else + raise "request not understood" + end + + puts + end + + responses +end + +# store the client in connected_users and users_status +# if already in users_status: +# check if the requests are the same +# if not: add them to the user structure in users_status +def hdl_authentication(event : IPC::Event::Message) + + authentication_message = + FileStorage::Message::Authentication.from_json( + String.new event.message.payload + ) + + userid = authentication_message.token.uid + + puts "user authentication: #{userid}" + + # Is the user already recorded in users_status? + if Context.users_status[userid]? + puts "We already knew this user" + + Context.connected_users[event.connection.fd] = userid + # TODO + pp! Context.connected_users + pp! Context.users_status[userid] + else + # AuthenticationMessage includes requests. + new_user = + User.new authentication_message.token, + authentication_message.requests + + Context.connected_users[event.connection.fd] = userid + + # record the new user in users_status + Context.users_status[userid] = new_user + + puts "New user is: #{new_user.token.login}" + end + + # The user is now connected. + user = Context.users_status[userid] + + # We verify the user's rights to upload files. + # TODO RIGHTS + # if user wants to upload but not allowed to: Response + # if user wants to get a file but not allowed to: Response + + # The user is authorized to upload files. + + # TODO: quotas + # Quotas are not defined yet. + + responses = hdl_requests authentication_message.requests, + Context.users_status[userid], + event + + # Sending a response, containing a response for each request. + # The response is "Ok" when the message is well received and authorized. + response = FileStorage::Message::Responses.new authentication_message.mid, "Ok", responses + event.connection.send FileStorage::MessageType::Responses.to_u8, response.to_json +end diff --git a/src/server/main-loop.cr b/src/server/main-loop.cr new file mode 100644 index 0000000..6fe57c4 --- /dev/null +++ b/src/server/main-loop.cr @@ -0,0 +1,142 @@ + +Context.service = IPC::Service.new Context.service_name + +Context.service.not_nil!.loop do |event| + case event + when IPC::Event::Timer + puts "#{CORANGE}IPC::Event::Timer#{CRESET}" + + when IPC::Event::Connection + puts "#{CBLUE}IPC::Event::Connection: #{event.connection.fd}#{CRESET}" + + when IPC::Event::Disconnection + puts "#{CBLUE}IPC::Event::Disconnection: #{event.connection.fd}#{CRESET}" + + Context.connected_users.select! do |fd, uid| + fd != event.connection.fd + end + + when IPC::Event::ExtraSocket + puts "#{CRED}IPC::Event::ExtraSocket: should not happen in this service#{CRESET}" + + when IPC::Event::Switch + puts "#{CRED}IPC::Event::Switch: should not happen in this service#{CRESET}" + + # IPC::Event::Message has to be the last entry + # because ExtraSocket and Switch inherit from Message class + when IPC::Event::Message + puts "#{CBLUE}IPC::Event::Message#{CRESET}: #{event.connection.fd}" + + # The first message sent to the server has to be the AuthenticationMessage. + # Users sent their token (JWT) to authenticate themselves. + # The token contains the user id, its login and a few other parameters. + # (see the authd documentation). + # TODO: for now, the token is replaced by a hardcoded one, for debugging + + mtype = FileStorage::MessageType.new event.message.type.to_i32 + + # First, the user has to be authenticated unless we are receiving its first message + userid = Context.connected_users[event.connection.fd]? + if ! userid + case mtype + when .authentication? + else + + mid = "message id not found" + m = String.new event.message.payload + + case mtype + when .authentication? + when .upload_request? + puts "Upload request" + request = FileStorage::Message::UploadRequest.from_json(m) + mid = request.mid + when .download_request? + puts "Download request" + request = FileStorage::Message::DownloadRequest.from_json(m) + mid = request.mid + when .response? + puts "Response message" + request = FileStorage::Message::Response.from_json(m) + mid = request.mid + raise "not implemented yet" + when .responses? + puts "Responses message" + request = FileStorage::Message::Responses.from_json(m) + mid = request.mid + raise "not implemented yet" + when .error? + puts "Error message" + request = FileStorage::Message::Error.from_json(m) + mid = request.mid + raise "not implemented yet" + when .transfer? + request = FileStorage::Message::Transfer.from_json(m) + mid = request.mid + else + raise "Event type not supported, message from a non connected user." + end + + response = FileStorage::Message::Response.new mid, "Not OK", "Action on non connected user" + event.connection.send FileStorage::MessageType::Response.to_u8, response.to_json + next + end + end + + case mtype + when .authentication? + puts "Receiving an authentication message" + # 1. test if the client is already authenticated + if userid + user = Context.users_status[userid] + raise "Authentication message while the user was already connected: this should not happen" + else + puts "User is not currently connected" + hdl_authentication event + end + when .upload_request? + puts "Upload request" + request = FileStorage::Message::UploadRequest.from_json( + String.new event.message.payload + ) + response = hdl_upload request, Context.users_status[userid], event + + event.connection.send FileStorage::MessageType::Response.to_u8, response.to_json + raise "not implemented yet" + when .download_request? + puts "Download request" + request = FileStorage::Message::DownloadRequest.from_json( + String.new event.message.payload + ) + response = hdl_download request, Context.users_status[userid], event + + event.connection.send FileStorage::MessageType::Response.to_u8, response.to_json + raise "not implemented yet" + when .response? + puts "Response message" + raise "not implemented yet" + when .responses? + puts "Responses message" + raise "not implemented yet" + when .error? + puts "Error message" + raise "not implemented yet" + when .transfer? + # throw an error if the user isn't recorded + unless user = Context.users_status[userid]? + raise "The user isn't recorded in the users_status structure" + end + + transfer = FileStorage::Message::Transfer.from_json( + String.new event.message.payload + ) + response = hdl_transfer transfer, Context.users_status[userid], event + + event.connection.send FileStorage::MessageType::Response.to_u8, response.to_json + + hdl_transfer transfer, user, event + end + else + raise "Event type not supported." + end +end diff --git a/src/server/main.cr b/src/server/main.cr new file mode 100644 index 0000000..49f1964 --- /dev/null +++ b/src/server/main.cr @@ -0,0 +1,35 @@ +require "option_parser" +require "ipc" +require "json" + +require "../common/colors" +require "../common/filestorage.cr" + +require "./context.cr" +require "./handlers.cr" + +# TODO: if the user is disconnected, we should ask him if it still want to process +# for old requests. +# +# Example: the user is on a web page, the connection is broken for some reason. +# The user can still browse the website, change page and discard what +# he was doing. Regardless of the result. With or without finishing to +# upload or download its files. + +# TODO: +# * elegantly handling errors +# * store the file, /files/userid/UID.bin for example: /files/1002/UID.bin +# * metadata should be in a dodb +# /storage/partitions/by_uid/UID.json -> content: +# data: /files/uid/UID.bin (storing raw files) +# uid: 1002 +# name: "The File About Things" +# size: 1500 +# tags: thing1 thing2 +# * authd integration +# * knowing which parts of the files are still to be sent +# * rights +# * quotas + +require "./cli.cr" +require "./main-loop.cr" diff --git a/src/tests/common-tests.cr b/src/tests/common-tests.cr new file mode 100644 index 0000000..393a668 --- /dev/null +++ b/src/tests/common-tests.cr @@ -0,0 +1,87 @@ +require "../common/filestorage.cr" + +# This file test the following code +# classes: +# * Chunk, FileInfo +# * UploadRequest, DownloadRequest +# * AuthenticationMessage, Response, TransferMessage +# functions: +# * data_digest, file_digest + +# data_digest +# `echo -n "coucou" | sha256sum` +# => 110812f67fa1e1f0117f6f3d70241c1a42a7b07711a93c2477cc516d9042f9db + +filename = "./README.md" + +data = "coucou".chomp.to_slice +pp! FileStorage.data_digest data + +puts + +# file_digest +# `cat README.md | sha256sum` +# => 79c66991a965185958a1efb17d12652bdd8dc2de0da89b2dc152e2eeb2e02eff +File.open(filename) do |file| + pp! FileStorage.file_digest file +end + +puts + +# Chunk +pp! FileStorage::Chunk.new 1, 2, "blablabla" + +puts + +# FileInfo +File.open(filename) do |file| + pp! FileStorage::FileInfo.new file, [ "tag1", "tag2" ] +end + +puts + +# Token +# XXX: should not exist, it will be replaced by an authd JWT token soon. +token = FileStorage::Token.new 1002, "jean-dupont" +pp! token + +puts + +# for later +requests = Array(FileStorage::Message::Request).new + +# UploadRequest +File.open(filename) do |file| + file_info = FileStorage::FileInfo.new file, [ "tag1", "tag2" ] + upload_request = FileStorage::Message::UploadRequest.new file_info + pp! upload_request + requests << upload_request +end + +puts + +# DownloadRequest +pp! FileStorage::Message::DownloadRequest.new uuid: "abc" +pp! FileStorage::Message::DownloadRequest.new name: "the other one" +pp! FileStorage::Message::DownloadRequest.new tags: [ "tag1", "tag2" ] + +puts + +# AuthenticationMessage +pp! FileStorage::Message::Authentication.new token, requests + +puts + +# Response +pp! FileStorage::Message::Response.new "UUID", "Ok" +pp! FileStorage::Message::Response.new "UUID", "Error", "Cannot store the file" + +puts + +# TransferMessage +File.open(filename) do |file| + file_info = FileStorage::FileInfo.new file, [ "tag1", "tag2" ] + + somedata = "coucou".to_slice + pp! FileStorage::Message::Transfer.new file_info, 1, somedata +end diff --git a/src/tests/json_tests.cr b/src/tests/json_tests.cr new file mode 100644 index 0000000..d0697d6 --- /dev/null +++ b/src/tests/json_tests.cr @@ -0,0 +1,32 @@ +require "json" + +require "../common/filestorage.cr" + +unless ARGV.size > 0 + raise "Usage: json_tests file" +end + +files_info = Array(FileStorage::FileInfo).new + +ARGV.each do |filename| + File.open(filename) do |file| + files_info << FileStorage::FileInfo.new file, %w(important truc machin) + end +end + + +token = FileStorage::Token.new 1002, "karchnu" + +requests = Array(FileStorage::Message::Request).new +files_info.each do |file_info| + requests << FileStorage::Message::UploadRequest.new file_info +end +authentication_message = FileStorage::Message::Authentication.new token, requests + +# TODO, TEST, DEBUG, XXX, FIXME +pp! authentication_message.to_json + + +am_from_json = FileStorage::Message::Authentication.from_json authentication_message.to_json + +pp! am_from_json