1 /*
2 * Copyright (C) 2014-2018 Christopho, Solarus - http://www.solarus-games.org
3 *
4 * Solarus Quest Editor is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * Solarus Quest Editor is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17 #include "editor_exception.h"
18 #include "file_tools.h"
19 #include <solarus/core/Common.h>
20 #include <QApplication>
21 #include <QDebug>
22 #include <QDir>
23 #include <QFile>
24 #include <QFileInfo>
25 #include <QRegularExpression>
26 #include <QTextStream>
27
28 #include <QDebug>
29
30 namespace SolarusEditor {
31
32 namespace FileTools {
33
34 namespace {
35 bool assets_path_initialized = false;
36 QString assets_path;
37 }
38
39 /**
40 * @brief Determines the path to the Solarus Quest Editor assets directory.
41 *
42 * The directory "assets" is searched in the following paths in this order:
43 * - The directory containing the executable.
44 * - The source path (macro SOLARUSEDITOR_SOURCE_PATH)
45 * (useful for developer builds).
46 * - The install path (macro SOLARUSEDITOR_DATADIR_PATH).
47 */
initialize_assets()48 void initialize_assets() {
49
50 const QString& executable_path = QCoreApplication::applicationDirPath();
51 QStringList potential_paths;
52
53 // Try the current directory first.
54 potential_paths << executable_path + "/assets";
55
56 // Try the source path if we are not running the installed executable.
57 bool running_installed_executable = (executable_path == SOLARUSEDITOR_BINDIR_PATH);
58 #ifdef SOLARUSEDITOR_SOURCE_PATH
59 if (!running_installed_executable) {
60 potential_paths << SOLARUSEDITOR_SOURCE_PATH "/assets";
61 }
62 #endif
63
64 // Try the install path if we are running the installed executable.
65 #ifdef SOLARUSEDITOR_DATADIR_PATH
66 if (running_installed_executable) {
67 potential_paths << SOLARUSEDITOR_DATADIR_PATH "/assets";
68 }
69 #endif
70
71 assets_path_initialized = true;
72 for (const QString& potential_path : potential_paths) {
73 if (QFile(potential_path).exists()) {
74 assets_path = potential_path;
75 return;
76 }
77 }
78 }
79
80 /**
81 * @brief Returns the path to the Solarus Quest Editor assets directrory.
82 * @return The assets path or an empty string if assets could not be found.
83 */
get_assets_path()84 QString get_assets_path() {
85
86 if (!assets_path_initialized) {
87 initialize_assets();
88 }
89 return assets_path;
90 }
91
92 /**
93 * @brief Utility function to copy a file or directory with its content.
94 * @param src The file or directory to copy.
95 * @param dst The destination file path. It should be the name of the file
96 * or directory to create.
97 * @throws EditorException if the copy failed. In this case, files already
98 * successfully copied are left.
99 */
copy_recursive(const QString & src,const QString & dst)100 void copy_recursive(const QString& src, const QString& dst) {
101
102 if (src == dst) {
103 throw EditorException(QApplication::tr("Source and destination are the same: '%1'").arg(src));
104 }
105
106 QFileInfo src_info(src);
107 QFileInfo dst_info(dst);
108
109 if (!src_info.exists()) {
110 throw EditorException(QApplication::tr("No such file or folder: '%1'").arg(src));
111 }
112
113 if (!src_info.isReadable()) {
114 throw EditorException(QApplication::tr("Source file cannot be read: '%1'").arg(src));
115 }
116
117 if (dst_info.exists()) {
118 throw EditorException(QApplication::tr("Destination already exists: '%1'").arg(dst));
119 }
120
121 if (src_info.isDir()) {
122
123 QDir dst_dir(dst);
124 dst_dir.cdUp();
125
126 if (!dst_dir.exists()) {
127 throw EditorException(QApplication::tr("No such folder: '%1'").arg(dst_dir.path()));
128 }
129
130 QString src_canonical_path = src_info.canonicalFilePath();
131 QString dst_parent_canonical_path = dst_dir.canonicalPath();
132
133 if (dst_parent_canonical_path.startsWith(src_canonical_path)) {
134 throw EditorException(QApplication::tr("Cannot copy folder '%1' to one of its own subfolders: '%2'").arg(src, dst));
135 }
136
137 if (!dst_dir.mkdir(dst_info.fileName())) {
138 throw EditorException(QApplication::tr("Cannot create folder '%1'").arg(dst));
139 }
140
141 QDir src_dir(src);
142 const QStringList& file_names = src_dir.entryList(
143 QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
144 for (const QString& file_name : file_names) {
145 QString next_src = src + '/' + file_name;
146 QString next_dst = dst + '/' + file_name;
147 copy_recursive(next_src, next_dst);
148 }
149 }
150 else {
151 if (!QFile::copy(src, dst)) {
152 throw EditorException(QApplication::tr("Cannot copy file '%1' to '%2'").arg(src, dst));
153 }
154
155 if (src.startsWith(":/")) {
156 // Files from Qt resources are read-only. Set usual permissions now.
157 QFile::setPermissions(dst,
158 QFile::ReadUser | QFile::WriteUser | QFile::ExeUser |
159 QFile::ReadGroup| QFile::ExeGroup |
160 QFile::ReadOther| QFile::ExeOther);
161 }
162 }
163 }
164
165 /**
166 * @brief Deletes a file or a directory with its content.
167 *
168 * Does nothing if the file or directory does not exist.
169 *
170 * @param path The file or directory to delete.
171 * @throws EditorException if the deletion failed.
172 */
delete_recursive(const QString & path)173 void delete_recursive(const QString& path) {
174
175 QFileInfo info(path);
176 if (!info.exists()) {
177 return;
178 }
179
180 if (!info.isDir()) {
181 // Not a directory.
182 if (!QFile::remove(path)) {
183 throw EditorException(QApplication::tr("Failed to delete file '%1'").arg(path));
184 }
185 }
186 else {
187 // Directory.
188 QDir dir(path);
189 const QStringList& file_names = dir.entryList(
190 QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
191 for (const QString& file_name : file_names) {
192 QString child_path = path + '/' + file_name;
193 delete_recursive(child_path);
194 }
195
196 if (!QDir().rmdir(path)) {
197 throw EditorException(QApplication::tr("Failed to delete folder '%1'").arg(path));
198 }
199 }
200 }
201
202 /**
203 * @brief Makes sure that the specified directory exists.
204 *
205 * Creates necessary parents directories if needed.
206 *
207 * @param path A directory path.
208 * @throws EditorException In case of error.
209 */
create_directories(const QString & path)210 void create_directories(const QString& path) {
211
212 bool success = QDir().mkpath(path);
213 if (!success) {
214 throw EditorException(QApplication::tr("Cannot create folder '%1'").arg(path));
215 }
216 }
217
218 /**
219 * @brief Replaces all occurences of the given pattern in a file.
220 * @param path Path of the file to modify.
221 * @param regexp The pattern to replace.
222 * @param replacement The string to put instead of the pattern.
223 * @param replace_all @c true to replace all occurences, @c false to only
224 * replace the first one.
225 * @return @c true if there was a change.
226 * @throws EditorException In case of error.
227 */
replace_in_file(const QString & path,const QRegularExpression & regex,const QString & replacement,bool replace_all)228 bool replace_in_file(
229 const QString& path,
230 const QRegularExpression& regex,
231 const QString& replacement,
232 bool replace_all
233 ) {
234 QFile file(path);
235
236 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
237 throw EditorException(QApplication::tr("Cannot open file '%1'").arg(file.fileName()));
238 }
239 QTextStream in(&file);
240 in.setCodec("UTF-8");
241 QString content = in.readAll();
242 file.close();
243
244 QString old_content = content;
245 if (replace_all) {
246 content.replace(regex, replacement);
247 } else {
248 QRegularExpressionMatch match = regex.match(content);
249 if (match.hasMatch()) {
250 content.replace(match.capturedStart(), match.capturedLength(), replacement);
251 }
252 }
253
254 if (content == old_content) {
255 // No change.
256 return false;
257 }
258
259 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
260 throw EditorException(QApplication::tr("Cannot open file '%1' for writing").arg(file.fileName()));
261 }
262 QTextStream out(&file);
263 out.setCodec("UTF-8");
264 out << content;
265 file.close();
266 return true;
267 }
268
269 } // namespace FileTools
270
271 } // namespace SolarusEditor
272