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