Created
March 5, 2026 07:29
-
-
Save vladborovtsov/78635153fa92ba5f1f14b95ee21e5d19 to your computer and use it in GitHub Desktop.
Simple PHP based clipboard/sharing tool for LAN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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