A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 656 lines 21 kB view raw
1/*************************************************************************** 2 * __________ __ ___. 3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___ 4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / 5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < 6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ 7 * \/ \/ \/ \/ \/ 8 * $Id$ 9 * 10 * Copyright (C) 2002 Björn Stenberg 11 * Copyright (C) 2024 William Wilgus 12 * 13 * This program is free software; you can redistribute it and/or 14 * modify it under the terms of the GNU General Public License 15 * as published by the Free Software Foundation; either version 2 16 * of the License, or (at your option) any later version. 17 * 18 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY 19 * KIND, either express or implied. 20 * 21 ****************************************************************************/ 22#include <stdbool.h> 23#include <errno.h> 24#include <stdio.h> 25#include <stdlib.h> 26#include <string.h> 27#include "string-extra.h" 28#include "debug.h" 29#include "powermgmt.h" 30 31#include "misc.h" 32#include "plugin.h" 33#include "dir.h" 34#include "tree.h" 35#include "fileop.h" 36#include "pathfuncs.h" 37 38#include "settings.h" 39#include "lang.h" 40#include "yesno.h" 41#include "splash.h" 42#include "keyboard.h" 43 44/* Used for directory move, copy and delete */ 45struct file_op_params 46{ 47 char path[MAX_PATH]; /* Buffer for full path */ 48 const char* toplevel_name; 49 bool is_dir; 50 int objects; /* how many files and subdirectories*/ 51 int processed; 52 unsigned long long total_size; 53 unsigned long long processed_size; 54 size_t append; /* Append position in 'path' for stack push */ 55 size_t extra_len; /* Length added by dst path compared to src */ 56}; 57 58static int prompt_name(char* buf, size_t bufsz) 59{ 60 if (kbd_input(buf, bufsz, NULL) < 0) 61 return FORC_CANCELLED; 62 /* at least prevent escapes out of the base directory from keyboard- 63 entered filenames; the file code should reject other invalidities */ 64 if (*buf != '\0' && !strchr(buf, PATH_SEPCH) && !is_dotdir_name(buf)) 65 return FORC_SUCCESS; 66 return FORC_UNKNOWN_FAILURE; 67} 68 69static bool poll_cancel_action(int operation, struct file_op_params *param) 70{ 71 static unsigned long last_tick; 72 73 if (operation == FOC_COUNT) 74 { 75 if (param->objects <= 1) 76 last_tick = current_tick; 77 else if (TIME_AFTER(current_tick, last_tick + HZ/2)) 78 { 79 clear_screen_buffer(false); 80 splashf(0, "%s (%d)", str(LANG_SCANNING_DISK), param->objects); 81 last_tick = current_tick; 82 } 83 } 84 else 85 { 86 const char *op_str = (operation == FOC_DELETE) ? str(LANG_DELETING) : 87 (operation == FOC_COPY) ? str(LANG_COPYING) : 88 str(LANG_MOVING); 89 90 if ((operation == FOC_DELETE || !param->total_size) && 91 param->objects > 0) 92 { 93 splash_progress(param->processed, param->objects, 94 "%s %s", op_str, param->toplevel_name); 95 } 96 else if (param->total_size >= 10 * 1024 * 1024) 97 { 98 int total_shft = (int) (param->total_size >> 15); 99 int current_shft = (int) (param->processed_size >> 15); 100 splash_progress(current_shft, total_shft, 101 "%s %s (%d MiB)\n%d MiB", 102 op_str, param->toplevel_name, 103 total_shft >> 5, current_shft >> 5); 104 } 105 else if (param->total_size >= 1024) 106 { 107 int total_kib = (int) (param->total_size >> 10); 108 int current_kib = (int) (param->processed_size >> 10); 109 splash_progress(current_kib, total_kib, 110 "%s %s (%d KiB)\n%d KiB", 111 op_str, param->toplevel_name, 112 total_kib, current_kib); 113 } 114 } 115 return ACTION_STD_CANCEL == get_action(CONTEXT_STD, TIMEOUT_NOBLOCK); 116} 117 118static void init_file_op(struct file_op_params *param, 119 const char *basename, 120 const char *selected_file) 121{ 122 /* Assumes: basename will never be NULL */ 123 if (selected_file == NULL) 124 { 125 param->append = strlcpy(param->path, basename, sizeof (param->path)); 126 path_basename(basename, &basename); 127 param->toplevel_name = basename; 128 } 129 else 130 { 131 param->append = path_append(param->path, basename, 132 selected_file, sizeof (param->path)); 133 param->toplevel_name = selected_file; 134 } 135 param->is_dir = dir_exists(param->path); 136 param->extra_len = 0; 137 param->objects = 0; /* how many files and subdirectories*/ 138 param->processed = 0; 139 param->total_size = 0; 140 param->processed_size = 0; 141} 142 143/* counts file objects, deletes file objects */ 144static int directory_fileop(struct file_op_params *param, enum file_op_current fileop) 145{ 146 errno = 0; 147 DIR *dir = opendir(param->path); 148 if (!dir) { 149 if (errno == EMFILE) { 150 return FORC_TOO_MANY_SUBDIRS; 151 } 152 return FORC_PATH_NOT_EXIST; /* open error */ 153 } 154 int rc = FORC_SUCCESS; 155 size_t append = param->append; 156 157 /* walk through the directory content */ 158 while (rc == FORC_SUCCESS) { 159 errno = 0; /* distinguish failure from eod */ 160 struct dirent *entry = readdir(dir); 161 if (!entry) { 162 if (errno) { 163 rc = FORC_PATH_NOT_EXIST; 164 } 165 break; 166 } 167 168 struct dirinfo info = dir_get_info(dir, entry); 169 if ((info.attribute & ATTR_DIRECTORY) && 170 is_dotdir_name(entry->d_name)) { 171 continue; /* skip these */ 172 } 173 174 /* append name to current directory */ 175 param->append = append + path_append(&param->path[append], 176 PA_SEP_HARD, entry->d_name, 177 sizeof (param->path) - append); 178 179 if (fileop == FOC_DELETE) 180 { 181 param->processed++; 182 /* at this point we've already counted and never had a path too long 183 in the other branch so we 'should not' encounter one here either */ 184 185 if (param->processed > param->objects) 186 { 187 rc = FORC_UNKNOWN_FAILURE; 188 break; 189 } 190 191 if (info.attribute & ATTR_DIRECTORY) { 192 /* remove a subdirectory */ 193 rc = directory_fileop(param, fileop); /* recursion */ 194 } else { 195 /* remove a file */ 196 if (poll_cancel_action(FOC_DELETE, param)) 197 { 198 rc = FORC_CANCELLED; 199 break; 200 } 201 rc = remove(param->path); 202 } 203 } 204 else /* count objects */ 205 { 206 param->objects++; 207 param->total_size += info.size; 208 209 if (poll_cancel_action(FOC_COUNT, param)) 210 { 211 rc = FORC_CANCELLED; 212 break; 213 } 214 215 if (param->append + param->extra_len >= sizeof (param->path)) { 216 rc = FORC_PATH_TOO_LONG; 217 break; /* no space left in buffer */ 218 } 219 220 if (info.attribute & ATTR_DIRECTORY) { 221 /* enter subdirectory */ 222 rc = directory_fileop(param, FOC_COUNT); /* recursion */ 223 } 224 } 225 param->append = append; /* other functions may use param, reset append */ 226 /* Remove basename we added above */ 227 param->path[append] = '\0'; 228 } 229 230 closedir(dir); 231 232 if (fileop == FOC_DELETE && rc == FORC_SUCCESS) { 233 /* remove the now empty directory */ 234 if (poll_cancel_action(FOC_DELETE, param)) 235 { 236 rc = FORC_CANCELLED; 237 } else { 238 rc = rmdir(param->path); 239 } 240 } 241 242 return rc; 243} 244 245/* Walk a directory tree and count the number of objects (dirs & files) 246 * also check that enough resources exist to do an operation */ 247static int check_count_fileobjects(struct file_op_params *param) 248{ 249 cpu_boost(true); 250 int rc = directory_fileop(param, FOC_COUNT); 251 cpu_boost(false); 252 DEBUGF("%s res:(%d) objects %d \n", __func__, rc, param->objects); 253 return rc; 254} 255 256/* Attempt to just rename a file or directory */ 257static int move_by_rename(struct file_op_params *src, 258 const char *dst_path, 259 unsigned int *pflags) 260{ 261 unsigned int flags = *pflags; 262 int rc = FORC_UNKNOWN_FAILURE; 263 reset_poweroff_timer(); 264 if (!(flags & (PASTE_COPY | PASTE_EXDEV))) { 265 if ((flags & PASTE_OVERWRITE) || !file_exists(dst_path)) { 266 /* Just try to move the directory / file */ 267 rc = rename(src->path, dst_path); 268#ifdef HAVE_MULTIVOLUME 269 if (rc < FORC_SUCCESS && errno == EXDEV) { 270 /* Failed because cross volume rename doesn't work */ 271 *pflags |= PASTE_EXDEV; /* force a move instead */ 272 } 273#endif /* HAVE_MULTIVOLUME */ 274 /* if (errno == ENOTEMPTY && (flags & PASTE_OVERWRITE)) { 275 * Directory is not empty thus rename() will not do a quick overwrite */ 276 } 277 278 } 279 return rc; 280} 281 282/* Paste a file */ 283static int copy_move_file(struct file_op_params *src, const char *dst_path, 284 unsigned int flags) 285{ 286 /* Try renaming first */ 287 int rc = move_by_rename(src, dst_path, &flags); 288 if (rc == FORC_SUCCESS) 289 { 290 src->total_size = 0; /* switch from counting size to number of items */ 291 return rc; 292 } 293 294 /* See if we can get the plugin buffer for the file copy buffer */ 295 size_t buffersize; 296 char *buffer = (char *) plugin_get_buffer(&buffersize); 297 if (buffer == NULL || buffersize < 512) { 298 /* Not large enough, try for a disk sector worth of stack 299 instead */ 300 buffersize = 512; 301 buffer = (char *)alloca(buffersize); 302 } 303 304 if (buffer == NULL) { 305 return FORC_NO_BUFFER_AVAIL; 306 } 307 308 buffersize &= ~0x1ff; /* Round buffer size to multiple of sector size */ 309 310 int src_fd = open(src->path, O_RDONLY); 311 if (src_fd >= 0) { 312 off_t src_sz = lseek(src_fd, 0, SEEK_END); 313 if (!src->total_size && !src->processed) /* single file copy */ 314 src->total_size = src_sz; 315 lseek(src_fd, 0, SEEK_SET); 316 317 int oflag = O_WRONLY|O_CREAT; 318 319 if (!(flags & PASTE_OVERWRITE)) { 320 oflag |= O_EXCL; 321 } 322 323 int dst_fd = open(dst_path, oflag, 0666); 324 if (dst_fd >= 0) { 325 off_t total_size = 0; 326 off_t next_cancel_test = 0; /* No excessive button polling */ 327 328 rc = FORC_SUCCESS; 329 330 while (rc == FORC_SUCCESS) { 331 if (total_size >= next_cancel_test) { 332 next_cancel_test = total_size + 0x10000; 333 if (poll_cancel_action(!(flags & PASTE_COPY) ? 334 FOC_MOVE : FOC_COPY, src)) 335 { 336 rc = FORC_CANCELLED; 337 break; 338 } 339 } 340 341 ssize_t bytesread = read(src_fd, buffer, buffersize); 342 if (bytesread <= 0) { 343 if (bytesread < 0) { 344 rc = FORC_READ_FAILURE; 345 } 346 /* else eof on buffer boundary; nothing to write */ 347 break; 348 } 349 350 ssize_t byteswritten = write(dst_fd, buffer, bytesread); 351 if (byteswritten < bytesread) { 352 /* Some I/O error */ 353 rc = FORC_WRITE_FAILURE; 354 break; 355 } 356 357 total_size += byteswritten; 358 src->processed_size += byteswritten; 359 360 if (bytesread < (ssize_t)buffersize) { 361 /* EOF with trailing bytes */ 362 break; 363 } 364 } 365 366 if (rc == FORC_SUCCESS) { 367 if (total_size != src_sz) 368 rc = FORC_UNKNOWN_FAILURE; 369 else { 370 /* If overwriting, set the correct length if original was longer */ 371 rc = ftruncate(dst_fd, total_size) * 10; 372 } 373 } 374 375 close(dst_fd); 376 377 if (rc != FORC_SUCCESS) { 378 /* Copy failed. Cleanup. */ 379 remove(dst_path); 380 } 381 } 382 383 close(src_fd); 384 } 385 386 if (rc == FORC_SUCCESS && !(flags & PASTE_COPY)) { 387 /* Remove the source file */ 388 rc = remove(src->path) * 10; 389 } 390 391 return rc; 392} 393 394/* Paste a directory */ 395static int copy_move_directory(struct file_op_params *src, 396 struct file_op_params *dst, 397 unsigned int flags) 398{ 399 DIR *srcdir = opendir(src->path); 400 401 if (!srcdir) 402 return FORC_PATH_NOT_EXIST; 403 404 /* Make a directory to copy things to */ 405 int rc = mkdir(dst->path) * 10; 406 if (rc < 0 && errno == EEXIST && (flags & PASTE_OVERWRITE)) { 407 /* Exists and overwrite was approved */ 408 rc = FORC_SUCCESS; 409 } 410 411 size_t srcap = src->append, dstap = dst->append; 412 413 /* Walk through the directory content; this loop will exit as soon as 414 there's a problem */ 415 while (rc == FORC_SUCCESS) { 416 errno = 0; /* Distinguish failure from eod */ 417 struct dirent *entry = readdir(srcdir); 418 if (!entry) { 419 if (errno) { 420 rc = FORC_PATH_NOT_EXIST; 421 } 422 break; 423 } 424 425 struct dirinfo info = dir_get_info(srcdir, entry); 426 if ((info.attribute & ATTR_DIRECTORY) && 427 is_dotdir_name(entry->d_name)) { 428 continue; /* Skip these */ 429 } 430 431 /* Append names to current directories */ 432 src->append = srcap + 433 path_append(&src->path[srcap], PA_SEP_HARD, entry->d_name, 434 sizeof (src->path) - srcap); 435 436 dst->append = dstap + 437 path_append(&dst->path[dstap], PA_SEP_HARD, entry->d_name, 438 sizeof (dst->path) - dstap); 439 /* src length was already checked by check_count_fileobjects() */ 440 if (dst->append >= sizeof (dst->path)) { 441 rc = FORC_PATH_TOO_LONG; /* No space left in buffer */ 442 break; 443 } 444 445 src->processed++; 446 if (src->processed > src->objects) 447 { 448 rc = FORC_UNKNOWN_FAILURE; 449 break; 450 } 451 452 if (poll_cancel_action(!(flags & PASTE_COPY) ? 453 FOC_MOVE : FOC_COPY, src)) 454 { 455 rc = FORC_CANCELLED; 456 break; 457 } 458 459 DEBUGF("Copy %s to %s\n", src->path, dst->path); 460 461 if (info.attribute & ATTR_DIRECTORY) { 462 src->processed_size += info.size; 463 /* Copy/move a subdirectory */ 464 rc = copy_move_directory(src, dst, flags); /* recursion */; 465 } else { 466 /* Copy/move a file */ 467 rc = copy_move_file(src, dst->path, flags); 468 } 469 470 /* Remove basenames we added above */ 471 src->path[srcap] = '\0'; 472 dst->path[dstap] = '\0'; 473 } 474 475 if (rc == FORC_SUCCESS && !(flags & PASTE_COPY)) { 476 /* Remove the now empty directory */ 477 rc = rmdir(src->path) * 10; 478 } 479 480 closedir(srcdir); 481 return rc; 482} 483 484/************************************************************************************/ 485/* PUBLIC FUNCTIONS */ 486/************************************************************************************/ 487 488/* Copy or move a file or directory see: file_op_flags */ 489int copy_move_fileobject(const char *src_path, const char *dst_path, unsigned int flags) 490{ 491 if (!src_path[0]) 492 return FORC_NOOP; 493 494 struct file_op_params src, dst; 495 496 /* Figure out the name of the selection */ 497 const char *nameptr; 498 path_basename(src_path, &nameptr); 499 500 /* Final target is current directory plus name of selection */ 501 init_file_op(&dst, dst_path, nameptr); 502 if (dst.append >= sizeof (dst.path)) 503 return FORC_PATH_TOO_LONG; 504 505 int rel = relate(src_path, dst.path); 506 if (rel == RELATE_SAME) 507 return FORC_NOOP; 508 509 if (rel == RELATE_DIFFERENT) { 510 int rc; 511 if (file_exists(dst.path)) { 512 /* If user chooses not to overwrite, cancel */ 513 if (!yesno_pop(ID2P(LANG_REALLY_OVERWRITE))) 514 { 515 splash(HZ, ID2P(LANG_CANCEL)); 516 return FORC_NOOVERWRT; 517 } 518 519 flags |= PASTE_OVERWRITE; 520 } 521 522 init_file_op(&src, src_path, NULL); 523 if (src.append >= sizeof (src.path)) 524 return FORC_PATH_TOO_LONG; 525 /* Now figure out what we're doing */ 526 cpu_boost(true); 527 if (src.is_dir) { 528 /* Copy or move a subdirectory */ 529 /* Try renaming first */ 530 rc = move_by_rename(&src, dst.path, &flags); 531 if (rc < FORC_SUCCESS) { 532 int extra_len = dst.append - src.append; 533 if (extra_len > 0) 534 src.extra_len = extra_len; 535 536 rc = check_count_fileobjects(&src); 537 if (rc == FORC_SUCCESS) { 538 rc = copy_move_directory(&src, &dst, flags); 539 } 540 } 541 } else { 542 /* Copy or move a file */ 543 rc = copy_move_file(&src, dst.path, flags); 544 } 545 546 cpu_boost(false); 547 DEBUGF("%s res: %d, ct: %d/%d %s\n", 548 __func__, rc, src.objects, src.processed, src.path); 549 return rc; 550 } 551 552 /* Else Some other relation / failure */ 553 DEBUGF("%s res: %d, rel: %d\n", __func__, FORC_UNKNOWN_FAILURE, rel); 554 return FORC_UNKNOWN_FAILURE; 555} 556 557int create_dir(void) 558{ 559 int rc; 560 char dirname[MAX_PATH]; 561 size_t pathlen = path_append(dirname, getcwd(NULL, 0), PA_SEP_HARD, 562 sizeof (dirname)); 563 char *basename = dirname + pathlen; 564 565 if (pathlen >= sizeof (dirname)) 566 return FORC_PATH_TOO_LONG; 567 568 rc = prompt_name(basename, sizeof (dirname) - pathlen); 569 if (rc == FORC_SUCCESS) 570 rc = mkdir(dirname) * 10; 571 return rc; 572} 573 574/* share code for file and directory deletion, saves space */ 575int delete_fileobject(const char *selected_file) 576{ 577 int rc; 578 struct file_op_params param; 579 init_file_op(&param, selected_file, NULL); 580 if (param.append >= sizeof (param.path)) 581 return FORC_PATH_TOO_LONG; 582 583 /* Note: delete_fileobject() will happily delete whatever 584 * path is passed (after confirmation) */ 585 if (confirm_delete_yesno(param.path) != YESNO_YES) { 586 return FORC_CANCELLED; 587 } 588 589 if (param.is_dir) { 590 int rc = check_count_fileobjects(&param); 591 DEBUGF("%s res: %d, ct: %d, %s", __func__, rc, param.objects, param.path); 592 if (rc != FORC_SUCCESS) 593 return rc; 594 } 595 596 clear_screen_buffer(true); 597 598 if (param.is_dir) { /* if directory */ 599 cpu_boost(true); 600 rc = directory_fileop(&param, FOC_DELETE); 601 cpu_boost(false); 602 } else { 603 param.objects = param.processed = 1; 604 if (poll_cancel_action(FOC_DELETE, &param)) 605 return FORC_CANCELLED; 606 rc = remove(param.path) * 10; 607 } 608 609 return rc; 610} 611 612int rename_file(const char *selected_file) 613{ 614 int rc; 615 char newname[MAX_PATH]; 616 char *newext = NULL; 617 const char *oldbase, *selection = selected_file; 618 619 path_basename(selection, &oldbase); 620 size_t pathlen = oldbase - selection; 621 char *newbase = newname + pathlen; 622 623 if (strmemccpy(newname, selection, sizeof (newname)) == NULL) 624 return FORC_PATH_TOO_LONG; 625 626 if ((*tree_get_context()->dirfilter > NUM_FILTER_MODES) && 627 (newext = strrchr(newbase, '.'))) 628 /* hide extension when renaming in lists restricted to a 629 single file format, such as in the Playlists menu */ 630 *newext = '\0'; 631 632 rc = prompt_name(newbase, sizeof (newname) - pathlen); 633 634 if (rc != FORC_SUCCESS) 635 return rc; 636 637 if (newext) /* re-add original extension */ 638 strlcat(newbase, strrchr(selection, '.'), sizeof (newname) - pathlen); 639 640 if (!strcmp(oldbase, newbase)) 641 return FORC_NOOP; /* No change at all */ 642 643 int rel = relate(selection, newname); 644 if (rel == RELATE_DIFFERENT) 645 { 646 if (file_exists(newname)) { /* don't overwrite */ 647 return FORC_PATH_EXISTS; 648 } 649 return rename(selection, newname) * 10; 650 } 651 if (rel == RELATE_SAME) 652 return rename(selection, newname) * 10; 653 654 /* Else Some other relation / failure */ 655 return FORC_UNKNOWN_FAILURE; 656}