1 | /* |
2 | This file is part of the KDE project |
3 | SPDX-FileCopyrightText: 1999 Simon Hausmann <hausmann@kde.org> |
4 | SPDX-FileCopyrightText: 1999-2005 David Faure <faure@kde.org> |
5 | |
6 | SPDX-License-Identifier: LGPL-2.0-or-later |
7 | */ |
8 | |
9 | #include "readwritepart.h" |
10 | #include "readwritepart_p.h" |
11 | |
12 | #include "kparts_logging.h" |
13 | |
14 | #define HAVE_KDIRNOTIFY __has_include(<KDirNotify>) |
15 | #if HAVE_KDIRNOTIFY |
16 | #include <KDirNotify> |
17 | #endif |
18 | |
19 | #include <KIO/FileCopyJob> |
20 | #include <KJobWidgets> |
21 | #include <KLocalizedString> |
22 | #include <KMessageBox> |
23 | |
24 | #include <QApplication> |
25 | #include <QFileDialog> |
26 | #include <QTemporaryFile> |
27 | |
28 | #include <qplatformdefs.h> |
29 | |
30 | #ifdef Q_OS_WIN |
31 | #include <qt_windows.h> //CreateHardLink() |
32 | #endif |
33 | |
34 | using namespace KParts; |
35 | |
36 | ReadWritePart::ReadWritePart(QObject *parent, const KPluginMetaData &data) |
37 | : ReadOnlyPart(*new ReadWritePartPrivate(this, data), parent) |
38 | { |
39 | } |
40 | |
41 | ReadWritePart::~ReadWritePart() |
42 | { |
43 | // parent destructor will delete temp file |
44 | // we can't call our own closeUrl() here, because |
45 | // "cancel" wouldn't cancel anything. We have to assume |
46 | // the app called closeUrl() before destroying us. |
47 | } |
48 | |
49 | void ReadWritePart::setReadWrite(bool readwrite) |
50 | { |
51 | Q_D(ReadWritePart); |
52 | |
53 | // Perhaps we should check isModified here and issue a warning if true |
54 | d->m_bReadWrite = readwrite; |
55 | } |
56 | |
57 | void ReadWritePart::setModified(bool modified) |
58 | { |
59 | Q_D(ReadWritePart); |
60 | |
61 | // qDebug() << "setModified(" << (modified ? "true" : "false") << ")"; |
62 | if (!d->m_bReadWrite && modified) { |
63 | qCCritical(KPARTSLOG) << "Can't set a read-only document to 'modified' !" ; |
64 | return; |
65 | } |
66 | d->m_bModified = modified; |
67 | } |
68 | |
69 | void ReadWritePart::setModified() |
70 | { |
71 | setModified(true); |
72 | } |
73 | |
74 | bool ReadWritePart::queryClose() |
75 | { |
76 | Q_D(ReadWritePart); |
77 | |
78 | if (!isReadWrite() || !isModified()) { |
79 | return true; |
80 | } |
81 | |
82 | QString docName = url().fileName(); |
83 | if (docName.isEmpty()) { |
84 | docName = i18n("Untitled" ); |
85 | } |
86 | |
87 | QWidget *parentWidget = widget(); |
88 | if (!parentWidget) { |
89 | parentWidget = QApplication::activeWindow(); |
90 | } |
91 | |
92 | int res = KMessageBox::warningTwoActionsCancel(parent: parentWidget, |
93 | i18n("The document \"%1\" has been modified.\n" |
94 | "Do you want to save your changes or discard them?" , |
95 | docName), |
96 | i18n("Close Document" ), |
97 | primaryAction: KStandardGuiItem::save(), |
98 | secondaryAction: KStandardGuiItem::discard()); |
99 | |
100 | bool abortClose = false; |
101 | bool handled = false; |
102 | |
103 | switch (res) { |
104 | case KMessageBox::PrimaryAction: |
105 | Q_EMIT sigQueryClose(handled: &handled, abortClosing: &abortClose); |
106 | if (!handled) { |
107 | if (d->m_url.isEmpty()) { |
108 | QUrl url = QFileDialog::getSaveFileUrl(parent: parentWidget); |
109 | if (url.isEmpty()) { |
110 | return false; |
111 | } |
112 | |
113 | saveAs(url); |
114 | } else { |
115 | save(); |
116 | } |
117 | } else if (abortClose) { |
118 | return false; |
119 | } |
120 | return waitSaveComplete(); |
121 | case KMessageBox::SecondaryAction: |
122 | return true; |
123 | default: // case KMessageBox::Cancel : |
124 | return false; |
125 | } |
126 | } |
127 | |
128 | bool ReadWritePart::closeUrl() |
129 | { |
130 | abortLoad(); // just in case |
131 | if (isReadWrite() && isModified()) { |
132 | if (!queryClose()) { |
133 | return false; |
134 | } |
135 | } |
136 | // Not modified => ok and delete temp file. |
137 | return ReadOnlyPart::closeUrl(); |
138 | } |
139 | |
140 | bool ReadWritePart::closeUrl(bool promptToSave) |
141 | { |
142 | return promptToSave ? closeUrl() : ReadOnlyPart::closeUrl(); |
143 | } |
144 | |
145 | bool ReadWritePart::save() |
146 | { |
147 | Q_D(ReadWritePart); |
148 | |
149 | d->m_saveOk = false; |
150 | if (d->m_file.isEmpty()) { // document was created empty |
151 | d->prepareSaving(); |
152 | } |
153 | if (saveFile()) { |
154 | return saveToUrl(); |
155 | } else { |
156 | Q_EMIT canceled(errMsg: QString()); |
157 | } |
158 | return false; |
159 | } |
160 | |
161 | bool ReadWritePart::saveAs(const QUrl &url) |
162 | { |
163 | Q_D(ReadWritePart); |
164 | |
165 | if (!url.isValid()) { |
166 | qCCritical(KPARTSLOG) << "saveAs: Malformed URL" << url; |
167 | return false; |
168 | } |
169 | d->m_duringSaveAs = true; |
170 | d->m_originalURL = d->m_url; |
171 | d->m_originalFilePath = d->m_file; |
172 | d->m_url = url; // Store where to upload in saveToURL |
173 | d->prepareSaving(); |
174 | bool result = save(); // Save local file and upload local file |
175 | if (result) { |
176 | if (d->m_originalURL != d->m_url) { |
177 | Q_EMIT urlChanged(url: d->m_url); |
178 | } |
179 | |
180 | Q_EMIT setWindowCaption(d->m_url.toDisplayString(options: QUrl::PreferLocalFile)); |
181 | } else { |
182 | d->m_url = d->m_originalURL; |
183 | d->m_file = d->m_originalFilePath; |
184 | d->m_duringSaveAs = false; |
185 | d->m_originalURL = QUrl(); |
186 | d->m_originalFilePath.clear(); |
187 | } |
188 | |
189 | return result; |
190 | } |
191 | |
192 | // Set m_file correctly for m_url |
193 | void ReadWritePartPrivate::prepareSaving() |
194 | { |
195 | // Local file |
196 | if (m_url.isLocalFile()) { |
197 | if (m_bTemp) { // get rid of a possible temp file first |
198 | // (happens if previous url was remote) |
199 | QFile::remove(fileName: m_file); |
200 | m_bTemp = false; |
201 | } |
202 | m_file = m_url.toLocalFile(); |
203 | } else { |
204 | // Remote file |
205 | // We haven't saved yet, or we did but locally - provide a temp file |
206 | if (m_file.isEmpty() || !m_bTemp) { |
207 | QTemporaryFile tempFile; |
208 | tempFile.setAutoRemove(false); |
209 | tempFile.open(); |
210 | m_file = tempFile.fileName(); |
211 | m_bTemp = true; |
212 | } |
213 | // otherwise, we already had a temp file |
214 | } |
215 | } |
216 | |
217 | static inline bool makeHardLink(const QString &src, const QString &dest) |
218 | { |
219 | #ifndef Q_OS_WIN |
220 | return ::link(from: QFile::encodeName(fileName: src).constData(), to: QFile::encodeName(fileName: dest).constData()) == 0; |
221 | #else |
222 | return CreateHardLinkW((LPCWSTR)dest.utf16(), (LPCWSTR)src.utf16(), nullptr) != 0; |
223 | #endif |
224 | } |
225 | |
226 | bool ReadWritePart::saveToUrl() |
227 | { |
228 | Q_D(ReadWritePart); |
229 | |
230 | if (d->m_url.isLocalFile()) { |
231 | setModified(false); |
232 | Q_EMIT completed(); |
233 | // if m_url is a local file there won't be a temp file -> nothing to remove |
234 | Q_ASSERT(!d->m_bTemp); |
235 | d->m_saveOk = true; |
236 | d->m_duringSaveAs = false; |
237 | d->m_originalURL = QUrl(); |
238 | d->m_originalFilePath.clear(); |
239 | return true; // Nothing to do |
240 | } else { |
241 | if (d->m_uploadJob) { |
242 | QFile::remove(fileName: d->m_uploadJob->srcUrl().toLocalFile()); |
243 | d->m_uploadJob->kill(); |
244 | d->m_uploadJob = nullptr; |
245 | } |
246 | QTemporaryFile *tempFile = new QTemporaryFile(); |
247 | tempFile->open(); |
248 | QString uploadFile = tempFile->fileName(); |
249 | delete tempFile; |
250 | QUrl uploadUrl = QUrl::fromLocalFile(localfile: uploadFile); |
251 | // Create hardlink |
252 | if (!makeHardLink(src: d->m_file, dest: uploadFile)) { |
253 | // Uh oh, some error happened. |
254 | return false; |
255 | } |
256 | d->m_uploadJob = KIO::file_move(src: uploadUrl, dest: d->m_url, permissions: -1, flags: KIO::Overwrite); |
257 | KJobWidgets::setWindow(job: d->m_uploadJob, widget: widget()); |
258 | |
259 | connect(sender: d->m_uploadJob, signal: &KJob::result, context: this, slot: [d](KJob *job) { |
260 | d->slotUploadFinished(job); |
261 | }); |
262 | |
263 | return true; |
264 | } |
265 | } |
266 | |
267 | void ReadWritePartPrivate::slotUploadFinished(KJob *) |
268 | { |
269 | Q_Q(ReadWritePart); |
270 | |
271 | if (m_uploadJob->error()) { |
272 | QFile::remove(fileName: m_uploadJob->srcUrl().toLocalFile()); |
273 | QString error = m_uploadJob->errorString(); |
274 | m_uploadJob = nullptr; |
275 | if (m_duringSaveAs) { |
276 | q->setUrl(m_originalURL); |
277 | m_file = m_originalFilePath; |
278 | } |
279 | Q_EMIT q->canceled(errMsg: error); |
280 | } else { |
281 | #if HAVE_KDIRNOTIFY |
282 | ::org::kde::KDirNotify::emitFilesAdded(directory: m_url.adjusted(options: QUrl::RemoveFilename)); |
283 | #endif |
284 | |
285 | m_uploadJob = nullptr; |
286 | q->setModified(false); |
287 | Q_EMIT q->completed(); |
288 | m_saveOk = true; |
289 | } |
290 | m_duringSaveAs = false; |
291 | m_originalURL = QUrl(); |
292 | m_originalFilePath.clear(); |
293 | if (m_waitForSave) { |
294 | m_eventLoop.quit(); |
295 | } |
296 | } |
297 | |
298 | bool ReadWritePart::isReadWrite() const |
299 | { |
300 | Q_D(const ReadWritePart); |
301 | |
302 | return d->m_bReadWrite; |
303 | } |
304 | |
305 | bool ReadWritePart::isModified() const |
306 | { |
307 | Q_D(const ReadWritePart); |
308 | |
309 | return d->m_bModified; |
310 | } |
311 | |
312 | bool ReadWritePart::waitSaveComplete() |
313 | { |
314 | Q_D(ReadWritePart); |
315 | |
316 | if (!d->m_uploadJob) { |
317 | return d->m_saveOk; |
318 | } |
319 | |
320 | d->m_waitForSave = true; |
321 | |
322 | d->m_eventLoop.exec(flags: QEventLoop::ExcludeUserInputEvents); |
323 | |
324 | d->m_waitForSave = false; |
325 | |
326 | return d->m_saveOk; |
327 | } |
328 | |
329 | #include "moc_readwritepart.cpp" |
330 | |