1/**************************************************************************/
2/* editor_file_server.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "editor_file_server.h"
32
33#include "../editor_settings.h"
34#include "core/io/marshalls.h"
35#include "editor/editor_node.h"
36#include "editor/export/editor_export_platform.h"
37
38#define FILESYSTEM_PROTOCOL_VERSION 1
39#define PASSWORD_LENGTH 32
40#define MAX_FILE_BUFFER_SIZE 100 * 1024 * 1024 // 100mb max file buffer size (description of files to update, compressed).
41
42static void _add_file(String f, const uint64_t &p_modified_time, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files) {
43 f = f.replace_first("res://", ""); // remove res://
44 const uint64_t *cached_mt = cached_files.getptr(f);
45 if (cached_mt && *cached_mt == p_modified_time) {
46 // File is good, skip it.
47 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.
48 return;
49 }
50 files_to_send.insert(f, p_modified_time);
51}
52
53void EditorFileServer::_scan_files_changed(EditorFileSystemDirectory *efd, const Vector<String> &p_tags, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files) {
54 for (int i = 0; i < efd->get_file_count(); i++) {
55 String f = efd->get_file_path(i);
56 if (FileAccess::exists(f + ".import")) {
57 // is imported, determine what to do
58 // Todo the modified times of remapped files should most likely be kept in EditorFileSystem to speed this up in the future.
59 Ref<ConfigFile> cf;
60 cf.instantiate();
61 Error err = cf->load(f + ".import");
62
63 ERR_CONTINUE(err != OK);
64 {
65 uint64_t mt = FileAccess::get_modified_time(f + ".import");
66 _add_file(f + ".import", mt, files_to_send, cached_files);
67 }
68
69 if (!cf->has_section("remap")) {
70 continue;
71 }
72
73 List<String> remaps;
74 cf->get_section_keys("remap", &remaps);
75
76 for (const String &remap : remaps) {
77 if (remap == "path") {
78 String remapped_path = cf->get_value("remap", remap);
79 uint64_t mt = FileAccess::get_modified_time(remapped_path);
80 _add_file(remapped_path, mt, files_to_send, cached_files);
81 } else if (remap.begins_with("path.")) {
82 String feature = remap.get_slice(".", 1);
83 if (p_tags.find(feature) != -1) {
84 String remapped_path = cf->get_value("remap", remap);
85 uint64_t mt = FileAccess::get_modified_time(remapped_path);
86 _add_file(remapped_path, mt, files_to_send, cached_files);
87 }
88 }
89 }
90 } else {
91 uint64_t mt = efd->get_file_modified_time(i);
92 _add_file(f, mt, files_to_send, cached_files);
93 }
94 }
95
96 for (int i = 0; i < efd->get_subdir_count(); i++) {
97 _scan_files_changed(efd->get_subdir(i), p_tags, files_to_send, cached_files);
98 }
99}
100
101static void _add_custom_file(const String f, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files) {
102 if (!FileAccess::exists(f)) {
103 return;
104 }
105 _add_file(f, FileAccess::get_modified_time(f), files_to_send, cached_files);
106}
107
108void EditorFileServer::poll() {
109 if (!active) {
110 return;
111 }
112
113 if (!server->is_connection_available()) {
114 return;
115 }
116
117 Ref<StreamPeerTCP> tcp_peer = server->take_connection();
118 ERR_FAIL_COND(tcp_peer.is_null());
119
120 // Got a connection!
121 EditorProgress pr("updating_remote_file_system", TTR("Updating assets on target device:"), 105);
122
123 pr.step(TTR("Syncing headers"), 0, true);
124 print_verbose("EFS: Connecting taken!");
125 char header[4];
126 Error err = tcp_peer->get_data((uint8_t *)&header, 4);
127 ERR_FAIL_COND(err != OK);
128 ERR_FAIL_COND(header[0] != 'G');
129 ERR_FAIL_COND(header[1] != 'R');
130 ERR_FAIL_COND(header[2] != 'F');
131 ERR_FAIL_COND(header[3] != 'S');
132
133 uint32_t protocol_version = tcp_peer->get_u32();
134 ERR_FAIL_COND(protocol_version != FILESYSTEM_PROTOCOL_VERSION);
135
136 char cpassword[PASSWORD_LENGTH + 1];
137 err = tcp_peer->get_data((uint8_t *)cpassword, PASSWORD_LENGTH);
138 cpassword[PASSWORD_LENGTH] = 0;
139 ERR_FAIL_COND(err != OK);
140 print_verbose("EFS: Got password: " + String(cpassword));
141 ERR_FAIL_COND_MSG(password != cpassword, "Client disconnected because password mismatch.");
142
143 uint32_t tag_count = tcp_peer->get_u32();
144 print_verbose("EFS: Getting tags: " + itos(tag_count));
145
146 ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);
147 Vector<String> tags;
148 for (uint32_t i = 0; i < tag_count; i++) {
149 String tag = tcp_peer->get_utf8_string();
150 print_verbose("EFS: tag #" + itos(i) + ": " + tag);
151 ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);
152 tags.push_back(tag);
153 }
154
155 uint32_t file_buffer_decompressed_size = tcp_peer->get_32();
156 HashMap<String, uint64_t> cached_files;
157
158 if (file_buffer_decompressed_size > 0) {
159 pr.step(TTR("Getting remote file system"), 1, true);
160
161 // Got files cached by client.
162 uint32_t file_buffer_size = tcp_peer->get_32();
163 print_verbose("EFS: Getting file buffer: compressed - " + String::humanize_size(file_buffer_size) + " decompressed: " + String::humanize_size(file_buffer_decompressed_size));
164
165 ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);
166 ERR_FAIL_COND(file_buffer_size > MAX_FILE_BUFFER_SIZE);
167 LocalVector<uint8_t> file_buffer;
168 file_buffer.resize(file_buffer_size);
169 LocalVector<uint8_t> file_buffer_decompressed;
170 file_buffer_decompressed.resize(file_buffer_decompressed_size);
171
172 err = tcp_peer->get_data(file_buffer.ptr(), file_buffer_size);
173
174 pr.step(TTR("Decompressing remote file system"), 2, true);
175
176 ERR_FAIL_COND(err != OK);
177 // Decompress the text with all the files
178 Compression::decompress(file_buffer_decompressed.ptr(), file_buffer_decompressed.size(), file_buffer.ptr(), file_buffer.size(), Compression::MODE_ZSTD);
179 String files_text = String::utf8((const char *)file_buffer_decompressed.ptr(), file_buffer_decompressed.size());
180 Vector<String> files = files_text.split("\n");
181
182 print_verbose("EFS: Total cached files received: " + itos(files.size()));
183 for (int i = 0; i < files.size(); i++) {
184 if (files[i].get_slice_count("::") != 2) {
185 continue;
186 }
187 String file = files[i].get_slice("::", 0);
188 uint64_t modified_time = files[i].get_slice("::", 1).to_int();
189
190 cached_files.insert(file, modified_time);
191 }
192 } else {
193 // Client does not have any files stored.
194 }
195
196 pr.step(TTR("Scanning for local changes"), 3, true);
197
198 print_verbose("EFS: Scanning changes:");
199
200 HashMap<String, uint64_t> files_to_send;
201 // Scan files to send.
202 _scan_files_changed(EditorFileSystem::get_singleton()->get_filesystem(), tags, files_to_send, cached_files);
203 // Add forced export files
204 Vector<String> forced_export = EditorExportPlatform::get_forced_export_files();
205 for (int i = 0; i < forced_export.size(); i++) {
206 _add_custom_file(forced_export[i], files_to_send, cached_files);
207 }
208
209 _add_custom_file("res://project.godot", files_to_send, cached_files);
210 // Check which files were removed and also add them
211 for (KeyValue<String, uint64_t> K : cached_files) {
212 if (!files_to_send.has(K.key)) {
213 files_to_send.insert(K.key, 0); //0 means removed
214 }
215 }
216
217 tcp_peer->put_32(files_to_send.size());
218
219 print_verbose("EFS: Sending list of changed files.");
220 pr.step(TTR("Sending list of changed files:"), 4, true);
221
222 // Send list of changed files first, to ensure that if connecting breaks, the client is not found in a broken state.
223 for (KeyValue<String, uint64_t> K : files_to_send) {
224 tcp_peer->put_utf8_string(K.key);
225 tcp_peer->put_64(K.value);
226 }
227
228 print_verbose("EFS: Sending " + itos(files_to_send.size()) + " files.");
229
230 int idx = 0;
231 for (KeyValue<String, uint64_t> K : files_to_send) {
232 pr.step(TTR("Sending file:") + " " + K.key.get_file(), 5 + idx * 100 / files_to_send.size(), false);
233 idx++;
234
235 if (K.value == 0 || !FileAccess::exists("res://" + K.key)) { // File was removed
236 continue;
237 }
238
239 Vector<uint8_t> array = FileAccess::_get_file_as_bytes("res://" + K.key);
240 tcp_peer->put_64(array.size());
241 tcp_peer->put_data(array.ptr(), array.size());
242 ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);
243 }
244
245 tcp_peer->put_data((const uint8_t *)"GEND", 4); // End marker.
246
247 print_verbose("EFS: Done.");
248}
249
250void EditorFileServer::start() {
251 if (active) {
252 stop();
253 }
254 port = EDITOR_GET("filesystem/file_server/port");
255 password = EDITOR_GET("filesystem/file_server/password");
256 Error err = server->listen(port);
257 ERR_FAIL_COND_MSG(err != OK, "EditorFileServer: Unable to listen on port " + itos(port));
258 active = true;
259}
260
261bool EditorFileServer::is_active() const {
262 return active;
263}
264
265void EditorFileServer::stop() {
266 if (active) {
267 server->stop();
268 active = false;
269 }
270}
271
272EditorFileServer::EditorFileServer() {
273 server.instantiate();
274
275 EDITOR_DEF("filesystem/file_server/port", 6010);
276 EDITOR_DEF("filesystem/file_server/password", "");
277}
278
279EditorFileServer::~EditorFileServer() {
280 stop();
281}
282