Imports a ProjectSend v1 install — its database and its uploaded files — into ProjectSend v2.
This is deliberately not part of v2. Migrating happens once, if ever, and the engine it needs — arbitrary database connections plus direct writes across the whole schema — is not something every install should carry idle. Install it when you want it, remove it when you're done.
composer require projectsend/v1-migration-tool
php artisan migrate # creates this package's two tables
npm run build # so its screen enters the frontend bundleThen open /system/migrate as a staff user with the Edit settings permission.
There is no sidebar link on purpose — a one-time tool doesn't earn a permanent slot in
the navigation.
When the migration is done:
php artisan projectsend:migrate:reset --drop # optional; also drops this package's tables
composer remove projectsend/v1-migration-tool--drop throws away the v1 → v2 id map. Keep it if you may ever want to redirect old
download.php?id=… links, which is the only thing that can resolve them.
| Requirement | Why |
|---|---|
| ProjectSend v2 with the schema this tool writes | Checked before anything runs — see src/Host/HostTables.php and src/Preflight/HostSchemaCheck.php. A mismatch is a hard stop, never a partial import |
| A fresh install — set up, not yet used | There are no merge semantics. See src/Preflight/FreshInstallCheck.php |
The staff route middleware alias and an edit_settings Gate |
The host's IdentityServiceProvider registers both |
| A running queue worker | A real import outlives any web request; the UI queues a job and polls a row |
Direct — v1 and v2 on the same machine. Point the tool at v1's database and its install directory; nothing is copied that doesn't have to be. This is the fast path: on one filesystem, file bytes are hardlinked rather than duplicated, so 400 GB migrates in seconds.
Bundle — v1 somewhere the v2 install cannot reach (a hosted ProjectSend, or simply a
different server). Run bin/projectsend-v1-export.php on the v1 box; it produces a
portable bundle you upload here. The exporter is a single dependency-free PHP file and
never writes to the v1 install.
Direct mode can be switched off entirely with V1_MIGRATION_DIRECT_MODE=false — letting
an administrator type an arbitrary database host into a web form is fine on a box they
own and unwanted on a hosted deployment.
It reports these rather than guessing, and names every affected row:
- Encrypted files. v1 could encrypt files at rest; v2 has no equivalent, and the keys
are wrapped by a master key that exists only in v1's
sys.config.php. - Files on S3, GCS or Azure. v1's per-file external storage doesn't map onto v2's single-bucket setting.
- Hidden assignments — v2 has no hidden state.
- Two-factor secrets — encrypted with v1's key; those users re-enrol.
- v1 options with no v2 equivalent — v2 has ~43 settings where v1 had ~180.
It also cannot add an Action, Capability or Setting case: all three are closed
PHP enums the host casts database columns to, so a value invented here would throw on
read for every row afterwards — including rows this tool never touched. Anything v1
logged that v2 has no vocabulary for is dropped and counted, never approximated.
v1 signs in with a username. v2 signs in with an email address. Every client's login therefore changes, and v1 did not require emails to be unique — preflight refuses to run until duplicates, blanks and invalid addresses are resolved, so nobody is silently merged or locked out. Tell your clients before you cut over; the run report gives you the list.
composer install
vendor/bin/pestTo exercise it against a real ProjectSend, scripts/sim.sh builds a disposable v2 with
this package installed and stages the seeded v1 fixtures into it:
scripts/sim.sh up # a v2 of its own on :8192
scripts/sim.sh fixture small # stage a v1 install
scripts/sim.sh run small # preflight, import, verify
scripts/sim.sh reset # undo it and try againA development checkout cannot stand in for that instance: the tool imports into a fresh install only, and a checkout has content and shares its file storage with everything else you have been doing.
Stop instances you are not using (scripts/sim.sh down, or docker compose -p projectsend-<name> stop to keep their data). Each one runs its own MySQL, and
Docker's VM has a memory limit well below the host's — when it is reached the
kernel kills MySQL, which surfaces later as getaddrinfo for db failed on a page
that worked ten minutes earlier. Exited (137) in docker ps -a is the tell.
Tests run against a throwaway Testbench app with no host present, so the host tables are
replicated in tests/Support/HostSchema.php. That replica is a convenience, not the
contract — the contract is src/Host/HostTables.php, and it is verified against the real
database at preflight time.
Free software under the GNU General Public License v2, or (at your option) any later version — see LICENSE, the same terms as ProjectSend itself.
Released as MIT up to and including v1.0.0; GPL from v1.0.1 onward. Anyone who took an earlier release under MIT keeps those terms for that release.
Contributions require signing a CLA — individual or entity — for the reasons set out in ProjectSend's CONTRIBUTING.md.