Attachments uploaded in the same second share one file on disk
What happened
I attached three files to a task in one go. All three appeared in the attachment list with the correct names, but clicking any of them opens the same file. Whatever I click, I get the contents of the last one. The other two are not reachable at all.
Uploading the same files one at a time, with a pause between them, works.
I also hit this when deleting. I had two attachments on a task, deleted one, and the other one stopped working: it is still listed, but downloading it returns "File not found".
Environment
- Leantime 3.9.8 (
leantime/leantime:3.9.8, officialdocker-leantimecompose stack) - PHP 8.3.32, MySQL 8.4, local disk storage (no S3)
Steps to reproduce — wrong file is served
- Prepare three files with distinct contents:
printf 'CONTENT-ONE\n' > one.txt printf 'CONTENT-TWO\n' > two.txt printf 'CONTENT-THREE\n' > three.txt - Attach all three to the same task at once.
- All three uploads return 200, and all three responses carry the same
encName:one -> HTTP 200 "encName":"2d13ac7bf690b01abab64ab6b4bcfd6e" two -> HTTP 200 "encName":"2d13ac7bf690b01abab64ab6b4bcfd6e" three -> HTTP 200 "encName":"2d13ac7bf690b01abab64ab6b4bcfd6e" - There are three rows but one file:
id encName extension realName 9 2d13ac7bf690b01abab64ab6b4bcfd6e txt one.txt 10 2d13ac7bf690b01abab64ab6b4bcfd6e txt two.txt 11 2d13ac7bf690b01abab64ab6b4bcfd6e txt three.txt -rw-r--r-- 1 www-data www-data 14 userfiles/2d13ac7bf690b01abab64ab6b4bcfd6e.txt - Download each of them:
row 9 one.txt -> CONTENT-THREE row 10 two.txt -> CONTENT-THREE row 11 three.txt -> CONTENT-THREE
Steps to reproduce — deleting one breaks the other
- Attach two files to a task at once, so they share an
encName. - Delete one of them, e.g.
GET /tickets/showTicket/1?delFile=1. - The remaining row is still listed, but its file is gone:
GET /files/get?encName=d09fe271...&ext=docx&realName=Test_Plain.docx -> HTTP 404 "File not found"
Cause
The stored filename comes from md5(userId . time()).
app/Core/Files/FileManager.php:180
$newName = pathinfo($fileName, PATHINFO_FILENAME);
if (config('filesystems.disks.'.$disk.'.renameFiles')) {
$newName = md5(session('userdata.id').time());
$fileName = $newName.'.'.$extension;
}time() has one-second resolution, so every file one user uploads within the same
second gets the same name. $storage->put() overwrites the previous file while a
separate zp_file row is inserted for each upload, so the rows all point at one
object holding the content of whichever upload finished last.
The Uppy widget is configured with autoProceed: true, so selecting several files
sends them all immediately, inside the same second. That is why uploading them one
by one avoids the problem: each request lands in a different second.
# three files selected at once
b1 -> encName b96c4cfd29247726d3f7255101bc3f1f
b2 -> encName b96c4cfd29247726d3f7255101bc3f1f
b3 -> encName b96c4cfd29247726d3f7255101bc3f1f
# same three files, ~1.2 s apart
s1 -> encName b96c4cfd29247726d3f7255101bc3f1f
s2 -> encName ee3c50ad3b124ebc5eaca6ce72f0744e
s3 -> encName 3c56d4f7129a64d6b72e422932577f70s1 landed in the same second as the earlier batch and overwrote it, so afterwards
b1.txt, b2.txt and b3.txt all serve CONTENT-s1. The collision is not
limited to one batch: a later upload by the same user in a matching second replaces
the content of earlier, unrelated attachments.
The attachment list resolves a file by encName + ext; realName only sets the
download name.
app/Domain/Tickets/Templates/submodules/attachments.blade.php:41
/files/get?module={{module}}&encName={{encName}}&ext={{extension}}&realName={{realName}}For the three attachments above the rendered links differ only in that cosmetic parameter, which is why every one of them opens the same file:
/files/get?module=ticket&encName=2d13ac7bf690b01abab64ab6b4bcfd6e&ext=txt&realName=one.txt
/files/get?module=ticket&encName=2d13ac7bf690b01abab64ab6b4bcfd6e&ext=txt&realName=two.txt
/files/get?module=ticket&encName=2d13ac7bf690b01abab64ab6b4bcfd6e&ext=txt&realName=three.txtDeletion removes the physical file by encName.extension without checking whether
another row still references it.
app/Domain/Files/Repositories/Files.php:230
public function deleteFile(int $id): bool
{
$result = $this->db->table('zp_file')->select('encName','extension')->where('id',$id)->first();
if ($result && isset($result->encName) && isset($result->extension)) {
$fileName = $result->encName.'.'.$result->extension;
$this->fileManager->deleteFile($fileName, 'default');
}
return $this->db->table('zp_file')->where('id',$id)->delete() > 0;
}Expected
- Each upload gets its own object in storage.
- Deleting one attachment does not affect another.
Related
#2515 ("File Browser Upload multiple images", open since 2024-05-21) reports uploading 41 images at once and getting "very strange results with many of the same image showing". That matches this behaviour: the rows are distinct and carry the right names, but they all point at a single stored object, so the same picture is rendered over and over.
Source: Leantime/leantime