Files Versions Developer Guide - edmozley/freeitsm GitHub Wiki
5.2.0 · #303 (from pappas81) · User guide: Files § Versions · Module guide: Files - Developer Guide · Storage: Files storage - Developer Guide
A file (files_items) points at its current version (current_version_id), and every earlier upload stays in files_versions. In 5.2.0 versions became a feature you can see and act on:
- a v3 badge on files with more than one version;
- Version history and Upload new version on a file's right-click menu, with an optional note;
- Restore and Delete on each version;
- the same-name choices (New version / Keep both / Overwrite / Skip) and how many versions to keep, both set under Files → Settings → Versions.
All the rules live in includes/services/files_versions.php.
| File | What |
|---|---|
includes/services/files_versions.php |
FilesVersionsService (settings, saveSettings, restore, deleteVersion, overwrite, applyKeep) and filesPurgeBytesIfUnused(); FILES_CONFLICT_CHOICES, FILES_KEEP_VERSIONS_CHOICES
|
api/files/upload.php |
start takes on_conflict (only the offered choices), item_id (Upload new version) and note; finish stores the note, then overwrites or applies keep-N |
api/files/item.php |
restore_version (Upload level) and delete_version (Modify) |
api/files/properties.php |
Each version's note and restored_from
|
api/files/settings.php |
?tab=versions GET / POST, Cap::FILES_VERSIONS
|
cron/files_versions_tidy.php |
Job files_versions_tidy - daily, CLI only, needed while keep-N is set |
includes/capabilities.php, files/settings/manifest.php
|
Cap::FILES_VERSIONS (files.versions, sensitive) and its tab |
files/settings/index.php, assets/js/files-settings.js (v=6) |
The Versions tab |
assets/js/files-desktop.js (v=14), assets/css/files.css (v=13) |
The same-name dialog, the right-click items, the badge, Properties' Restore / Delete |
includes/files/audit.php |
New audit actions restore_version, delete_version, overwrite
|
-- files_versions: two new columns
note VARCHAR(255) NULL, -- what changed, typed with Upload new version
restored_from_no INT NULL, -- set = this version is a restore of version N (shares its bytes)
-- files_uploads: the choice and the note travel from start to finish
replace_mode VARCHAR(10) NULL, -- 'overwrite' = delete the file's earlier versions on finish
version_note VARCHAR(255) NULL,Settings (system_settings, written only by FilesVersionsService::saveSettings()):
| Key | Values | Default |
|---|---|---|
files_conflict_choices |
comma list of version, rename, overwrite, skip
|
version,rename,skip - Overwrite is off
|
files_conflict_default |
the highlighted choice (never skip) |
version |
files_keep_versions |
one of 0, 2, 3, 5, 10, 20, 50 (a dropdown) - 0 = every version |
0 |
// api/files/upload.php - start
$vs = FilesVersionsService::settings($conn);
if ($conflict !== 'ask' && !in_array($conflict, $vs['choices'], true) && !($target > 0 && $conflict === 'version')) {
filesApiFail('That choice is not offered here - see Files -> Settings -> Versions.');
}
if ($conflict === 'overwrite') filesNeed($conn, $analystId, $folderId, FilesAcl::MODIFY, 'overwrite');
// ... a clash with $conflict === 'ask' answers:
filesApiOk(['conflict' => true, 'item_id' => ..., 'name' => ..., 'choices' => $vs['choices'], 'default' => $vs['default']]);// assets/js/files-desktop.js - the dialog is drawn from what the server offered
function askConflict(u, name, offer) {
var choices = (offer && offer.choices) || ['version', 'rename', 'skip'];
var buttons = choices.map(function (c) { return { label: LABEL[c], value: c, primary: c === dflt, cancel: c === 'skip' }; });
// ... "Do this for every clash in this upload" remembers the answer for the batch
}🔑 The server decides what is allowed, not the dialog. A choice the settings do not offer is refused at start, whatever the browser sends. Overwrite needs Modify on the folder, because it deletes versions. It is checked at start and again at finish, in case the grant was removed while a big file was going up.
Upload new version sends item_id. The upload then becomes that file's next version whatever the picked file is called, and it is always allowed with Upload, whatever the same-name settings are.
// Restore: a NEW version that shares the old one's bytes - nothing is lost
INSERT INTO files_versions (item_id, version_no, storage_path, size_bytes, mime_type, sha256, uploaded_by, uploaded_datetime, note, restored_from_no)
VALUES (?, MAX(version_no) + 1, <old version's storage_path>, ..., <old note>, <old version_no>);
UPDATE files_items SET current_version_id = <new id>, size_bytes = ..., mime_type = ...;-
Delete version: never the current one. The row goes, then
filesPurgeBytesIfUnused(). - Overwrite: every version except the new one goes, and the new one is renumbered 1. The file starts again.
-
Keep N (
applyKeep()): rows are ordered current first, then newest. Everything past the first N goes. So the current version always stays, even when it is a restored old number. It runs:- on every new version (upload or restore);
- once a day from the job, for files nobody has touched since the setting was lowered.
function filesPurgeBytesIfUnused(PDO $conn, string $key): void
{
if (!filesStorageKeyIsValid($key)) return;
// ANY files_versions row naming it - another version, a copied file, a binned file's history - keeps it
$st = $conn->prepare("SELECT 1 FROM files_versions WHERE storage_path = ? LIMIT 1");
$st->execute([$key]);
if ($st->fetchColumn()) return;
// During a storage move the bytes may be in the old place, the new place, or both
foreach ([filesActiveStore($conn), filesPreviousStore($conn)] as $store) { ... $store->delete($key) ... }
$conn->prepare("DELETE FROM storage_migration_items WHERE store = 'files' AND object_key = ?")->execute([$key]);
}🔑 Three things share bytes, and each one is why this check exists:
-
a copied file - its version 1 is the source's
storage_path; - a restored version - it points at the version it restores;
- a binned file - its versions stay until a purge.
- TRAP: never delete bytes because a version row went. Call
filesPurgeBytesIfUnused(). It deletes only when nothing at all names the path. - TRAP:
applyKeep()ordersis_current DESCfirst. Ordering byversion_noalone would delete a restored current version whose number is old. - TRAP: the same-name choices are enforced at
upload.php?action=start, not in the dialog. A browser can send anyon_conflict. - TRAP:
FilesVersionsService::restore()joins a caller's transaction ($own = !$conn->inTransaction()). Opening its own transaction inside another fails with "There is already an active transaction". The test found this.
-
tests/files-versions.php(18, test database, temporary storage folder, rolled back):- the defaults, and saving refusing Skip alone, an unticked highlighted choice and a keep value not in the list;
- restore making version 3, which shares version 1's bytes, with version 2 kept;
- deleting the current version refused;
- deleting a version whose bytes are still shared keeps them, and deleting one nothing shares removes them;
- keep 2 of 5 keeping the current (old-numbered) version and the newest;
- overwrite leaving one version;
- a copied file's bytes never deleted.
-
A live run on the test install through the real endpoints (15):
- the tab;
- the conflict reply offering the settings' choices;
- Overwrite refused until it is ticked, then leaving one version numbered 1;
- New version making version 2, and Keep both saving
notes (2).txt; - Upload new version targeting the file whatever its name, with the note stored on one line;
- Restore making version 4 and the download giving version 1's contents;
- deleting the current version refused, deleting version 2 working;
- keep-2 trimming on the next upload.
- Chrome: the Versions tab fills in (choices, highlighted, keep list), and the Files desktop starts.
See also: Files - Developer Guide · Files storage - Developer Guide · Scheduled tasks