All Calendar
My Calendar
World Calendar
📥 Incoming Claims (Cash Sales)
These are log book products sent to your email. Sign in with the matching email, then claim them here.
🛍 Log Book Purchases / Owned Records
These are log book records you currently own. You can resell via + Product in the TXTWRK post box, or cash sale directly from here without creating a new post.
🕓 Pending / Listed Log Book Sales
These are your log book records that are listed, pending, or waiting for a buyer to claim by email.
🛍 All Purchases
Every item you purchased across the network.
💰 Sales Made (Sold Items)
Records of products you have sold to others.
🕑 Previous History
Coming soon…
Checklist directory Each view loads fresh results from the database. Search filters only the current loaded view. Select a checklist to reveal its items.
Your Data
Click the button below to download all your data from our platform in CSV format (e.g., posts, purchases).
Download My Data
Forever Data Preservation
Preserve your digital legacy indefinitely on TXTWRK. Meaningful content — posts, creations, and contributions — should never disappear as platforms evolve or close. Launched in 2024, our permanence-first platform gives you full control over how your data is preserved, with transparency, fairness, and infrastructure built to last centuries.
TXTWRK empowers users to choose how their content endures, while offering networks the ability to integrate our API kit to provide the same control to their communities. This ensures a resilient, permanent digital ecosystem that protects history, creativity, and community contributions.
Core Features & Policy
Free Storage: 5GB per user to start, enabling early content contribution.
Lifetime Data Packs: £100 per 1GB for additional storage — transparent, fair pricing.
Forever Preservation: Data is retained indefinitely, with optional heavy archival for communities or networks.
Flexible Control: Users can adjust preservation settings for individual content.
Network Integration: API kit allows other platforms to adopt Forever Preservation for their users.
Long-term Reliability: Infrastructure built to scale, survive, and self-manage for centuries.
Storage & Cost Overview
Storage Size
Price (£)
Description
5GB
Free
Initial allocation for content creation and posting
1GB
£100
Additional permanent storage pack
5GB
£500
Expanded legacy storage for creators or startups
10GB+
£1,000+
Full archival for heavy contributors, communities, or networks
Why TXTWRK Matters
Preserves digital heritage for centuries, supporting culture and community memory.
Aligns storage with actual cost and long-term value, ensuring sustainability.
Creates a permanence-first culture: encouraging meaningful contributions over ephemeral content.
Supports startups, creators, and networks with equal, transparent pricing.
Join the journey to make your contributions last. Update your settings today, invest in your Forever Data Preservation, and help build a digital ecosystem designed for longevity, resilience, and integrity.
Load Forever Settings
Preservation Status:
Active
Inactive
Save Forever Settings
Partner Network Alliance Integration
We are taking steps to innovate & preserve the world's data for 1000's of years to come. Through innovation, collaboration and friendly donors, we can make this possible. Join the cause via Team@TXTWRK.com to support our community initiative. Join our mission to preserve user data forever, whether you’re a major network like TikTok or a micro network for niche communities. Our project, fueled by community support and donors, ensures public profiles and content live on, even if your service shuts down.
Why it matters:
Preserving data protects users’ digital legacy—posts, profiles, and contributions—vital for personal identity, community history, and cultural value.
Requirements:
Submit one request per domain, using your owned domain (e.g., yourdomain.com) and it also helps to make the request from your network/brand page here at TXTWRK such as TXTWRK.com/@TikTok for e.g. or TXTWRK.com/@MyDrivingNetwork
Commitment: If your service closes, you agree to transfer the domain and public user data to us, so we can host profiles at txtwrk.com/network/[yournetwork]/[username]. Our data transfer kit (soon to be developed) will make this seamless, with full transparency about what data will be preserved exactly.
Review process: We evaluate requests for community impact, user value, and alignment with our goal of a resilient digital ecosystem. Submit below and track status in “Your Network Requests.” Contact Team@TXTWRK.com for help. Currently you can install our widget into your settings panel using our Legacy Forecer API kit and your users can set intent today, which helps us understand our role & development better.
Request Network Integration
Select Your Network
Your Networks:
Select a network
Legacy Forever API Kit (kit.php)
Place this file in your /txtwrk/restore/ directory on your server (e.g., yourdomain.com/txtwrk/restore/kit.php). Note: API key is only available for approved networks.
Copy API Kit Code
Settings Widget Code
Paste this code into your settings page where you want the widget to appear. Ensure your session provides $_SESSION['memberID'] (as an integer) or replace with your session-based user ID variable:
Copy Widget Code
Your Network Requests
View the status of your network integration requests.
Load Your Network Requests
Network
Domain
Status
Action
Partner Network Status
View the status of all network partnerships for data preservation.
Load Partner Networks
Review Network Requests
Approve or reject partner network requests for data preservation integration.
Load Review Networks
Network
Domain
Status
Action
Connect Your Server
Verify ownership by uploading a small text file to your server's root directory. This proves you control the domain.
Server Base URL:
Add Server & Get Verification File
1. Create this file on your server
Filename: plug_txtwrk_server.txt Place it in the root directory (where your index.php or homepage lives).Content must be exactly the code below — no extra spaces or newlines at the end.
Copy File Content
2. After uploading the file, click Verify Now in the list below.
Your Connected Servers
Refresh / Load Servers
Messaging Access —
Close
Grant Access
File Access —
Close
Folders (one per line, e.g. /core/dir/123/)
Files (one per line, e.g. file.php or /core/dir/index.php)
Grant File Access
Install TXTWRK Server Bridge
After your server is verified, copy the code below and upload it to:
/server/txtwrk/manage.php
Copy manage.php
Once uploaded, TXTWRK workspaces will be able to connect to your server for file editing, chat, and requests.
<?php
declare(strict_types=1);
/*
TXTWRK client-side server bridge
Location expected:
/server/txtwrk/manage.php
Root resolved as:
/server/txtwrk/../../ => public/server root
*/
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
$action = $_GET['action'] ?? "list";
$root = realpath(__DIR__ . "/../../");
if (!$root || !is_dir($root)) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode(["success" => false, "error" => "Invalid server root"]);
exit;
}
/* ═══════════════════════════════════════════════════════
OUTPUT / BASIC HELPERS
═══════════════════════════════════════════════════════ */
function json_out(array $arr): void {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($arr);
exit;
}
function path_inside_root(string $path, string $root): bool {
$root = rtrim($root, DIRECTORY_SEPARATOR);
return $path === $root || strpos($path, $root . DIRECTORY_SEPARATOR) === 0;
}
function normalise_relative_path(string $path): string {
$path = trim($path);
$path = str_replace("\\", "/", $path);
$path = trim($path, "/");
$parts = [];
foreach (explode("/", $path) as $part) {
if ($part === "" || $part === ".") continue;
if ($part === "..") return "";
$parts[] = $part;
}
return implode("/", $parts);
}
function safe_name_only(string $name): string {
$name = trim($name);
$name = str_replace(["\\", "/"], "", $name);
$name = basename($name);
if ($name === "" || $name === "." || $name === "..") {
return "";
}
return $name;
}
function safe_existing_dir(string $root, string $path): ?string {
$path = normalise_relative_path($path);
$target = $path === "" ? $root : realpath($root . "/" . $path);
if (!$target || !is_dir($target) || !path_inside_root($target, $root)) {
return null;
}
return $target;
}
function safe_existing_file(string $root, string $file): ?string {
$file = normalise_relative_path($file);
if ($file === "") return null;
$base = realpath($root . "/" . $file);
if (!$base || !is_file($base) || !path_inside_root($base, $root)) {
return null;
}
return $base;
}
function safe_child_path(string $targetDir, string $name, string $root): ?string {
$safeName = safe_name_only($name);
if ($safeName === "") return null;
$path = $targetDir . "/" . $safeName;
$parentReal = realpath($targetDir);
if (!$parentReal || !path_inside_root($parentReal, $root)) {
return null;
}
return $path;
}
function get_posted_content(): string {
if (isset($_POST['content'])) {
return (string)$_POST['content'];
}
$raw = file_get_contents("php://input");
return $raw === false ? "" : $raw;
}
/* ═══════════════════════════════════════════════════════
CONTROLLER FILE
═══════════════════════════════════════════════════════ */
function controller_file(string $root): string {
return $root . "/server/txtwrk/manage.txtwrk_controller";
}
function default_controller_data(): array {
return [
"messages" => [],
"requests" => [],
"chat_count" => 0
];
}
function read_controller_data(string $root): array {
$flag = controller_file($root);
if (!file_exists($flag)) {
@mkdir(dirname($flag), 0755, true);
file_put_contents($flag, json_encode(default_controller_data(), JSON_PRETTY_PRINT), LOCK_EX);
}
$raw = file_get_contents($flag);
$data = json_decode($raw ?: "", true);
if (!is_array($data)) {
$data = default_controller_data();
}
if (!isset($data["messages"]) || !is_array($data["messages"])) $data["messages"] = [];
if (!isset($data["requests"]) || !is_array($data["requests"])) $data["requests"] = [];
if (!isset($data["chat_count"])) $data["chat_count"] = 0;
return $data;
}
function write_controller_data(string $root, array $data): bool {
$flag = controller_file($root);
@mkdir(dirname($flag), 0755, true);
$fp = fopen($flag, "c+");
if (!$fp) return false;
flock($fp, LOCK_EX);
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, json_encode($data, JSON_PRETTY_PRINT));
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
return true;
}
/* ═══════════════════════════════════════════════════════
LOCK / EDIT VERSION HELPERS
═══════════════════════════════════════════════════════ */
function member_lock_file(string $base, int $memberID): string {
return $base . "." . intval($memberID);
}
function is_edit_version_file(string $path): bool {
return preg_match('/\.txtwrk\.\d+$/', $path) === 1;
}
function is_request_file(string $path): bool {
return preg_match('/\.requests\.\d+$/', $path) === 1;
}
function is_numeric_lock_file(string $path): bool {
if (is_edit_version_file($path)) return false;
if (is_request_file($path)) return false;
return preg_match('/\.\d+$/', $path) === 1;
}
function get_lock_owner(string $base): ?int {
$locks = glob($base . ".*");
if (!$locks) return null;
foreach ($locks as $lock) {
if (!is_numeric_lock_file($lock)) continue;
if (preg_match('/\.(\d+)$/', $lock, $m)) {
return intval($m[1]);
}
}
return null;
}
function find_lock_file(string $base): ?string {
$locks = glob($base . ".*");
if (!$locks) return null;
foreach ($locks as $lock) {
if (is_numeric_lock_file($lock)) {
return $lock;
}
}
return null;
}
function latest_edit_version(string $base): int {
$files = glob($base . ".txtwrk.*");
$latest = 0;
if ($files) {
foreach ($files as $file) {
if (preg_match('/\.txtwrk\.(\d+)$/', $file, $m)) {
$version = intval($m[1]);
if ($version > $latest) {
$latest = $version;
}
}
}
}
return $latest;
}
function edit_version_file(string $base, int $version): string {
return $base . ".txtwrk." . intval($version);
}
function ensure_first_edit_version(string $base): int {
$latest = latest_edit_version($base);
if ($latest > 0) {
return $latest;
}
$content = file_get_contents($base);
if ($content === false) $content = "";
$first = edit_version_file($base, 1);
file_put_contents($first, $content, LOCK_EX);
return 1;
}
function user_owns_lock(string $base, int $memberID): bool {
return file_exists(member_lock_file($base, $memberID));
}
/* ═══════════════════════════════════════════════════════
RECURSIVE DELETE
═══════════════════════════════════════════════════════ */
function delete_recursive(string $path, string $root): bool {
$real = realpath($path);
if (!$real || !path_inside_root($real, $root) || $real === $root) {
return false;
}
if (is_file($real) || is_link($real)) {
return @unlink($real);
}
if (!is_dir($real)) {
return false;
}
$items = scandir($real);
if ($items === false) {
return false;
}
foreach ($items as $item) {
if ($item === "." || $item === "..") continue;
$child = $real . "/" . $item;
if (!delete_recursive($child, $root)) {
return false;
}
}
return @rmdir($real);
}
/* ═══════════════════════════════════════════════════════
TARGET PATH
═══════════════════════════════════════════════════════ */
$path = normalise_relative_path($_GET['path'] ?? "");
$target = safe_existing_dir($root, $path);
if (!$target) {
json_out([
"success" => false,
"error" => "Invalid path: " . $path
]);
}
/* ═══════════════════════════════════════════════════════
TXTWRK 2026-08 UI BRIDGE EXTENSIONS
- batch/nested create
- recursive directory catalogue
- move
- file status/revalidation
═══════════════════════════════════════════════════════ */
function ensure_relative_directory(string $root, string $relative): ?string {
$relative = normalise_relative_path($relative);
if ($relative === '') return $root;
$current = $root;
foreach (explode('/', $relative) as $part) {
$safe = safe_name_only($part);
if ($safe === '') return null;
$current .= '/' . $safe;
if (!file_exists($current)) {
if (!@mkdir($current, 0755, true) && !is_dir($current)) return null;
}
$real = realpath($current);
if (!$real || !is_dir($real) || !path_inside_root($real, $root)) return null;
$current = $real;
}
return $current;
}
function recursive_directory_list(string $root): array {
$dirs = [''];
try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST,
RecursiveIteratorIterator::CATCH_GET_CHILD
);
foreach ($iterator as $info) {
if (!$info->isDir()) continue;
$absolute = $info->getPathname();
if (!path_inside_root($absolute, $root)) continue;
$relative = substr($absolute, strlen(rtrim($root, DIRECTORY_SEPARATOR)) + 1);
$relative = normalise_relative_path(str_replace(DIRECTORY_SEPARATOR, '/', $relative));
if ($relative !== '') $dirs[] = $relative;
}
} catch (UnexpectedValueException $e) {
// Return whatever was readable. Root is always included.
}
$dirs = array_values(array_unique($dirs));
natcasesort($dirs);
return array_values($dirs);
}
if ($action === "list_dirs_recursive") {
json_out([
"success" => true,
"folders" => recursive_directory_list($root)
]);
}
if ($action === "file_status") {
$file = normalise_relative_path($_GET['file'] ?? '');
if ($file === '') json_out(["success" => false, "exists" => false, "error" => "Missing file"]);
$base = safe_existing_file($root, $file);
if (!$base) {
json_out([
"success" => true,
"exists" => false,
"file" => $file,
"latest_edit" => 0
]);
}
json_out([
"success" => true,
"exists" => true,
"file" => $file,
"size" => (int)(@filesize($base) ?: 0),
"mtime" => (int)(@filemtime($base) ?: 0),
"latest_edit" => latest_edit_version($base)
]);
}
if ($action === "create_paths") {
$entries = $_POST['entries'] ?? [];
if (!is_array($entries)) $entries = [$entries];
$createdFiles = [];
$createdFolders = [];
$errors = [];
foreach ($entries as $rawEntry) {
$rawEntry = trim(str_replace("\\", "/", (string)$rawEntry));
if ($rawEntry === '') continue;
$isFolder = ($rawEntry !== '' && substr($rawEntry, -1) === '/');
$relative = normalise_relative_path($rawEntry);
if ($relative === '') {
$errors[] = ["path" => $rawEntry, "error" => "Invalid path"];
continue;
}
$fullRelative = normalise_relative_path(($path !== '' ? $path . '/' : '') . $relative);
$parts = explode('/', $fullRelative);
$leaf = array_pop($parts);
$parentRelative = implode('/', $parts);
$parent = ensure_relative_directory($root, $parentRelative);
if (!$parent) {
$errors[] = ["path" => $rawEntry, "error" => "Cannot create parent directory"];
continue;
}
if ($isFolder) {
$folder = $parent . '/' . safe_name_only($leaf);
if (safe_name_only($leaf) === '') {
$errors[] = ["path" => $rawEntry, "error" => "Invalid folder name"];
continue;
}
if (file_exists($folder)) {
if (is_dir($folder)) {
$createdFolders[] = $fullRelative;
} else {
$errors[] = ["path" => $rawEntry, "error" => "A file already exists at this path"];
}
continue;
}
if (@mkdir($folder, 0755, true)) $createdFolders[] = $fullRelative;
else $errors[] = ["path" => $rawEntry, "error" => "Cannot create folder"];
continue;
}
$safeLeaf = safe_name_only($leaf);
if ($safeLeaf === '') {
$errors[] = ["path" => $rawEntry, "error" => "Invalid file name"];
continue;
}
$file = $parent . '/' . $safeLeaf;
if (file_exists($file)) {
$errors[] = ["path" => $rawEntry, "error" => "File already exists"];
continue;
}
if (file_put_contents($file, '') !== false) $createdFiles[] = $fullRelative;
else $errors[] = ["path" => $rawEntry, "error" => "Cannot create file"];
}
json_out([
"success" => count($errors) === 0,
"created_files" => array_values(array_unique($createdFiles)),
"created_folders" => array_values(array_unique($createdFolders)),
"errors" => $errors
]);
}
if ($action === "create_files") {
$names = $_POST['names'] ?? [];
if (!is_array($names)) $names = [$names];
$created = [];
$errors = [];
foreach ($names as $rawName) {
$relative = normalise_relative_path((string)$rawName);
if ($relative === '') {
$errors[] = ["name" => (string)$rawName, "error" => "Invalid file path"];
continue;
}
$fullRelative = normalise_relative_path(($path !== '' ? $path . '/' : '') . $relative);
$parts = explode('/', $fullRelative);
$leaf = array_pop($parts);
$parent = ensure_relative_directory($root, implode('/', $parts));
$safeLeaf = safe_name_only($leaf);
if (!$parent || $safeLeaf === '') {
$errors[] = ["name" => (string)$rawName, "error" => "Invalid path"];
continue;
}
$file = $parent . '/' . $safeLeaf;
if (file_exists($file)) {
$errors[] = ["name" => (string)$rawName, "error" => "File already exists"];
continue;
}
if (file_put_contents($file, '') !== false) $created[] = $relative;
else $errors[] = ["name" => (string)$rawName, "error" => "Cannot create file"];
}
json_out([
"success" => count($errors) === 0,
"created" => $created,
"errors" => $errors
]);
}
if ($action === "move") {
$destination = normalise_relative_path($_POST['destination'] ?? '');
$items = $_POST['items'] ?? [];
if (!is_array($items)) $items = [$items];
$destDir = safe_existing_dir($root, $destination);
if (!$destDir) json_out(["success" => false, "error" => "Destination folder no longer exists"]);
$moved = [];
$errors = [];
foreach ($items as $rawItem) {
$relative = normalise_relative_path((string)$rawItem);
if ($relative === '') continue;
$source = realpath($root . '/' . $relative);
if (!$source || !path_inside_root($source, $root) || $source === $root) {
$errors[] = ["path" => $relative, "error" => "Source no longer exists"];
continue;
}
$name = basename($source);
$dest = rtrim($destDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name;
if (file_exists($dest)) {
$errors[] = ["path" => $relative, "error" => "Destination already contains " . $name];
continue;
}
if (!@rename($source, $dest)) {
$errors[] = ["path" => $relative, "error" => "Move failed"];
continue;
}
// Move sidecars together when the selected source is a normal file.
if (is_file($dest)) {
$oldAbsolute = $root . '/' . $relative;
foreach (glob($oldAbsolute . '.*') ?: [] as $sidecar) {
if (!is_file($sidecar)) continue;
$suffix = substr($sidecar, strlen($oldAbsolute));
@rename($sidecar, $dest . $suffix);
}
}
$newRelative = normalise_relative_path(($destination !== '' ? $destination . '/' : '') . $name);
$moved[] = ["old" => $relative, "new" => $newRelative];
}
json_out([
"success" => count($errors) === 0,
"moved" => $moved,
"errors" => $errors
]);
}
/* ═══════════════════════════════════════════════════════
WORKSET / QUICK-OPEN API
These actions intentionally avoid returning the full tree.
═══════════════════════════════════════════════════════ */
function txtwrk_human_bytes(int $bytes): string {
if ($bytes < 1024) return $bytes . " B";
$units = ["KB", "MB", "GB", "TB"];
$v = $bytes / 1024;
foreach ($units as $unit) {
if ($v < 1024 || $unit === "TB") {
$rounded = $v >= 10 ? round($v) : round($v, 1);
return $rounded . " " . $unit;
}
$v /= 1024;
}
return $bytes . " B";
}
if ($action === "resolve_paths") {
$paths = $_POST["paths"] ?? [];
if (!is_array($paths)) $paths = [$paths];
$valid = [];
$missing = [];
foreach ($paths as $raw) {
$relative = normalise_relative_path((string)$raw);
if ($relative === "") continue;
$base = safe_existing_file($root, $relative);
if (!$base) {
$missing[] = $relative;
continue;
}
$valid[] = [
"path" => $relative,
"size" => (int)(@filesize($base) ?: 0),
"mtime" => (int)(@filemtime($base) ?: 0),
"latest_edit" => latest_edit_version($base)
];
}
json_out([
"success" => true,
"valid" => $valid,
"missing" => array_values(array_unique($missing))
]);
}
if ($action === "batch_status") {
$paths = $_POST["paths"] ?? [];
if (!is_array($paths)) $paths = [$paths];
$items = [];
foreach (array_slice($paths, 0, 200) as $raw) {
$relative = normalise_relative_path((string)$raw);
if ($relative === "") continue;
$base = safe_existing_file($root, $relative);
$items[] = [
"path" => $relative,
"exists" => $base !== null,
"size" => $base ? (int)(@filesize($base) ?: 0) : 0,
"mtime" => $base ? (int)(@filemtime($base) ?: 0) : 0,
"latest_edit" => $base ? latest_edit_version($base) : 0
];
}
json_out(["success" => true, "items" => $items]);
}
if ($action === "search_paths") {
$query = trim((string)($_GET["q"] ?? ""));
$limit = (int)($_GET["limit"] ?? 40);
if ($limit < 1) $limit = 1;
if ($limit > 100) $limit = 100;
if ($query === "") {
json_out(["success" => true, "results" => []]);
}
$queryLower = strtolower(str_replace("\\", "/", $query));
$results = [];
try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY,
RecursiveIteratorIterator::CATCH_GET_CHILD
);
foreach ($iterator as $info) {
if (count($results) >= $limit) break;
if (!$info->isFile()) continue;
$absolute = $info->getPathname();
if (!path_inside_root($absolute, $root)) continue;
if (is_edit_version_file($absolute) || is_request_file($absolute) || is_numeric_lock_file($absolute)) continue;
$relative = substr($absolute, strlen(rtrim($root, DIRECTORY_SEPARATOR)) + 1);
$relative = str_replace(DIRECTORY_SEPARATOR, "/", $relative);
$haystack = strtolower($relative);
if (strpos($haystack, $queryLower) === false) continue;
$size = (int)($info->getSize() ?: 0);
$results[] = [
"type" => "file",
"path" => $relative,
"name" => basename($relative),
"size" => $size,
"size_human" => txtwrk_human_bytes($size),
"mtime" => (int)$info->getMTime()
];
}
} catch (UnexpectedValueException $e) {
// Return readable results collected so far.
}
usort($results, function(array $a, array $b) use ($queryLower): int {
$an = strtolower($a["name"]);
$bn = strtolower($b["name"]);
$aExact = ($an === $queryLower) ? 0 : 1;
$bExact = ($bn === $queryLower) ? 0 : 1;
if ($aExact !== $bExact) return $aExact <=> $bExact;
$aStart = (strpos($an, $queryLower) === 0) ? 0 : 1;
$bStart = (strpos($bn, $queryLower) === 0) ? 0 : 1;
if ($aStart !== $bStart) return $aStart <=> $bStart;
$aDepth = substr_count($a["path"], "/");
$bDepth = substr_count($b["path"], "/");
if ($aDepth !== $bDepth) return $aDepth <=> $bDepth;
return strcasecmp($a["path"], $b["path"]);
});
json_out([
"success" => true,
"query" => $query,
"results" => array_slice($results, 0, $limit)
]);
}
/* ═══════════════════════════════════════════════════════
SETTINGS
═══════════════════════════════════════════════════════ */
if ($action === "settings") {
json_out([
"success" => true,
"settings" => [
"request_poll_interval" => 15000,
"autosave_delay" => 7000
]
]);
}
/* ═══════════════════════════════════════════════════════
LIST DIRECTORY
═══════════════════════════════════════════════════════ */
if ($action === "list") {
$folders = [];
$files = [];
$items = @scandir($target);
if ($items === false) {
json_out([
"success" => false,
"error" => "Cannot read directory"
]);
}
foreach ($items as $item) {
if ($item === "." || $item === "..") continue;
$full = $target . "/" . $item;
if (is_dir($full)) {
$folders[] = $item;
} else {
$files[] = $item;
}
}
sort($folders);
sort($files);
json_out([
"success" => true,
"path" => $path,
"folders" => $folders,
"files" => $files
]);
}
/* ═══════════════════════════════════════════════════════
CREATE FOLDER
═══════════════════════════════════════════════════════ */
if ($action === "mkdir") {
$name = $_POST['name'] ?? "";
$new = safe_child_path($target, $name, $root);
if (!$new) {
json_out(["success" => false, "error" => "Invalid folder name"]);
}
if (file_exists($new)) {
json_out(["success" => false, "error" => "Folder already exists"]);
}
$ok = @mkdir($new, 0755, true);
json_out($ok
? ["success" => true, "name" => basename($new)]
: ["success" => false, "error" => "Cannot create folder. Check permissions."]
);
}
/* ═══════════════════════════════════════════════════════
CREATE FILE
═══════════════════════════════════════════════════════ */
if ($action === "create_file") {
$name = $_POST['name'] ?? "";
$file = safe_child_path($target, $name, $root);
if (!$file) {
json_out(["success" => false, "error" => "Invalid file name"]);
}
if (file_exists($file)) {
json_out(["success" => false, "error" => "File already exists"]);
}
$ok = file_put_contents($file, "") !== false;
json_out($ok
? ["success" => true, "name" => basename($file)]
: ["success" => false, "error" => "Cannot create file. Check permissions."]
);
}
/* ═══════════════════════════════════════════════════════
RENAME FILE / FOLDER
═══════════════════════════════════════════════════════ */
if ($action === "rename") {
$oldName = $_POST['old_name'] ?? "";
$newName = $_POST['new_name'] ?? "";
$oldSafe = safe_name_only($oldName);
$newSafe = safe_name_only($newName);
if ($oldSafe === "" || $newSafe === "") {
json_out(["success" => false, "error" => "Invalid rename value"]);
}
if ($oldSafe === $newSafe) {
json_out(["success" => true, "name" => $newSafe, "message" => "No change"]);
}
$oldPath = $target . "/" . $oldSafe;
$oldReal = realpath($oldPath);
if (!$oldReal || !path_inside_root($oldReal, $root)) {
json_out(["success" => false, "error" => "Original item not found"]);
}
$newPath = $target . "/" . $newSafe;
if (file_exists($newPath)) {
json_out(["success" => false, "error" => "An item with that name already exists"]);
}
/*
If renaming a real source file, also rename its lock/edit/request sidecars.
Example:
index.php
index.php.12
index.php.txtwrk.1
index.php.requests.12
*/
$sidecars = [];
if (is_file($oldReal)) {
$matches = glob($oldReal . ".*");
if ($matches) {
foreach ($matches as $m) {
if (is_file($m)) {
$sidecars[] = $m;
}
}
}
}
$ok = @rename($oldReal, $newPath);
if (!$ok) {
json_out(["success" => false, "error" => "Rename failed. Check permissions."]);
}
foreach ($sidecars as $sidecar) {
$suffix = substr($sidecar, strlen($oldReal));
$newSidecar = $newPath . $suffix;
@rename($sidecar, $newSidecar);
}
json_out([
"success" => true,
"old_name" => $oldSafe,
"new_name" => $newSafe
]);
}
/* ═══════════════════════════════════════════════════════
DELETE FILE / FOLDER
═══════════════════════════════════════════════════════ */
if ($action === "delete") {
$name = $_POST['name'] ?? "";
$safeName = safe_name_only($name);
if ($safeName === "") {
json_out(["success" => false, "error" => "Missing target"]);
}
$full = $target . "/" . $safeName;
$real = realpath($full);
if (!$real || !path_inside_root($real, $root) || $real === $root) {
json_out(["success" => false, "error" => "Item not found or invalid"]);
}
/*
If deleting a real file, also clear its TXTWRK sidecar files.
If deleting a folder, recursive delete removes everything inside.
*/
if (is_file($real)) {
$sidecars = glob($real . ".*");
$ok = @unlink($real);
if ($ok && $sidecars) {
foreach ($sidecars as $sidecar) {
if (is_file($sidecar) && path_inside_root(realpath($sidecar) ?: "", $root)) {
@unlink($sidecar);
}
}
}
json_out(["success" => $ok]);
}
if (is_dir($real)) {
$ok = delete_recursive($real, $root);
json_out(["success" => $ok]);
}
json_out(["success" => false, "error" => "Unknown item type"]);
}
/* ═══════════════════════════════════════════════════════
FILE UPLOAD
═══════════════════════════════════════════════════════ */
if ($action === "upload") {
$incoming = [];
if (isset($_FILES['files']) && is_array($_FILES['files']['name'] ?? null)) {
$count = count($_FILES['files']['name']);
for ($i = 0; $i < $count; $i++) {
$incoming[] = [
"name" => $_FILES['files']['name'][$i] ?? "",
"tmp_name" => $_FILES['files']['tmp_name'][$i] ?? "",
"error" => $_FILES['files']['error'][$i] ?? UPLOAD_ERR_NO_FILE
];
}
} elseif (isset($_FILES['file'])) {
$incoming[] = $_FILES['file'];
}
if (!$incoming) json_out(["success" => false, "error" => "No file uploaded"]);
$uploaded = [];
$errors = [];
foreach ($incoming as $entry) {
$name = safe_name_only($entry['name'] ?? "");
if ($name === "" || (int)($entry['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
$errors[] = ["name" => $entry['name'] ?? "", "error" => "Invalid upload"];
continue;
}
$dest = $target . "/" . $name;
if (!path_inside_root(dirname($dest), $root)) {
$errors[] = ["name" => $name, "error" => "Invalid upload target"];
continue;
}
if (@move_uploaded_file($entry['tmp_name'], $dest)) $uploaded[] = $name;
else $errors[] = ["name" => $name, "error" => "Upload failed"];
}
json_out([
"success" => count($errors) === 0,
"uploaded" => $uploaded,
"errors" => $errors
]);
}
/* ═══════════════════════════════════════════════════════
REQUEST FILE ACCESS
═══════════════════════════════════════════════════════ */
if ($action === "request_file") {
$file = $_POST['file'] ?? "";
$memberID = (int)($_POST['memberID'] ?? 0);
if (!$file || !$memberID) {
json_out(["success" => false, "error" => "Missing params"]);
}
$base = safe_existing_file($root, $file);
if (!$base) {
json_out(["success" => false, "error" => "Invalid file"]);
}
$currentOwner = get_lock_owner($base);
if (!$currentOwner) {
json_out(["success" => false, "error" => "No current owner"]);
}
if ($currentOwner === $memberID) {
json_out(["success" => false, "error" => "You already own this file"]);
}
$requestFile = $base . ".requests." . $currentOwner;
$list = [];
if (file_exists($requestFile)) {
$json = json_decode(file_get_contents($requestFile) ?: "", true);
if (is_array($json)) $list = $json;
}
foreach ($list as $r) {
if (
isset($r["user"], $r["status"]) &&
(int)$r["user"] === $memberID &&
$r["status"] === "pending"
) {
json_out(["success" => false, "error" => "Already requested"]);
}
}
$list[] = [
"user" => $memberID,
"status" => "pending",
"time" => time()
];
file_put_contents($requestFile, json_encode($list, JSON_PRETTY_PRINT), LOCK_EX);
$data = read_controller_data($root);
$found = false;
foreach ($data["requests"] as &$r) {
if (
isset($r["user"], $r["file"]) &&
(int)$r["user"] === $memberID &&
$r["file"] === normalise_relative_path($file)
) {
$r["count"] = (int)($r["count"] ?? 0) + 1;
$found = true;
break;
}
}
unset($r);
if (!$found) {
$data["requests"][] = [
"user" => $memberID,
"file" => normalise_relative_path($file),
"count" => 1
];
}
write_controller_data($root, $data);
json_out(["success" => true]);
}
/* ═══════════════════════════════════════════════════════
POLL REQUESTS
═══════════════════════════════════════════════════════ */
if ($action === "poll_requests") {
$data = read_controller_data($root);
json_out([
"success" => true,
"data" => $data
]);
}
/* ═══════════════════════════════════════════════════════
GRANT FILE ACCESS
═══════════════════════════════════════════════════════ */
if ($action === "grant_request") {
$file = $_POST['file'] ?? "";
$memberID = (int)($_POST['memberID'] ?? 0);
if (!$file || !$memberID) {
json_out(["success" => false, "error" => "Missing params"]);
}
$file = normalise_relative_path($file);
$base = safe_existing_file($root, $file);
if (!$base) {
json_out(["success" => false, "error" => "Invalid file"]);
}
$currentOwner = get_lock_owner($base);
$currentLockFile = find_lock_file($base);
if (!$currentOwner || !$currentLockFile) {
json_out(["success" => false, "error" => "No current owner"]);
}
$requestFile = $base . ".requests." . $currentOwner;
$queue = [];
if (file_exists($requestFile)) {
$json = json_decode(file_get_contents($requestFile) ?: "", true);
if (is_array($json)) $queue = $json;
}
$queue = array_values(array_filter($queue, function ($r) use ($memberID) {
return !isset($r["user"]) || (int)$r["user"] !== $memberID;
}));
file_put_contents($requestFile, json_encode($queue, JSON_PRETTY_PRINT), LOCK_EX);
$newLock = member_lock_file($base, $memberID);
if (!@rename($currentLockFile, $newLock)) {
json_out(["success" => false, "error" => "Failed transferring ownership"]);
}
$data = read_controller_data($root);
foreach ($data["requests"] as $k => $r) {
if (isset($r["file"]) && $r["file"] === $file) {
unset($data["requests"][$k]);
}
}
$data["requests"] = array_values($data["requests"]);
write_controller_data($root, $data);
json_out([
"success" => true,
"new_owner" => $memberID
]);
}
/* ═══════════════════════════════════════════════════════
ENSURE EDIT
═══════════════════════════════════════════════════════ */
if ($action === "ensure_edit") {
$file = $_POST['file'] ?? "";
$memberID = (int)($_POST['memberID'] ?? 0);
if (!$file || !$memberID) {
json_out(["success" => false, "error" => "Missing parameters"]);
}
$base = safe_existing_file($root, $file);
if (!$base) {
json_out(["success" => false, "error" => "Invalid file"]);
}
$latest = ensure_first_edit_version($base);
$owner = get_lock_owner($base);
$myLock = member_lock_file($base, $memberID);
$readonly = false;
if ($owner && $owner !== $memberID) {
$readonly = true;
} else {
if (!file_exists($myLock)) {
file_put_contents($myLock, "", LOCK_EX);
}
$owner = $memberID;
}
$versionFile = edit_version_file($base, $latest);
$content = file_exists($versionFile) ? file_get_contents($versionFile) : file_get_contents($base);
if ($content === false) $content = "";
json_out([
"success" => true,
"readonly" => $readonly,
"owner" => $owner,
"version" => $latest,
"latestVersion" => $latest,
"content" => $content
]);
}
/* ═══════════════════════════════════════════════════════
LOAD LATEST EDIT
═══════════════════════════════════════════════════════ */
if ($action === "load_latest_edit") {
$file = $_GET['file'] ?? "";
$base = safe_existing_file($root, $file);
if (!$base) {
json_out(["success" => false, "error" => "Invalid file"]);
}
$latest = ensure_first_edit_version($base);
$versionFile = edit_version_file($base, $latest);
$content = file_exists($versionFile) ? file_get_contents($versionFile) : "";
if ($content === false) $content = "";
json_out([
"success" => true,
"version" => $latest,
"latestVersion" => $latest,
"content" => $content
]);
}
/* ═══════════════════════════════════════════════════════
LOAD EDIT VERSION
═══════════════════════════════════════════════════════ */
if ($action === "load_edit_version") {
$file = $_GET['file'] ?? "";
$version = (int)($_GET['version'] ?? 0);
if (!$file || $version <= 0) {
json_out(["success" => false, "error" => "Missing file or version"]);
}
$base = safe_existing_file($root, $file);
if (!$base) {
json_out(["success" => false, "error" => "Invalid file"]);
}
$latest = ensure_first_edit_version($base);
$versionFile = edit_version_file($base, $version);
if (!file_exists($versionFile)) {
json_out(["success" => false, "error" => "Version does not exist"]);
}
$content = file_get_contents($versionFile);
if ($content === false) $content = "";
json_out([
"success" => true,
"version" => $version,
"latestVersion" => $latest,
"content" => $content
]);
}
/* ═══════════════════════════════════════════════════════
SAVE EDIT VERSION
═══════════════════════════════════════════════════════ */
if ($action === "save_edit_version") {
$file = $_POST['file'] ?? ($_GET['file'] ?? "");
$memberID = (int)($_POST['memberID'] ?? ($_GET['memberID'] ?? 0));
$content = get_posted_content();
if (!$file || !$memberID) {
json_out(["success" => false, "error" => "Missing parameters"]);
}
$base = safe_existing_file($root, $file);
if (!$base) {
json_out(["success" => false, "error" => "Invalid file"]);
}
if (!user_owns_lock($base, $memberID)) {
json_out([
"success" => false,
"error" => "You do not own the edit lock",
"expected_lock" => member_lock_file($base, $memberID),
"existing_owner" => get_lock_owner($base),
"latest_version" => latest_edit_version($base)
]);
}
$latest = ensure_first_edit_version($base);
$lastFile = edit_version_file($base, $latest);
$lastContent = file_exists($lastFile) ? file_get_contents($lastFile) : null;
if ($lastContent === $content) {
json_out([
"success" => true,
"version" => $latest,
"latestVersion" => $latest,
"message" => "No change detected"
]);
}
$newVersion = $latest + 1;
$newFile = edit_version_file($base, $newVersion);
$fp = fopen($newFile, "wb");
if (!$fp) {
json_out(["success" => false, "error" => "Unable to create edit version"]);
}
flock($fp, LOCK_EX);
fwrite($fp, $content);
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
json_out([
"success" => true,
"version" => $newVersion,
"latestVersion" => $newVersion
]);
}
/* ═══════════════════════════════════════════════════════
SAVE BASE FILE
═══════════════════════════════════════════════════════ */
if ($action === "save_base") {
$file = $_POST['file'] ?? ($_GET['file'] ?? "");
$memberID = (int)($_POST['memberID'] ?? ($_GET['memberID'] ?? 0));
$content = get_posted_content();
if (!$file || !$memberID) {
json_out(["success" => false, "error" => "Missing parameters"]);
}
$base = safe_existing_file($root, $file);
if (!$base) {
json_out(["success" => false, "error" => "Invalid file"]);
}
if (!user_owns_lock($base, $memberID)) {
json_out([
"success" => false,
"error" => "You do not own the edit lock",
"expected_lock" => member_lock_file($base, $memberID),
"existing_owner" => get_lock_owner($base),
"latest_version" => latest_edit_version($base)
]);
}
$fp = fopen($base, "wb");
if (!$fp) {
json_out(["success" => false, "error" => "Unable to open base file"]);
}
flock($fp, LOCK_EX);
fwrite($fp, $content);
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
json_out(["success" => true]);
}
/* ═══════════════════════════════════════════════════════
QUICK EDIT PRIMITIVES
═══════════════════════════════════════════════════════ */
if ($action === "list_edit_versions") {
$file = normalise_relative_path($_GET['file'] ?? '');
$base = safe_existing_file($root, $file);
if (!$base) json_out(["success" => false, "error" => "Invalid file"]);
$latest = ensure_first_edit_version($base);
$versions = [];
foreach (glob($base . ".txtwrk.*") ?: [] as $vf) {
if (!is_file($vf)) continue;
if (!preg_match('/\.txtwrk\.(\d+)$/', $vf, $m)) continue;
$versions[] = [
"version" => (int)$m[1],
"size" => (int)(@filesize($vf) ?: 0),
"mtime" => (int)(@filemtime($vf) ?: 0)
];
}
usort($versions, function(array $a, array $b): int {
return $b["version"] <=> $a["version"];
});
json_out([
"success" => true,
"file" => $file,
"latestVersion" => $latest,
"versions" => $versions
]);
}
if ($action === "release_file_lock") {
$file = normalise_relative_path($_POST['file'] ?? ($_GET['file'] ?? ''));
$base = safe_existing_file($root, $file);
if (!$base) json_out(["success" => false, "error" => "Invalid file"]);
$lock = find_lock_file($base);
if (!$lock) json_out(["success" => true, "released" => false, "message" => "No lock exists"]);
$owner = get_lock_owner($base);
$ok = @unlink($lock);
json_out($ok
? ["success" => true, "released" => true, "owner" => $owner]
: ["success" => false, "error" => "Could not remove lock"]
);
}
/* ═══════════════════════════════════════════════════════
CHECK FILE OWNER
═══════════════════════════════════════════════════════ */
if ($action === "check_file_owner") {
$file = $_GET['file'] ?? "";
$base = safe_existing_file($root, $file);
if (!$base) {
json_out(["success" => false, "error" => "Invalid file"]);
}
json_out([
"success" => true,
"owner" => get_lock_owner($base)
]);
}
/* ═══════════════════════════════════════════════════════
CLOSE SESSION
═══════════════════════════════════════════════════════ */
if ($action === "close_session") {
$memberID = (int)($_POST['memberID'] ?? 0);
if (!$memberID) {
json_out(["success" => false, "error" => "Missing memberID"]);
}
$deleted = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $fileInfo) {
if (!$fileInfo->isFile()) continue;
$pathName = $fileInfo->getPathname();
$fileName = $fileInfo->getFilename();
if (is_edit_version_file($fileName)) continue;
if (is_request_file($fileName)) continue;
if (preg_match('/\.' . preg_quote((string)$memberID, '/') . '$/', $fileName)) {
if (@unlink($pathName)) {
$deleted++;
}
}
}
json_out([
"success" => true,
"deleted" => $deleted,
"message" => "Removed {$deleted} lock file(s)"
]);
}
/* ═══════════════════════════════════════════════════════
CHAT COUNT — INCREMENT
═══════════════════════════════════════════════════════ */
if ($action === "increment_chat_count") {
$data = read_controller_data($root);
$data["chat_count"] = (int)($data["chat_count"] ?? 0) + 1;
write_controller_data($root, $data);
json_out([
"success" => true,
"count" => $data["chat_count"]
]);
}
/* ═══════════════════════════════════════════════════════
CHAT COUNT — GET
═══════════════════════════════════════════════════════ */
if ($action === "get_chat_count") {
$data = read_controller_data($root);
json_out([
"success" => true,
"count" => (int)($data["chat_count"] ?? 0)
]);
}
/* ═══════════════════════════════════════════════════════
DOWNLOAD MANIFEST — ALL TXTWRK EDITS + REQUESTED OPEN FILES
═══════════════════════════════════════════════════════ */
if ($action === "list_download_manifest") {
@set_time_limit(0);
@ini_set("max_execution_time", "0");
$manifest = [];
$totalBytes = 0;
$addManifestFile = static function (string $absolute, string $relative) use (&$manifest, &$totalBytes, $root): void {
if (!is_file($absolute) || !is_readable($absolute) || !path_inside_root($absolute, $root)) return;
$relative = normalise_relative_path($relative);
if ($relative === '' || isset($manifest[$relative])) return;
$size = @filesize($absolute);
if ($size === false) $size = 0;
$mtime = @filemtime($absolute);
if ($mtime === false) $mtime = 0;
$manifest[$relative] = [
"path" => $relative,
"size" => (int)$size,
"mtime" => (int)$mtime
];
$totalBytes += (int)$size;
};
try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY,
RecursiveIteratorIterator::CATCH_GET_CHILD
);
foreach ($iterator as $info) {
if (!$info->isFile() || !$info->isReadable()) continue;
if (!preg_match('/\.txtwrk\.\d+$/', $info->getFilename())) continue;
$absolute = $info->getPathname();
$relative = substr($absolute, strlen(rtrim($root, DIRECTORY_SEPARATOR)) + 1);
$relative = str_replace(DIRECTORY_SEPARATOR, '/', $relative);
$addManifestFile($absolute, $relative);
}
} catch (UnexpectedValueException $e) {
json_out(["success" => false, "error" => "Cannot scan server root: " . $e->getMessage()]);
}
$requested = $_POST['files'] ?? [];
if (!is_array($requested)) $requested = [$requested];
foreach ($requested as $relative) {
$relative = normalise_relative_path((string)$relative);
if ($relative === '') continue;
$absolute = safe_existing_file($root, $relative);
if ($absolute !== null) $addManifestFile($absolute, $relative);
}
$files = array_values($manifest);
usort($files, static function (array $a, array $b): int {
return strnatcasecmp($a["path"], $b["path"]);
});
json_out([
"success" => true,
"count" => count($files),
"total_bytes" => $totalBytes,
"files" => $files
]);
}
/* ═══════════════════════════════════════════════════════
DOWNLOAD SINGLE FILE
═══════════════════════════════════════════════════════ */
if ($action === "download") {
$file = $_GET['file'] ?? "";
$base = safe_existing_file($root, $file);
if (!$base) {
http_response_code(404);
exit;
}
header_remove("Content-Type");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"" . basename($base) . "\"");
header("Content-Length: " . filesize($base));
readfile($base);
exit;
}
/* ═══════════════════════════════════════════════════════
CLEAR EDITS
═══════════════════════════════════════════════════════ */
if ($action === "clear_edits") {
$deleted = 0;
if (!is_dir($root) || !is_readable($root)) {
json_out(["success" => false, "error" => "Root not accessible"]);
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $fileInfo) {
if (!$fileInfo->isFile()) continue;
$fileName = $fileInfo->getFilename();
if (preg_match('/\.txtwrk\.\d+$/', $fileName)) {
if (@unlink($fileInfo->getPathname())) {
$deleted++;
}
}
}
json_out([
"success" => true,
"deleted" => $deleted,
"message" => "Cleared {$deleted} .txtwrk files"
]);
}
/* ═══════════════════════════════════════════════════════
DOWNLOAD ZIP — LARGE, DIRECT, RECURSIVE
═══════════════════════════════════════════════════════ */
if ($action === "download_zip") {
@set_time_limit(0);
@ini_set("max_execution_time", "0");
@ini_set("memory_limit", "256M");
ignore_user_abort(true);
if (!class_exists("ZipArchive")) {
json_out(["success" => false, "error" => "ZipArchive is not enabled on this server"]);
}
$scopeAll = ($_POST["scope"] ?? "") === "all";
$requested = $_POST["items"] ?? ($_POST["files"] ?? []);
$includeEdits = (($_POST["include_edits"] ?? "0") === "1");
if (!$scopeAll && (!is_array($requested) || empty($requested))) {
json_out(["success" => false, "error" => "No files"]);
}
$tmpBase = tempnam(sys_get_temp_dir(), "txtwrk_");
if ($tmpBase === false) {
json_out(["success" => false, "error" => "Could not create temporary ZIP file"]);
}
$tmpZipPath = $tmpBase . ".zip";
@unlink($tmpBase);
$zip = new ZipArchive();
$opened = $zip->open($tmpZipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
if ($opened !== true) {
json_out(["success" => false, "error" => "Cannot create ZIP", "code" => $opened]);
}
$added = 0;
$addPath = static function (string $absolute, string $relative) use ($zip, &$added): void {
$relative = ltrim(str_replace("\\", "/", $relative), "/");
if ($relative === "" || !is_file($absolute) || !is_readable($absolute)) return;
if ($zip->addFile($absolute, $relative)) $added++;
};
if ($scopeAll) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY,
RecursiveIteratorIterator::CATCH_GET_CHILD
);
foreach ($iterator as $info) {
if (!$info->isFile() || !$info->isReadable()) continue;
$absolute = $info->getPathname();
if (!path_inside_root($absolute, $root)) continue;
$relative = substr($absolute, strlen(rtrim($root, DIRECTORY_SEPARATOR)) + 1);
$addPath($absolute, $relative);
}
} else {
$seenAbsolute = [];
foreach ($requested as $f) {
$relative = normalise_relative_path((string)$f);
if ($relative === '') continue;
$absolute = realpath($root . '/' . $relative);
if (!$absolute || !path_inside_root($absolute, $root)) continue;
if (is_dir($absolute)) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($absolute, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY,
RecursiveIteratorIterator::CATCH_GET_CHILD
);
foreach ($iterator as $info) {
if (!$info->isFile() || !$info->isReadable()) continue;
$child = $info->getPathname();
if (!path_inside_root($child, $root)) continue;
$childRelative = substr($child, strlen(rtrim($root, DIRECTORY_SEPARATOR)) + 1);
$childRelative = str_replace(DIRECTORY_SEPARATOR, '/', $childRelative);
if (!$includeEdits && is_edit_version_file($child)) continue;
if (is_request_file($child) || is_numeric_lock_file($child)) continue;
if (!isset($seenAbsolute[$child])) {
$seenAbsolute[$child] = true;
$addPath($child, $childRelative);
}
}
continue;
}
if (!is_file($absolute)) continue;
if (!isset($seenAbsolute[$absolute])) {
$seenAbsolute[$absolute] = true;
$addPath($absolute, $relative);
}
if ($includeEdits) {
foreach (glob($absolute . ".txtwrk.*") ?: [] as $sidecar) {
if (!is_file($sidecar) || !path_inside_root($sidecar, $root)) continue;
$sideRelative = $relative . substr($sidecar, strlen($absolute));
if (!isset($seenAbsolute[$sidecar])) {
$seenAbsolute[$sidecar] = true;
$addPath($sidecar, $sideRelative);
}
}
}
}
}
if (!$zip->close()) {
@unlink($tmpZipPath);
json_out(["success" => false, "error" => "Failed finalising ZIP. Check free disk space and permissions."]);
}
clearstatcache(true, $tmpZipPath);
$zipSize = is_file($tmpZipPath) ? filesize($tmpZipPath) : false;
if ($added <= 0 || $zipSize === false || $zipSize <= 0) {
@unlink($tmpZipPath);
json_out(["success" => false, "error" => "No readable files were added"]);
}
while (ob_get_level() > 0) @ob_end_clean();
header_remove("Content-Type");
header("Content-Type: application/zip");
header("Content-Disposition: attachment; filename=\"txtwrk_data_" . gmdate("Ymd_His") . ".zip\"");
header("Content-Length: " . $zipSize);
header("Content-Transfer-Encoding: binary");
header("Accept-Ranges: none");
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("X-Content-Type-Options: nosniff");
$fp = fopen($tmpZipPath, "rb");
if ($fp === false) {
@unlink($tmpZipPath);
http_response_code(500);
exit;
}
// Stream in 8 MiB chunks; PHP memory stays nearly constant even for 5+ GB.
$chunkSize = 8 * 1024 * 1024;
while (!feof($fp)) {
$buffer = fread($fp, $chunkSize);
if ($buffer === false) break;
echo $buffer;
flush();
}
fclose($fp);
@unlink($tmpZipPath);
exit;
}
json_out([
"success" => false,
"error" => "Unknown action: " . $action
]);
Domains where you can create workspaces
These are your verified servers or servers where someone has granted you file access.
Coins Overview
Hey
Signed in as
You have 0 coins.
Withdraw Funds
Money: £0.00
Transaction History
Filter by Status:
All
Purchases
Withdrawals
Pending Withdrawals
Approved Withdrawals
Rejected Withdrawals
Load More
Donations Overview
Signed in as
Total Incoming Donations: 0 (Total: £0.00 )
Total Outgoing Donations: 0 (Total: £0.00 )
You have the API Kit installed, enabling advanced donation features!
Incoming Donations
Load More
Outgoing Donations
Load More
Investor Overview
Signed in as
Total Investments Developed: 0 (Total Raised: £0.00 )
Total Investments Purchased: 0 (Total Invested: £0.00 )
Investments Developed
Filter:
All
Search
Load More
Investments Purchased
Filter:
All
Search
Load More
Payee Details
These details will be used when the company/person you invested in needs to pay you (e.g. profit share, dividends, refunds).
People to Pay
Investors who have put money into your projects. Use this to process payments, profit shares, or refunds.
Download People to Pay (CSV)
Load People to Pay
Load More
Business Overview
Signed in as
Total Incoming: £0.00
Total Outgoing: £0.00
Business Opportunities
Create partnership, sponsorship or grant opportunities for business, community or education development.
View Opportunities Made
+ New Business Opportunity
My Opportunities
Click “View Opportunities Made” to load your opportunities.
Business Opportunities Profile Module
Choose whether your Business Opportunities module appears on your profile. This setting applies to the module as a whole.
Individual opportunities still use their own Visibility and Status settings.
Profile Display:
Do not feature on profile
Feature on my profile
Save Profile Display Setting
Business Applications
Apply for public open partnership, sponsorship or grant opportunities. Your applications made will appear below.
Business Applications Made
Your latest applications will appear here.
View More Applications
Questionnaires
Create questionnaire sets from your existing public post questions. These can be used for communities, enrolment, business feedback, event interest, community questions and customer insight.
View Questionnaire Settings
+ New Questionnaire
My Questionnaires
Click “View Questionnaire Settings” to load your questionnaires.
Questionnaire Profile Module
Choose whether your questionnaire module appears on your profile. This setting applies to all questionnaires.
Individual questionnaires can still be set as Draft, Published, or Ended.
Profile Display:
Do not feature on profile
Feature on my profile
Save Profile Display Setting
Search Organisations
Here you can add your organisation to a group to consolidate feedback.
Selected Organisations
Confirm Selection
My Organisations
Ad Credits Overview
Hey
Signed in as
You have 0 ad credits.
Buy Ad Credits
Credits: 0
Transaction History
Filter by Type:
All
Purchases
Load More
Create or Modify Advert
Make/Update an Advert
Subscription Overview
Hey
Signed in as
Your plan: Free
Expiry: -
Learn About Premium
Upgrade to Premium and get early access to:
Now Available...
Set Feed Landing
Get Subscribers & get paid 100% via PayPal*
Label people & access them in features
Supporting early helps us grow faster
...list in update
Coming soon in additional Tiers...
AI-powered image & video post creation
Next-gen checklist tools (open-source)
Organise your quick panel icons
Spaces: Custom dashboards for your work
Share spaces and summaries
More in dev!
Premium will be our main Tier alongside Platinum in development, where we will have Additional Tiers such as 1. Our AI tool and 2. Storage Tiers. Premium will eventually have 30 perks, platinum will have 100!
By making your own Tiers in Get Subscribers, you can set the price of the Tier, make it active/inactive, delete it if you have no existing subscribers with it and define it via our post creation box or update the PPV settings in Manage on a post.
Subscribe Now
1 Year Premium (£100)
3 Years Premium (£200, Save £100)
Confirm Selection
Upgrade to Premium
Connected to many TXTWRK features across our network, designed to bring people and communities closer to you.
View Examples
Select a Country to see hierarchy examples:
☰
Code Playground
HTML/CSS/JS
Python
C
C++
Java
Run ▶
Close ✖
Manage your pages, employees, workplace confirmations, volunteers, writers and checklisting links.
Workplaces
All
Confirmed
Pending
View More
Writers & Journalists
All
My Applications
Writer Requests
+ Apply to Page
View More
⛶
Call of Duty ;
Clips 22:52 09/05/2025 Views: 212
Video Plays: 9
Acknowledgements: 1
▶ Video
REPORT
Submit Report
SAFETY
COMMENT
Submit Comment
FEATURE
Submit GIFT
ASK AI
Copilot
ChatGPT
GROK
Gemini*
AID by TXTWRK*
Ask Copilot
Submit
DOWNLOAD .TXT
Use Download .TXT to upload the post to an AI that can't read our posts fully yet. It all depends on which data plan you have enabled with these AI's
By signing into these AI services, you'll get more of an experience!
Ask ChatGPT
Submit
DOWNLOAD .TXT
Use Download .TXT to upload the post to an AI that can't read our posts fully yet. It all depends on which data plan you have enabled with these AI's
By signing into these AI services, you'll get more of an experience!
Ask GROK
Submit
DOWNLOAD .TXT
Use Download .TXT to upload the post to an AI that can't read our posts fully yet. It all depends on which data plan you have enabled with these AI's
By signing into these AI services, you'll get more of an experience!
Ask Gemini*
Submit
DOWNLOAD .TXT
Use Download .TXT to upload the post to an AI that can't read our posts fully yet. It all depends on which data plan you have enabled with these AI's
By signing into these AI services, you'll get more of an experience!
Ask AID by TXTWRK*
Submit
DOWNLOAD .TXT
Use Download .TXT to upload the post to an AI that can't read our posts fully yet. It all depends on which data plan you have enabled with these AI's
By signing into these AI services, you'll get more of an experience!
SHARE
COPY LINK
COPY EMBED CODE
COPY POST ID