blob: 9629e3b0ffe4f033bcfafbd6c1f4bc3f2d208cfe [file] [log] [blame]
James Kuszmaulcf324122023-01-14 14:07:17 -08001// Copyright (c) FIRST and other WPILib contributors.
2// Open Source Software; you can modify and/or share it under the terms of
3// the WPILib BSD license file in the root directory of this project.
4
5#include "Downloader.h"
6
James Kuszmaulcf324122023-01-14 14:07:17 -08007#ifdef _WIN32
8#include <fcntl.h>
9#include <io.h>
10#else
11#include <sys/fcntl.h>
12#endif
13
14#include <algorithm>
15#include <filesystem>
16
17#include <fmt/format.h>
18#include <glass/Storage.h>
19#include <imgui.h>
20#include <imgui_stdlib.h>
James Kuszmaulb13e13f2023-11-22 20:44:04 -080021#include <libssh/sftp.h>
James Kuszmaulcf324122023-01-14 14:07:17 -080022#include <portable-file-dialogs.h>
23#include <wpi/StringExtras.h>
24#include <wpi/fs.h>
25
26#include "Sftp.h"
27
28Downloader::Downloader(glass::Storage& storage)
29 : m_serverTeam{storage.GetString("serverTeam")},
30 m_remoteDir{storage.GetString("remoteDir", "/home/lvuser")},
31 m_username{storage.GetString("username", "lvuser")},
32 m_localDir{storage.GetString("localDir")},
33 m_deleteAfter{storage.GetBool("deleteAfter", true)},
34 m_thread{[this] { ThreadMain(); }} {}
35
36Downloader::~Downloader() {
37 {
38 std::scoped_lock lock{m_mutex};
39 m_state = kExit;
40 }
41 m_cv.notify_all();
42 m_thread.join();
43}
44
45void Downloader::DisplayConnect() {
46 // IP or Team Number text box
47 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 12);
48 ImGui::InputText("Team Number / Address", &m_serverTeam);
49
50 // Username/password
51 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 12);
52 ImGui::InputText("Username", &m_username);
53 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 12);
54 ImGui::InputText("Password", &m_password, ImGuiInputTextFlags_Password);
55
56 // Connect button
57 if (ImGui::Button("Connect")) {
58 m_state = kConnecting;
59 m_cv.notify_all();
60 }
61}
62
63void Downloader::DisplayDisconnectButton() {
64 if (ImGui::Button("Disconnect")) {
65 m_state = kDisconnecting;
66 m_cv.notify_all();
67 }
68}
69
70void Downloader::DisplayRemoteDirSelector() {
71 ImGui::SameLine();
72 if (ImGui::Button("Refresh")) {
73 m_state = kGetFiles;
74 m_cv.notify_all();
75 }
76
77 ImGui::SameLine();
78 if (ImGui::Button("Deselect All")) {
James Kuszmaulb13e13f2023-11-22 20:44:04 -080079 for (auto&& download : m_fileList) {
80 download.selected = false;
James Kuszmaulcf324122023-01-14 14:07:17 -080081 }
82 }
83
84 ImGui::SameLine();
85 if (ImGui::Button("Select All")) {
James Kuszmaulb13e13f2023-11-22 20:44:04 -080086 for (auto&& download : m_fileList) {
87 download.selected = true;
James Kuszmaulcf324122023-01-14 14:07:17 -080088 }
89 }
90
91 // Remote directory text box
92 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 20);
93 if (ImGui::InputText("Remote Dir", &m_remoteDir,
94 ImGuiInputTextFlags_EnterReturnsTrue)) {
95 m_state = kGetFiles;
96 m_cv.notify_all();
97 }
98
99 // List directories
100 for (auto&& dir : m_dirList) {
101 if (ImGui::Selectable(dir.c_str())) {
102 if (dir == "..") {
103 if (wpi::ends_with(m_remoteDir, '/')) {
104 m_remoteDir.resize(m_remoteDir.size() - 1);
105 }
106 m_remoteDir = wpi::rsplit(m_remoteDir, '/').first;
107 if (m_remoteDir.empty()) {
108 m_remoteDir = "/";
109 }
110 } else {
111 if (!wpi::ends_with(m_remoteDir, '/')) {
112 m_remoteDir += '/';
113 }
114 m_remoteDir += dir;
115 }
116 m_state = kGetFiles;
117 m_cv.notify_all();
118 }
119 }
120}
121
122void Downloader::DisplayLocalDirSelector() {
123 // Local directory text / select button
124 if (ImGui::Button("Select Download Folder...")) {
125 m_localDirSelector =
126 std::make_unique<pfd::select_folder>("Select Download Folder");
127 }
128 ImGui::TextUnformatted(m_localDir.c_str());
129
130 // Delete after download (checkbox)
131 ImGui::Checkbox("Delete after download", &m_deleteAfter);
132
133 // Download button
134 if (!m_localDir.empty()) {
135 if (ImGui::Button("Download")) {
136 m_state = kDownload;
137 m_cv.notify_all();
138 }
139 }
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800140
141 ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(180, 0, 0, 255));
142 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(210, 0, 0, 255));
143 ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(255, 0, 0, 255));
144 if (ImGui::Button("Delete WITHOUT Downloading")) {
145 ImGui::OpenPopup("DeleteConfirm");
146 }
147 ImGui::PopStyleColor(3);
148 if (ImGui::BeginPopup("DeleteConfirm")) {
149 ImGui::TextUnformatted("Are you sure? This will NOT download the files");
150 if (ImGui::Button("DELETE")) {
151 m_state = kDelete;
152 m_cv.notify_all();
153 ImGui::CloseCurrentPopup();
154 }
155 ImGui::SameLine();
156 if (ImGui::Button("Cancel")) {
157 ImGui::CloseCurrentPopup();
158 }
159 ImGui::EndPopup();
160 }
James Kuszmaulcf324122023-01-14 14:07:17 -0800161}
162
163size_t Downloader::DisplayFiles() {
164 // List of files (multi-select) (changes to progress bar for downloading)
165 size_t fileCount = 0;
166 if (ImGui::BeginTable(
167 "files", 3,
168 ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) {
169 ImGui::TableSetupColumn("File");
170 ImGui::TableSetupColumn("Size");
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800171 ImGui::TableSetupColumn((m_state == kDownload || m_state == kDownloadDone ||
172 m_state == kDelete || m_state == kDeleteDone)
173 ? "Status"
174 : "Selected");
James Kuszmaulcf324122023-01-14 14:07:17 -0800175 ImGui::TableHeadersRow();
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800176 for (auto&& file : m_fileList) {
177 if ((m_state == kDownload || m_state == kDownloadDone ||
178 m_state == kDelete || m_state == kDeleteDone) &&
179 !file.selected) {
James Kuszmaulcf324122023-01-14 14:07:17 -0800180 continue;
181 }
182
183 ++fileCount;
184
185 ImGui::TableNextRow();
186 ImGui::TableNextColumn();
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800187 ImGui::TextUnformatted(file.name.c_str());
James Kuszmaulcf324122023-01-14 14:07:17 -0800188 ImGui::TableNextColumn();
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800189 auto sizeText = fmt::format("{}", file.size);
James Kuszmaulcf324122023-01-14 14:07:17 -0800190 ImGui::TextUnformatted(sizeText.c_str());
191 ImGui::TableNextColumn();
192 if (m_state == kDownload || m_state == kDownloadDone) {
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800193 if (!file.status.empty()) {
194 ImGui::TextUnformatted(file.status.c_str());
James Kuszmaulcf324122023-01-14 14:07:17 -0800195 } else {
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800196 ImGui::ProgressBar(file.complete);
197 }
198 } else if (m_state == kDelete || m_state == kDeleteDone) {
199 if (!file.status.empty()) {
200 ImGui::TextUnformatted(file.status.c_str());
James Kuszmaulcf324122023-01-14 14:07:17 -0800201 }
202 } else {
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800203 auto checkboxLabel = fmt::format("##{}", file.name);
204 ImGui::Checkbox(checkboxLabel.c_str(), &file.selected);
James Kuszmaulcf324122023-01-14 14:07:17 -0800205 }
206 }
207 ImGui::EndTable();
208 }
209
210 return fileCount;
211}
212
213void Downloader::Display() {
214 if (m_localDirSelector && m_localDirSelector->ready(0)) {
215 m_localDir = m_localDirSelector->result();
216 m_localDirSelector.reset();
217 }
218
219 std::scoped_lock lock{m_mutex};
220
221 if (!m_error.empty()) {
222 ImGui::TextUnformatted(m_error.c_str());
223 }
224
225 switch (m_state) {
226 case kDisconnected:
227 DisplayConnect();
228 break;
229 case kConnecting:
230 DisplayDisconnectButton();
231 ImGui::Text("Connecting to %s...", m_serverTeam.c_str());
232 break;
233 case kDisconnecting:
234 ImGui::TextUnformatted("Disconnecting...");
235 break;
236 case kConnected:
237 case kGetFiles:
238 DisplayDisconnectButton();
239 DisplayRemoteDirSelector();
240 if (DisplayFiles() > 0) {
241 DisplayLocalDirSelector();
242 }
243 break;
244 case kDownload:
245 case kDownloadDone:
246 DisplayDisconnectButton();
247 DisplayFiles();
248 if (m_state == kDownloadDone) {
249 if (ImGui::Button("Download complete!")) {
250 m_state = kGetFiles;
251 m_cv.notify_all();
252 }
253 }
254 break;
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800255 case kDelete:
256 case kDeleteDone:
257 DisplayDisconnectButton();
258 DisplayFiles();
259 if (m_state == kDeleteDone) {
260 if (ImGui::Button("Deletion complete!")) {
261 m_state = kGetFiles;
262 m_cv.notify_all();
263 }
264 }
265 break;
James Kuszmaulcf324122023-01-14 14:07:17 -0800266 default:
267 break;
268 }
269}
270
271void Downloader::ThreadMain() {
272 std::unique_ptr<sftp::Session> session;
273
274 static constexpr size_t kBufSize = 32 * 1024;
275 std::unique_ptr<uint8_t[]> copyBuf = std::make_unique<uint8_t[]>(kBufSize);
276
277 std::unique_lock lock{m_mutex};
278 while (m_state != kExit) {
279 State prev = m_state;
280 m_cv.wait(lock, [&] { return m_state != prev; });
281 m_error.clear();
282 try {
283 switch (m_state) {
284 case kConnecting:
285 if (auto team = wpi::parse_integer<unsigned int>(m_serverTeam, 10)) {
286 // team number
287 session = std::make_unique<sftp::Session>(
288 fmt::format("roborio-{}-frc.local", team.value()), 22,
289 m_username, m_password);
290 } else {
291 session = std::make_unique<sftp::Session>(m_serverTeam, 22,
292 m_username, m_password);
293 }
294 lock.unlock();
295 try {
296 session->Connect();
297 } catch (...) {
298 lock.lock();
299 throw;
300 }
301 lock.lock();
302 // FALLTHROUGH
303 case kGetFiles: {
304 std::string dir = m_remoteDir;
305 std::vector<sftp::Attributes> fileList;
306 lock.unlock();
307 try {
308 fileList = session->ReadDir(dir);
309 } catch (sftp::Exception& ex) {
310 lock.lock();
311 if (ex.err == SSH_FX_OK || ex.err == SSH_FX_CONNECTION_LOST) {
312 throw;
313 }
314 m_error = ex.what();
315 m_dirList.clear();
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800316 m_fileList.clear();
James Kuszmaulcf324122023-01-14 14:07:17 -0800317 m_state = kConnected;
318 break;
319 }
320 std::sort(
321 fileList.begin(), fileList.end(),
322 [](const auto& l, const auto& r) { return l.name < r.name; });
323 lock.lock();
324
325 m_dirList.clear();
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800326 m_fileList.clear();
James Kuszmaulcf324122023-01-14 14:07:17 -0800327 for (auto&& attr : fileList) {
328 if (attr.type == SSH_FILEXFER_TYPE_DIRECTORY) {
329 if (attr.name != ".") {
330 m_dirList.emplace_back(attr.name);
331 }
332 } else if (attr.type == SSH_FILEXFER_TYPE_REGULAR &&
333 (attr.flags & SSH_FILEXFER_ATTR_SIZE) != 0 &&
334 wpi::ends_with(attr.name, ".wpilog")) {
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800335 m_fileList.emplace_back(attr.name, attr.size);
James Kuszmaulcf324122023-01-14 14:07:17 -0800336 }
337 }
338
339 m_state = kConnected;
340 break;
341 }
342 case kDisconnecting:
343 session.reset();
344 m_state = kDisconnected;
345 break;
346 case kDownload: {
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800347 for (auto&& file : m_fileList) {
James Kuszmaulcf324122023-01-14 14:07:17 -0800348 if (m_state != kDownload) {
349 // user aborted
350 break;
351 }
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800352 if (!file.selected) {
James Kuszmaulcf324122023-01-14 14:07:17 -0800353 continue;
354 }
355
356 auto remoteFilename = fmt::format(
357 "{}{}{}", m_remoteDir,
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800358 wpi::ends_with(m_remoteDir, '/') ? "" : "/", file.name);
359 auto localFilename = fs::path{m_localDir} / file.name;
360 uint64_t fileSize = file.size;
James Kuszmaulcf324122023-01-14 14:07:17 -0800361
362 lock.unlock();
363
364 // open local file
365 std::error_code ec;
366 fs::file_t of = fs::OpenFileForWrite(localFilename, ec,
367 fs::CD_CreateNew, fs::OF_None);
368 if (ec) {
369 // failed to open
370 lock.lock();
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800371 file.status = ec.message();
James Kuszmaulcf324122023-01-14 14:07:17 -0800372 continue;
373 }
374 int ofd = fs::FileToFd(of, ec, fs::OF_None);
375 if (ofd == -1 || ec) {
376 // failed to convert to fd
377 lock.lock();
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800378 file.status = ec.message();
James Kuszmaulcf324122023-01-14 14:07:17 -0800379 continue;
380 }
381
382 try {
383 // open remote file
384 sftp::File f = session->Open(remoteFilename, O_RDONLY, 0);
385
386 // copy in chunks
387 uint64_t total = 0;
388 while (total < fileSize) {
389 uint64_t toCopy = (std::min)(fileSize - total,
390 static_cast<uint64_t>(kBufSize));
391 auto copied = f.Read(copyBuf.get(), toCopy);
392 if (write(ofd, copyBuf.get(), copied) !=
393 static_cast<int64_t>(copied)) {
394 // error writing
395 close(ofd);
396 fs::remove(localFilename, ec);
397 lock.lock();
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800398 file.status = "error writing local file";
James Kuszmaulcf324122023-01-14 14:07:17 -0800399 goto err;
400 }
401 total += copied;
402 lock.lock();
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800403 file.complete = static_cast<float>(total) / fileSize;
James Kuszmaulcf324122023-01-14 14:07:17 -0800404 lock.unlock();
405 }
406
407 // close local file
408 close(ofd);
409 ofd = -1;
410
411 // delete remote file (if enabled)
412 if (m_deleteAfter) {
413 f = sftp::File{};
414 session->Unlink(remoteFilename);
415 }
416 } catch (sftp::Exception& ex) {
417 if (ofd != -1) {
418 // close local file and delete it (due to failure)
419 close(ofd);
420 fs::remove(localFilename, ec);
421 }
422 lock.lock();
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800423 file.status = ex.what();
James Kuszmaulcf324122023-01-14 14:07:17 -0800424 if (ex.err == SSH_FX_OK || ex.err == SSH_FX_CONNECTION_LOST) {
425 throw;
426 }
427 continue;
428 }
429 lock.lock();
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800430 err: {}
James Kuszmaulcf324122023-01-14 14:07:17 -0800431 }
432 if (m_state == kDownload) {
433 m_state = kDownloadDone;
434 }
435 break;
436 }
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800437 case kDelete: {
438 for (auto&& file : m_fileList) {
439 if (m_state != kDelete) {
440 // user aborted
441 break;
442 }
443 if (!file.selected) {
444 continue;
445 }
446
447 auto remoteFilename = fmt::format(
448 "{}{}{}", m_remoteDir,
449 wpi::ends_with(m_remoteDir, '/') ? "" : "/", file.name);
450
451 lock.unlock();
452 try {
453 session->Unlink(remoteFilename);
454 } catch (sftp::Exception& ex) {
455 lock.lock();
456 file.status = ex.what();
457 if (ex.err == SSH_FX_OK || ex.err == SSH_FX_CONNECTION_LOST) {
458 throw;
459 }
460 continue;
461 }
462 lock.lock();
463 file.status = "Deleted";
464 }
465 if (m_state == kDelete) {
466 m_state = kDeleteDone;
467 }
468 break;
469 }
James Kuszmaulcf324122023-01-14 14:07:17 -0800470 default:
471 break;
472 }
473 } catch (sftp::Exception& ex) {
474 m_error = ex.what();
475 session.reset();
476 m_state = kDisconnected;
477 }
478 }
479}