#3783·leantime

Attachments uploaded in the same second share one file on disk

Author: loberaPogCreated Sep 11, 2026Updated Sep 11, 2026

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, official docker-leantime compose stack)
  • PHP 8.3.32, MySQL 8.4, local disk storage (no S3)

Steps to reproduce — wrong file is served

  1. Prepare three files with distinct contents:
    bash
    printf 'CONTENT-ONE\n'   > one.txt
    printf 'CONTENT-TWO\n'   > two.txt
    printf 'CONTENT-THREE\n' > three.txt
  2. Attach all three to the same task at once.
  3. 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"
  4. 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
  5. 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

  1. Attach two files to a task at once, so they share an encName.
  2. Delete one of them, e.g. GET /tickets/showTicket/1?delFile=1.
  3. 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

php
$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 3c56d4f7129a64d6b72e422932577f70

s1 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.txt

Deletion removes the physical file by encName.extension without checking whether another row still references it.

app/Domain/Files/Repositories/Files.php:230

php
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.