2023-01-05 12:25:55 +00:00
/**************************************************************************/
/* editor_file_server.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
2018-01-04 23:50:27 +00:00
2014-02-10 01:10:30 +00:00
# include "editor_file_server.h"
2017-01-16 07:04:19 +00:00
2017-03-05 15:44:50 +00:00
# include "../editor_settings.h"
2018-09-11 16:13:45 +00:00
# include "core/io/marshalls.h"
2023-04-28 11:15:36 +00:00
# include "editor/editor_node.h"
# include "editor/export/editor_export_platform.h"
# define FILESYSTEM_PROTOCOL_VERSION 1
# define PASSWORD_LENGTH 32
# define MAX_FILE_BUFFER_SIZE 100 * 1024 * 1024 // 100mb max file buffer size (description of files to update, compressed).
static void _add_file ( String f , const uint64_t & p_modified_time , HashMap < String , uint64_t > & files_to_send , HashMap < String , uint64_t > & cached_files ) {
f = f . replace_first ( " res:// " , " " ) ; // remove res://
const uint64_t * cached_mt = cached_files . getptr ( f ) ;
if ( cached_mt & & * cached_mt = = p_modified_time ) {
// File is good, skip it.
2023-05-08 12:49:51 +00:00
cached_files . erase ( f ) ; // Erase to mark this file as existing. Remaining files not added to files_to_send will be considered erased here, so they need to be erased in the client too.
2023-04-28 11:15:36 +00:00
return ;
}
files_to_send . insert ( f , p_modified_time ) ;
}
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
void EditorFileServer : : _scan_files_changed ( EditorFileSystemDirectory * efd , const Vector < String > & p_tags , HashMap < String , uint64_t > & files_to_send , HashMap < String , uint64_t > & cached_files ) {
for ( int i = 0 ; i < efd - > get_file_count ( ) ; i + + ) {
String f = efd - > get_file_path ( i ) ;
if ( FileAccess : : exists ( f + " .import " ) ) {
// is imported, determine what to do
// Todo the modified times of remapped files should most likely be kept in EditorFileSystem to speed this up in the future.
Ref < ConfigFile > cf ;
cf . instantiate ( ) ;
Error err = cf - > load ( f + " .import " ) ;
ERR_CONTINUE ( err ! = OK ) ;
{
uint64_t mt = FileAccess : : get_modified_time ( f + " .import " ) ;
_add_file ( f + " .import " , mt , files_to_send , cached_files ) ;
}
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
if ( ! cf - > has_section ( " remap " ) ) {
continue ;
}
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
List < String > remaps ;
cf - > get_section_keys ( " remap " , & remaps ) ;
for ( const String & remap : remaps ) {
if ( remap = = " path " ) {
String remapped_path = cf - > get_value ( " remap " , remap ) ;
uint64_t mt = FileAccess : : get_modified_time ( remapped_path ) ;
_add_file ( remapped_path , mt , files_to_send , cached_files ) ;
} else if ( remap . begins_with ( " path. " ) ) {
String feature = remap . get_slice ( " . " , 1 ) ;
if ( p_tags . find ( feature ) ! = - 1 ) {
String remapped_path = cf - > get_value ( " remap " , remap ) ;
uint64_t mt = FileAccess : : get_modified_time ( remapped_path ) ;
_add_file ( remapped_path , mt , files_to_send , cached_files ) ;
}
}
}
} else {
uint64_t mt = efd - > get_file_modified_time ( i ) ;
_add_file ( f , mt , files_to_send , cached_files ) ;
}
2020-02-26 10:28:13 +00:00
}
2023-04-28 11:15:36 +00:00
for ( int i = 0 ; i < efd - > get_subdir_count ( ) ; i + + ) {
_scan_files_changed ( efd - > get_subdir ( i ) , p_tags , files_to_send , cached_files ) ;
2014-02-10 01:10:30 +00:00
}
}
2023-04-28 11:15:36 +00:00
static void _add_custom_file ( const String f , HashMap < String , uint64_t > & files_to_send , HashMap < String , uint64_t > & cached_files ) {
if ( ! FileAccess : : exists ( f ) ) {
return ;
}
_add_file ( f , FileAccess : : get_modified_time ( f ) , files_to_send , cached_files ) ;
}
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
void EditorFileServer : : poll ( ) {
if ( ! active ) {
return ;
2014-02-10 01:10:30 +00:00
}
2023-04-28 11:15:36 +00:00
if ( ! server - > is_connection_available ( ) ) {
return ;
2014-02-10 01:10:30 +00:00
}
2023-04-28 11:15:36 +00:00
Ref < StreamPeerTCP > tcp_peer = server - > take_connection ( ) ;
ERR_FAIL_COND ( tcp_peer . is_null ( ) ) ;
// Got a connection!
EditorProgress pr ( " updating_remote_file_system " , TTR ( " Updating assets on target device: " ) , 105 ) ;
2023-06-12 12:57:08 +00:00
pr . step ( TTR ( " Syncing headers " ) , 0 , true ) ;
2023-04-28 11:15:36 +00:00
print_verbose ( " EFS: Connecting taken! " ) ;
char header [ 4 ] ;
Error err = tcp_peer - > get_data ( ( uint8_t * ) & header , 4 ) ;
ERR_FAIL_COND ( err ! = OK ) ;
ERR_FAIL_COND ( header [ 0 ] ! = ' G ' ) ;
ERR_FAIL_COND ( header [ 1 ] ! = ' R ' ) ;
ERR_FAIL_COND ( header [ 2 ] ! = ' F ' ) ;
ERR_FAIL_COND ( header [ 3 ] ! = ' S ' ) ;
uint32_t protocol_version = tcp_peer - > get_u32 ( ) ;
ERR_FAIL_COND ( protocol_version ! = FILESYSTEM_PROTOCOL_VERSION ) ;
char cpassword [ PASSWORD_LENGTH + 1 ] ;
err = tcp_peer - > get_data ( ( uint8_t * ) cpassword , PASSWORD_LENGTH ) ;
cpassword [ PASSWORD_LENGTH ] = 0 ;
ERR_FAIL_COND ( err ! = OK ) ;
print_verbose ( " EFS: Got password: " + String ( cpassword ) ) ;
ERR_FAIL_COND_MSG ( password ! = cpassword , " Client disconnected because password mismatch. " ) ;
uint32_t tag_count = tcp_peer - > get_u32 ( ) ;
print_verbose ( " EFS: Getting tags: " + itos ( tag_count ) ) ;
ERR_FAIL_COND ( tcp_peer - > get_status ( ) ! = StreamPeerTCP : : STATUS_CONNECTED ) ;
Vector < String > tags ;
for ( uint32_t i = 0 ; i < tag_count ; i + + ) {
String tag = tcp_peer - > get_utf8_string ( ) ;
print_verbose ( " EFS: tag # " + itos ( i ) + " : " + tag ) ;
ERR_FAIL_COND ( tcp_peer - > get_status ( ) ! = StreamPeerTCP : : STATUS_CONNECTED ) ;
tags . push_back ( tag ) ;
}
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
uint32_t file_buffer_decompressed_size = tcp_peer - > get_32 ( ) ;
HashMap < String , uint64_t > cached_files ;
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
if ( file_buffer_decompressed_size > 0 ) {
pr . step ( TTR ( " Getting remote file system " ) , 1 , true ) ;
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
// Got files cached by client.
uint32_t file_buffer_size = tcp_peer - > get_32 ( ) ;
print_verbose ( " EFS: Getting file buffer: compressed - " + String : : humanize_size ( file_buffer_size ) + " decompressed: " + String : : humanize_size ( file_buffer_decompressed_size ) ) ;
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
ERR_FAIL_COND ( tcp_peer - > get_status ( ) ! = StreamPeerTCP : : STATUS_CONNECTED ) ;
ERR_FAIL_COND ( file_buffer_size > MAX_FILE_BUFFER_SIZE ) ;
LocalVector < uint8_t > file_buffer ;
file_buffer . resize ( file_buffer_size ) ;
LocalVector < uint8_t > file_buffer_decompressed ;
file_buffer_decompressed . resize ( file_buffer_decompressed_size ) ;
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
err = tcp_peer - > get_data ( file_buffer . ptr ( ) , file_buffer_size ) ;
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
pr . step ( TTR ( " Decompressing remote file system " ) , 2 , true ) ;
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
ERR_FAIL_COND ( err ! = OK ) ;
// Decompress the text with all the files
Compression : : decompress ( file_buffer_decompressed . ptr ( ) , file_buffer_decompressed . size ( ) , file_buffer . ptr ( ) , file_buffer . size ( ) , Compression : : MODE_ZSTD ) ;
String files_text = String : : utf8 ( ( const char * ) file_buffer_decompressed . ptr ( ) , file_buffer_decompressed . size ( ) ) ;
Vector < String > files = files_text . split ( " \n " ) ;
print_verbose ( " EFS: Total cached files received: " + itos ( files . size ( ) ) ) ;
for ( int i = 0 ; i < files . size ( ) ; i + + ) {
if ( files [ i ] . get_slice_count ( " :: " ) ! = 2 ) {
continue ;
}
String file = files [ i ] . get_slice ( " :: " , 0 ) ;
uint64_t modified_time = files [ i ] . get_slice ( " :: " , 1 ) . to_int ( ) ;
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
cached_files . insert ( file , modified_time ) ;
}
} else {
// Client does not have any files stored.
}
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
pr . step ( TTR ( " Scanning for local changes " ) , 3 , true ) ;
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
print_verbose ( " EFS: Scanning changes: " ) ;
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
HashMap < String , uint64_t > files_to_send ;
// Scan files to send.
_scan_files_changed ( EditorFileSystem : : get_singleton ( ) - > get_filesystem ( ) , tags , files_to_send , cached_files ) ;
// Add forced export files
Vector < String > forced_export = EditorExportPlatform : : get_forced_export_files ( ) ;
for ( int i = 0 ; i < forced_export . size ( ) ; i + + ) {
_add_custom_file ( forced_export [ i ] , files_to_send , cached_files ) ;
}
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
_add_custom_file ( " res://project.godot " , files_to_send , cached_files ) ;
// Check which files were removed and also add them
for ( KeyValue < String , uint64_t > K : cached_files ) {
if ( ! files_to_send . has ( K . key ) ) {
files_to_send . insert ( K . key , 0 ) ; //0 means removed
2014-02-10 01:10:30 +00:00
}
}
2023-04-28 11:15:36 +00:00
tcp_peer - > put_32 ( files_to_send . size ( ) ) ;
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
print_verbose ( " EFS: Sending list of changed files. " ) ;
pr . step ( TTR ( " Sending list of changed files: " ) , 4 , true ) ;
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
// Send list of changed files first, to ensure that if connecting breaks, the client is not found in a broken state.
for ( KeyValue < String , uint64_t > K : files_to_send ) {
tcp_peer - > put_utf8_string ( K . key ) ;
tcp_peer - > put_64 ( K . value ) ;
}
print_verbose ( " EFS: Sending " + itos ( files_to_send . size ( ) ) + " files. " ) ;
int idx = 0 ;
for ( KeyValue < String , uint64_t > K : files_to_send ) {
2023-06-12 12:57:08 +00:00
pr . step ( TTR ( " Sending file: " ) + " " + K . key . get_file ( ) , 5 + idx * 100 / files_to_send . size ( ) , false ) ;
2023-04-28 11:15:36 +00:00
idx + + ;
2014-02-10 01:10:30 +00:00
2023-04-28 11:15:36 +00:00
if ( K . value = = 0 | | ! FileAccess : : exists ( " res:// " + K . key ) ) { // File was removed
continue ;
2014-02-10 01:10:30 +00:00
}
2023-04-28 11:15:36 +00:00
Vector < uint8_t > array = FileAccess : : _get_file_as_bytes ( " res:// " + K . key ) ;
tcp_peer - > put_64 ( array . size ( ) ) ;
tcp_peer - > put_data ( array . ptr ( ) , array . size ( ) ) ;
ERR_FAIL_COND ( tcp_peer - > get_status ( ) ! = StreamPeerTCP : : STATUS_CONNECTED ) ;
2014-02-10 01:10:30 +00:00
}
2023-04-28 11:15:36 +00:00
tcp_peer - > put_data ( ( const uint8_t * ) " GEND " , 4 ) ; // End marker.
print_verbose ( " EFS: Done. " ) ;
2014-02-10 01:10:30 +00:00
}
void EditorFileServer : : start ( ) {
2023-04-28 11:15:36 +00:00
if ( active ) {
stop ( ) ;
}
2022-03-06 20:39:19 +00:00
port = EDITOR_GET ( " filesystem/file_server/port " ) ;
password = EDITOR_GET ( " filesystem/file_server/password " ) ;
2023-04-28 11:15:36 +00:00
Error err = server - > listen ( port ) ;
ERR_FAIL_COND_MSG ( err ! = OK , " EditorFileServer: Unable to listen on port " + itos ( port ) ) ;
active = true ;
2014-02-10 01:10:30 +00:00
}
bool EditorFileServer : : is_active ( ) const {
return active ;
}
2017-03-05 15:44:50 +00:00
void EditorFileServer : : stop ( ) {
2023-04-28 11:15:36 +00:00
if ( active ) {
server - > stop ( ) ;
active = false ;
}
2014-02-10 01:10:30 +00:00
}
EditorFileServer : : EditorFileServer ( ) {
2021-06-17 22:03:09 +00:00
server . instantiate ( ) ;
2014-02-10 01:10:30 +00:00
2017-03-05 15:44:50 +00:00
EDITOR_DEF ( " filesystem/file_server/port " , 6010 ) ;
EDITOR_DEF ( " filesystem/file_server/password " , " " ) ;
2014-02-10 01:10:30 +00:00
}
EditorFileServer : : ~ EditorFileServer ( ) {
2023-04-28 11:15:36 +00:00
stop ( ) ;
2014-02-10 01:10:30 +00:00
}