Skip to content

Commit 53da45d

Browse files
committed
Added command to download all external-only attachments to the local file system
1 parent 57f0432 commit 53da45d

File tree

2 files changed

+139
-2
lines changed

2 files changed

+139
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
24+
namespace App\Command\Attachments;
25+
26+
use App\Entity\Attachments\Attachment;
27+
use App\Entity\Attachments\AttachmentUpload;
28+
use App\Exceptions\AttachmentDownloadException;
29+
use App\Services\Attachments\AttachmentManager;
30+
use App\Services\Attachments\AttachmentSubmitHandler;
31+
use Doctrine\ORM\EntityManagerInterface;
32+
use Symfony\Component\Console\Attribute\AsCommand;
33+
use Symfony\Component\Console\Command\Command;
34+
use Symfony\Component\Console\Input\InputInterface;
35+
use Symfony\Component\Console\Output\OutputInterface;
36+
use Symfony\Component\Console\Style\SymfonyStyle;
37+
38+
#[AsCommand('partdb:attachments:download', "Downloads all attachments which have only an external URL to the local filesystem.")]
39+
class DownloadAttachmentsCommand extends Command
40+
{
41+
public function __construct(private readonly AttachmentSubmitHandler $attachmentSubmitHandler,
42+
private EntityManagerInterface $entityManager)
43+
{
44+
parent::__construct();
45+
}
46+
47+
public function configure(): void
48+
{
49+
$this->setHelp('This command downloads all attachments, which only have an external URL, to the local filesystem, so that you have an offline copy of the attachments.');
50+
$this->addOption('--private', null, null, 'If set, the attachments will be downloaded to the private storage.');
51+
}
52+
53+
protected function execute(InputInterface $input, OutputInterface $output): int
54+
{
55+
$io = new SymfonyStyle($input, $output);
56+
57+
$qb = $this->entityManager->createQueryBuilder();
58+
$qb->select('attachment')
59+
->from(Attachment::class, 'attachment')
60+
->where('attachment.external_path IS NOT NULL')
61+
->andWhere('attachment.external_path != \'\'')
62+
->andWhere('attachment.internal_path IS NULL');
63+
64+
$query = $qb->getQuery();
65+
$attachments = $query->getResult();
66+
67+
if (count($attachments) === 0) {
68+
$io->success('No attachments with external URL found.');
69+
return Command::SUCCESS;
70+
}
71+
72+
$io->note('Found ' . count($attachments) . ' attachments with external URL, that will be downloaded.');
73+
74+
//If the option --private is set, the attachments will be downloaded to the private storage.
75+
$private = $input->getOption('private');
76+
if ($private) {
77+
if (!$io->confirm('Attachments will be downloaded to the private storage. Continue?')) {
78+
return Command::SUCCESS;
79+
}
80+
} else {
81+
if (!$io->confirm('Attachments will be downloaded to the public storage, where everybody knowing the correct URL can access it. Continue?')){
82+
return Command::SUCCESS;
83+
}
84+
}
85+
86+
$progressBar = $io->createProgressBar(count($attachments));
87+
$progressBar->setFormat("%current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% \n%message%");
88+
89+
$progressBar->setMessage('Starting download...');
90+
$progressBar->start();
91+
92+
93+
$errors = [];
94+
95+
foreach ($attachments as $attachment) {
96+
/** @var Attachment $attachment */
97+
$progressBar->setMessage(sprintf('%s (ID: %s) from %s', $attachment->getName(), $attachment->getID(), $attachment->getHost()));
98+
$progressBar->advance();
99+
100+
try {
101+
$attachmentUpload = new AttachmentUpload(file: null, downloadUrl: true, private: $private);
102+
$this->attachmentSubmitHandler->handleUpload($attachment, $attachmentUpload);
103+
104+
//Write changes to the database
105+
$this->entityManager->flush();
106+
} catch (AttachmentDownloadException $e) {
107+
$errors[] = [
108+
'attachment' => $attachment,
109+
'error' => $e->getMessage()
110+
];
111+
}
112+
}
113+
114+
$progressBar->finish();
115+
116+
//Fix the line break after the progress bar
117+
$io->newLine();
118+
$io->newLine();
119+
120+
if (count($errors) > 0) {
121+
$io->warning('Some attachments could not be downloaded:');
122+
foreach ($errors as $error) {
123+
$io->warning(sprintf("Attachment %s (ID %s) could not be downloaded from %s:\n%s",
124+
$error['attachment']->getName(),
125+
$error['attachment']->getID(),
126+
$error['attachment']->getExternalPath(),
127+
$error['error'])
128+
);
129+
}
130+
} else {
131+
$io->success('All attachments downloaded successfully.');
132+
}
133+
134+
return Command::SUCCESS;
135+
}
136+
}

src/Repository/AttachmentRepository.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ public function getExternalAttachments(): int
7575
{
7676
$qb = $this->createQueryBuilder('attachment');
7777
$qb->select('COUNT(attachment)')
78-
->andWhere('attachment.internal_path IS NULL')
79-
->where('attachment.external_path IS NOT NULL');
78+
->where('attachment.external_path IS NOT NULL')
79+
->andWhere('attachment.internal_path IS NULL');
80+
8081
$query = $qb->getQuery();
8182

8283
return (int) $query->getSingleScalarResult();

0 commit comments

Comments
 (0)