---
title: "File batches — Imports for Laravel | Roundly"
description: "Many files or a ZIP, one record per file — FileImporter, allowed extensions, SHA-256 duplicate checks, ImportFile, ZIP-bomb and path-traversal defences."
url: https://roundly-consulting.com/open-source/docs/imports-for-laravel/file-batches
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Imports for Laravel](https://roundly-consulting.com/open-source/docs/imports-for-laravel.md)

# File batches

A FileImporter turns each uploaded file — or each entry of a ZIP — into one record, a document for example:

```php
use RoundlyConsulting\Imports\Definition\{FileRules, Ownership};
use RoundlyConsulting\Imports\FileImporter;
use RoundlyConsulting\Imports\Rows\{ImportContext, ImportFile};

final class DocumentsImporter extends FileImporter
{
    public function key(): string { return 'documents'; }
    public function label(): string { return 'Documents'; }
    public function ownership(): Ownership { return Ownership::column('team_id'); }
    public function accepts(): FileRules { return FileRules::extensions('pdf', 'docx')->maxSize(20_480); }

    public function authorize(?Authenticatable $user, Model $owner, ImportOperation $operation): bool
    {
        return $user?->can('manage-documents', $owner) ?? false;
    }

    public function create(ImportFile $file, ImportContext $context): Model
    {
        return Document::create(['team_id' => $context->owner->getKey(), 'name' => $file->name, 'path' => $file->storeOn('documents')]);
    }
}

Imports::for($team)->by($user)->uploadFiles('documents', $request->file('files'))->preview()->commit();
```

## Methods

| Method | Required | Default |
| --- | --- | --- |
| `key() · label()` | no | from the class name, as for Importer |
| `ownership(): Ownership · authorize(?Authenticatable, Model $owner, ImportOperation): bool` | yes | — (fail closed) |
| `accepts(): FileRules` | yes | — |
| `create(ImportFile $file, ImportContext $context): Model` | yes | — |
| `duplicateCheck(): FileDuplicates` | no | FileDuplicates::ledger() |
| `rollback() · changeDetection() · checksumAttributes()` | no | as for Importer |

FileRules::extensions('pdf', 'docx') (lower-cased match) or FileRules::any(), then ->maxSize(int $kilobytes) (default imports.files.max\_file\_size); allows() and maxBytes() read it back. A file importer has no fields, no mapping and no modes — Imports::importer('documents')->modes() is \[\] and its descriptor lists extensions.

## Preview and duplicates

- Input: one or many uploads and/or ZIP archives (a .zip name with ZIP content); each file or entry is one row, up to files.max\_files. Every entry is listed in the row results (label, size, SHA-256, status).
- Per file: valid, invalid (extension\_not\_allowed, file\_too\_large, empty\_file), a duplicate in the batch (duplicate\_file) or of an earlier import of this owner and importer whose record is still there (file\_imported\_earlier).
- A file whose record an undo kept still counts as imported; only an undo that removed the record (deleted, handled, missing) makes the file importable again.
- Hashes are re-checked at commit. Commit and undo work as for rows — the row number is the entry index and the ledger keeps the content hash.
- errorReport() lists the refused files: a File and an Errors column, one row per file.

```php
use RoundlyConsulting\Imports\Definition\FileDuplicates;

public function duplicateCheck(): FileDuplicates
{
    return FileDuplicates::using(
        fn (string $sha256, Model $owner) => Document::whereBelongsTo($owner, 'team')->where('sha256', $sha256)->first(),
    );   // or FileDuplicates::ledger() (the default), FileDuplicates::none()
}
```

## ImportFile

```php
$file->index;                  // the entry index (the row number)
$file->name;                   // sanitised, metadata only
$file->extension; $file->size; $file->sha256;
$file->readStream();           // a resource
$file->contents();             // small files only
$file->storeOn('s3', 'documents');   // copies under a generated ULID name + extension, returns the path
```

Copies made by storeOn() are outside the commit transaction: a rolled-back chunk can leave one behind, so pair it with a rollback() hook or a cleanup if that matters.

## ZIP safety

- Checked before anything is extracted: opened read-only with a consistency check; the archive within files.max\_archive\_size; entries within files.max\_files; the declared total within files.max\_uncompressed; each entry within the importer’s size cap.
- The compression ratio within files.max\_ratio, per entry and overall, for anything over files.ratio\_min\_size.
- Names: no absolute paths, no .. (also with backslashes), no control characters, at most 255 bytes; no symlinks; no encrypted entries. Directories and files.ignore entries are skipped; nested archives are plain files.
- During extraction the real inflated bytes are counted per entry and in total — a header that lies is stopped mid-stream.
- Entries are stored under generated names; no path from an archive ever touches the disk. Any violation rejects the whole upload and deletes what was staged.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/imports-for-laravel.md)
