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