Sorting some files.
This commit is contained in:
parent
d6aa9f35fb
commit
f87fd35a64
12
shard.yml
12
shard.yml
@ -16,11 +16,13 @@ dependencies:
|
|||||||
git: https://github.com/Lukc/authd
|
git: https://github.com/Lukc/authd
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
|
common-tests:
|
||||||
|
main: src/tests/common-tests.cr
|
||||||
json-tests:
|
json-tests:
|
||||||
main: src/json_tests.cr
|
main: src/tests/json_tests.cr
|
||||||
filestorage:
|
server:
|
||||||
main: src/main.cr
|
main: src/server/main.cr
|
||||||
filestorageclient:
|
client:
|
||||||
main: src/client.cr
|
main: src/client/main.cr
|
||||||
|
|
||||||
license: MIT
|
license: MIT
|
||||||
|
@ -2,7 +2,11 @@ require "option_parser"
|
|||||||
require "ipc"
|
require "ipc"
|
||||||
require "json"
|
require "json"
|
||||||
|
|
||||||
require "./common.cr"
|
require "base64"
|
||||||
|
|
||||||
|
require "../common/filestorage.cr"
|
||||||
|
|
||||||
|
alias FM = FileStorage::Message
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
# For now, this example only upload files.
|
# For now, this example only upload files.
|
||||||
@ -13,7 +17,7 @@ service_name = "filestorage"
|
|||||||
files_and_directories_to_transfer = Array(String).new
|
files_and_directories_to_transfer = Array(String).new
|
||||||
|
|
||||||
# This is the requests we will send to the server
|
# This is the requests we will send to the server
|
||||||
requets = Array(Requets).new
|
requests = Array(FM::Request).new
|
||||||
|
|
||||||
|
|
||||||
OptionParser.parse do |parser|
|
OptionParser.parse do |parser|
|
||||||
@ -29,6 +33,7 @@ OptionParser.parse do |parser|
|
|||||||
|
|
||||||
parser.on "-h", "--help", "Show this help" do
|
parser.on "-h", "--help", "Show this help" do
|
||||||
puts parser
|
puts parser
|
||||||
|
puts "program [OPTIONS] <files-to-upload>"
|
||||||
exit 0
|
exit 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -39,7 +44,8 @@ end
|
|||||||
# For now, we only want to upload files, so we create an UploadRequest
|
# 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"
|
puts "files and directories to transfer"
|
||||||
files_and_directories_to_transfer.each do |f|
|
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"
|
puts "Directories not supported, for now"
|
||||||
elsif File.file?(f) && File.readable? f
|
elsif File.file?(f) && File.readable? f
|
||||||
File.open(f) do |file|
|
File.open(f) do |file|
|
||||||
files_info << FileInfo.new file
|
files_info[file.path] = FileStorage::FileInfo.new file
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
if ! File.exists? f
|
if ! File.exists? f
|
||||||
@ -61,9 +67,11 @@ files_and_directories_to_transfer.each do |f|
|
|||||||
end
|
end
|
||||||
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
|
# Connection to the service
|
||||||
@ -75,11 +83,9 @@ client = IPC::Client.new service_name
|
|||||||
# Sending the authentication message, including files info
|
# Sending the authentication message, including files info
|
||||||
#
|
#
|
||||||
|
|
||||||
token = Token.new 1002, "karchnu"
|
token = FileStorage::Token.new 1002, "karchnu"
|
||||||
authentication_message = AuthenticationMessage.new token, files_info
|
authentication_message = FM::Authentication.new token, requests
|
||||||
|
client.send FileStorage::MessageType::Authentication.to_u8, authentication_message.to_json
|
||||||
|
|
||||||
client.send(1.to_u8, authentication_message.to_json)
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Receiving a response
|
# Receiving a response
|
||||||
@ -89,29 +95,43 @@ m = client.read
|
|||||||
# puts "message received: #{m.to_s}"
|
# puts "message received: #{m.to_s}"
|
||||||
# puts "message received payload: #{String.new m.payload}"
|
# 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
|
if response.mid == authentication_message.mid
|
||||||
puts "This is a response for the authentication message"
|
puts "This is a response for the authentication message"
|
||||||
else
|
else
|
||||||
puts "Message IDs from authentication message and its response differ"
|
raise "Message IDs from authentication message and its response differ"
|
||||||
end
|
end
|
||||||
|
|
||||||
#
|
#
|
||||||
# file transfer
|
# file transfer
|
||||||
#
|
#
|
||||||
|
|
||||||
puts "transfer"
|
def file_transfer(client : IPC::Client, file : File, file_info : FileStorage::FileInfo)
|
||||||
files_and_directories_to_transfer.each do |f|
|
buffer_size = 1_000
|
||||||
puts "- #{f}"
|
|
||||||
|
|
||||||
if File.directory? f
|
buffer = Bytes.new buffer_size
|
||||||
# TODO
|
counter = 1
|
||||||
elsif File.file?(f) && File.readable? f
|
size = 0
|
||||||
File.open(f) do |file|
|
|
||||||
# TODO
|
while (size = file.read(buffer)) > 0
|
||||||
# file
|
# transfer message = file_info, chunk count, data (will be base64'd)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
108
src/common.cr
108
src/common.cr
@ -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
|
|
219
src/common/filestorage.cr
Normal file
219
src/common/filestorage.cr
Normal file
@ -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
|
@ -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
|
|
146
src/main.cr
146
src/main.cr
@ -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
|
|
17
src/server/cli.cr
Normal file
17
src/server/cli.cr
Normal file
@ -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
|
26
src/server/context.cr
Normal file
26
src/server/context.cr
Normal file
@ -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
|
132
src/server/handlers.cr
Normal file
132
src/server/handlers.cr
Normal file
@ -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
|
142
src/server/main-loop.cr
Normal file
142
src/server/main-loop.cr
Normal file
@ -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
|
35
src/server/main.cr
Normal file
35
src/server/main.cr
Normal file
@ -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"
|
87
src/tests/common-tests.cr
Normal file
87
src/tests/common-tests.cr
Normal file
@ -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
|
32
src/tests/json_tests.cr
Normal file
32
src/tests/json_tests.cr
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user