Skip to content

Instantly share code, notes, and snippets.

@vladborovtsov
Created March 5, 2026 07:29
Show Gist options
  • Select an option

  • Save vladborovtsov/78635153fa92ba5f1f14b95ee21e5d19 to your computer and use it in GitHub Desktop.

Select an option

Save vladborovtsov/78635153fa92ba5f1f14b95ee21e5d19 to your computer and use it in GitHub Desktop.
Simple PHP based clipboard/sharing tool for LAN
<?php
/**
* Single-file PHP Clipboard and File Sharing Tool
* Compatible with PHP 7.4+
*/
// Configuration
$dataFile = __DIR__ . '/clipboard_data.txt';
$uploadDir = __DIR__ . '/shared_files/';
// Ensure directories and files exist
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
if (!file_exists($dataFile)) {
file_put_contents($dataFile, "");
}
// Handle File Downloads
if (isset($_GET['download'])) {
$file = basename($_GET['download']); // Prevent directory traversal
$filepath = $uploadDir . $file;
if (file_exists($filepath) && is_file($filepath)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $file . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($filepath));
readfile($filepath);
exit;
}
die("File not found.");
}
// Handle API Requests (AJAX)
if (isset($_GET['api'])) {
header('Content-Type: application/json');
$action = $_GET['api'];
// GET: Fetch current text and file list
if ($action === 'get') {
$text = file_get_contents($dataFile);
$files = [];
$items = scandir($uploadDir);
foreach ($items as $item) {
if ($item !== '.' && $item !== '..' && is_file($uploadDir . $item)) {
$files[] = [
'name' => $item,
'size' => round(filesize($uploadDir . $item) / 1024, 2) . ' KB'
];
}
}
echo json_encode(['text' => $text, 'files' => $files]);
exit;
}
// POST: Save text
if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['text'])) {
file_put_contents($dataFile, $input['text']);
echo json_encode(['status' => 'success']);
}
exit;
}
// POST: Clear text
if ($action === 'clear' && $_SERVER['REQUEST_METHOD'] === 'POST') {
file_put_contents($dataFile, "");
echo json_encode(['status' => 'success']);
exit;
}
// POST: Upload file
if ($action === 'upload' && $_SERVER['REQUEST_METHOD'] === 'POST') {
if (!empty($_FILES['file']['name'])) {
$filename = basename($_FILES['file']['name']);
// Add timestamp to prevent overwriting files with the exact same name
if (file_exists($uploadDir . $filename)) {
$filename = time() . '_' . $filename;
}
move_uploaded_file($_FILES['file']['tmp_name'], $uploadDir . $filename);
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error', 'message' => 'No file sent']);
}
exit;
}
// POST: Delete file
if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['file'])) {
$file = basename($input['file']);
$filepath = $uploadDir . $file;
if (file_exists($filepath)) {
unlink($filepath);
}
echo json_encode(['status' => 'success']);
}
exit;
}
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VM Shared Clipboard</title>
<style>
:root {
--bg-color: #1e1e2e;
--panel-bg: #282a36;
--text-color: #f8f8f2;
--accent: #bd93f9;
--danger: #ff5555;
--success: #50fa7b;
--border: #44475a;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
box-sizing: border-box;
}
.container {
width: 100%;
max-width: 900px;
display: flex;
flex-direction: column;
gap: 20px;
height: 100%;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.status {
font-size: 0.9em;
color: var(--success);
opacity: 0;
transition: opacity 0.3s ease;
}
.status.show { opacity: 1; }
textarea {
width: 100%;
flex-grow: 1;
background-color: var(--panel-bg);
color: var(--text-color);
border: 1px solid var(--border);
border-radius: 8px;
padding: 15px;
font-family: monospace;
font-size: 16px;
resize: none;
box-sizing: border-box;
}
textarea:focus {
outline: none;
border-color: var(--accent);
}
.controls {
display: flex;
gap: 10px;
}
button {
background-color: var(--accent);
color: #282a36;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
transition: opacity 0.2s;
}
button:hover { opacity: 0.8; }
button.danger { background-color: var(--danger); color: white; }
.files-panel {
background-color: var(--panel-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 15px;
}
.file-upload-row {
display: flex;
gap: 10px;
margin-bottom: 15px;
align-items: center;
}
input[type="file"] {
flex-grow: 1;
}
.file-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 200px;
overflow-y: auto;
}
.file-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid var(--border);
}
.file-list li:last-child { border-bottom: none; }
.file-actions a, .file-actions span {
color: var(--accent);
text-decoration: none;
margin-left: 15px;
cursor: pointer;
}
.file-actions span.delete { color: var(--danger); }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>Shared Clipboard</h2>
<span id="sync-status" class="status">Saved</span>
</div>
<textarea id="clipboard" placeholder="Type or paste text here... Changes are saved and synced instantly."></textarea>
<div class="controls">
<button class="danger" onclick="clearClipboard()">Clear Clipboard</button>
<button onclick="copyToClipboard()">Copy All to Local Clipboard</button>
</div>
<div class="files-panel">
<h3>Shared Files</h3>
<div class="file-upload-row">
<input type="file" id="file-input">
<button onclick="uploadFile()">Upload File</button>
</div>
<ul class="file-list" id="file-list">
<!-- Files injected via JS -->
</ul>
</div>
</div>
<script>
const textarea = document.getElementById('clipboard');
const statusEl = document.getElementById('sync-status');
const fileListEl = document.getElementById('file-list');
let isTyping = false;
let typingTimer;
let currentServerText = "";
// Show temporary "Saved" indicator
function showStatus(text, isError = false) {
statusEl.textContent = text;
statusEl.style.color = isError ? 'var(--danger)' : 'var(--success)';
statusEl.classList.add('show');
setTimeout(() => statusEl.classList.remove('show'), 2000);
}
// --- TEXT SYNC LOGIC ---
// Send text to server
async function saveText() {
try {
await fetch('?api=save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: textarea.value })
});
currentServerText = textarea.value;
showStatus('Saved');
} catch (e) {
showStatus('Error saving', true);
}
}
// Listen for user typing or pasting
textarea.addEventListener('input', () => {
isTyping = true;
clearTimeout(typingTimer);
// Debounce saving slightly so we don't spam the server on every keystroke
typingTimer = setTimeout(() => {
saveText();
isTyping = false;
}, 500);
});
async function clearClipboard() {
textarea.value = '';
currentServerText = '';
try {
await fetch('?api=clear', { method: 'POST' });
showStatus('Cleared');
} catch (e) {
showStatus('Error clearing', true);
}
}
function copyToClipboard() {
textarea.select();
document.execCommand('copy');
showStatus('Copied to local clipboard');
}
// --- BACKGROUND CHECKER (POLLING) ---
async function fetchUpdates() {
try {
const response = await fetch('?api=get');
const data = await response.json();
// Only update textarea if the user is NOT actively typing AND the server text differs from local
if (!isTyping && data.text !== textarea.value) {
textarea.value = data.text;
currentServerText = data.text;
}
// Render file list
renderFiles(data.files);
} catch (e) {
console.error("Sync error:", e);
}
}
// --- FILE SHARING LOGIC ---
async function uploadFile() {
const fileInput = document.getElementById('file-input');
if (!fileInput.files.length) return alert('Please select a file first.');
const formData = new FormData();
formData.append('file', fileInput.files[0]);
statusEl.textContent = 'Uploading...';
statusEl.classList.add('show');
try {
const res = await fetch('?api=upload', {
method: 'POST',
body: formData
});
const result = await res.json();
if (result.status === 'success') {
fileInput.value = ''; // clear input
fetchUpdates(); // refresh list
showStatus('File uploaded');
}
} catch (e) {
showStatus('Upload failed', true);
}
}
async function deleteFile(filename) {
if (!confirm(`Delete ${filename}?`)) return;
try {
await fetch('?api=delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file: filename })
});
fetchUpdates();
showStatus('File deleted');
} catch (e) {
showStatus('Delete failed', true);
}
}
function renderFiles(files) {
fileListEl.innerHTML = '';
if (files.length === 0) {
fileListEl.innerHTML = '<li><span style="color: gray">No files shared yet.</span></li>';
return;
}
files.forEach(file => {
const li = document.createElement('li');
li.innerHTML = `
<span>${file.name} <small style="color:gray">(${file.size})</small></span>
<div class="file-actions">
<a href="?download=${encodeURIComponent(file.name)}">Download</a>
<span class="delete" onclick="deleteFile('${file.name.replace(/'/g, "\\'")}')">Delete</span>
</div>
`;
fileListEl.appendChild(li);
});
}
// Initial load, then poll every 2 seconds
fetchUpdates();
setInterval(fetchUpdates, 2000);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment