A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
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(¶m->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(¶m, 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(¶m);
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(¶m, FOC_DELETE);
601 cpu_boost(false);
602 } else {
603 param.objects = param.processed = 1;
604 if (poll_cancel_action(FOC_DELETE, ¶m))
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}