DEV Community

Cover image for One-pager powered by json content files and synced to git
Tim Geyssens
Tim Geyssens

Posted on

One-pager powered by json content files and synced to git

Recently I took Warp for a testdrive. I have a couple of friends who have a eco-glamping and where in need of a website. So perfect opportunity to see the current state of AI in webdev.

And colour me impressed. I was able to create a one pager powered by a tailored CMS. The frontend is plain html/css/js powered by json files that are managed by the php backend.

Now as an additional challenge I wanted to make sure the content was also source controlled. Every *.json change and updated file in /images (the path where user uploaded files are stored) should be stored in git.

It took me a couple of prompts to end up with a php scripts that uses the github content api to check the status and if necessary update the content files.

Running this as a scheduled task on a Plesk interface from a cheap hosting solution.

`

<?php
/**
* GitHub Sync Script for DenBerendries - Using Contents API
*
* This script uses the GitHub Contents API which handles encoding automatically
* and syncs both JSON files in root and images in /images directory
*/

// Configuration
$GITHUB_TOKEN = '???';
$GITHUB_USERNAME = 'timgeyssens';
$GITHUB_REPO = 'DenBerendries';
$BRANCH = 'main';
$COMMIT_MESSAGE = 'Auto commit from server on ' . date('Y-m-d H:i:s');

// Set content type to JSON
header('Content-Type: application/json');

// Check if script is called via HTTP or CLI
$isHttp = (php_sapi_name() !== 'cli');

// Function to make GitHub API requests
function githubApiRequest($url, $token, $method = 'GET', $data = null) {
    $ch = curl_init();
    $headers = [
        'Authorization: token ' . $token,
        'User-Agent: PHP-GitHub-Sync-Script',
        'Content-Type: application/json',
    ];

    curl_setopt($ch, CURLOPT_URL, 'https://api.github.com' . $url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HEADER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

    if ($method === 'POST') {
        curl_setopt($ch, CURLOPT_POST, true);
        if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    } elseif ($method === 'PUT') {
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
        if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    } elseif ($method === 'PATCH') {
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
        if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    }

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return ['code' => $httpCode, 'body' => json_decode($response, true)];
}

// Function to find JSON files in root and images in /images directory
function findFilesToSync() {
    $files = [];
    $basePath = realpath('.');

    // Supported image extensions
    $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'];

    // 1. Find JSON files in root directory
    $rootIterator = new DirectoryIterator($basePath);
    foreach ($rootIterator as $file) {
        if ($file->isFile() && !$file->isDot()) {
            $extension = strtolower(pathinfo($file->getFilename(), PATHINFO_EXTENSION));

            // Check if it's a JSON file
            if ($extension === 'json') {
                // Get relative path from the base directory
                $relativePath = substr($file->getRealPath(), strlen($basePath) + 1);

                // Normalize path separators for GitHub
                $relativePath = str_replace('\\', '/', $relativePath);

                $files[] = $relativePath;
            }
        }
    }

    // 2. Find image files in /images directory
    $imagesPath = $basePath . DIRECTORY_SEPARATOR . 'images';
    if (is_dir($imagesPath)) {
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($imagesPath, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $extension = strtolower(pathinfo($file->getFilename(), PATHINFO_EXTENSION));

                // Check if it's an image file
                if (in_array($extension, $imageExtensions)) {
                    // Get relative path from the base directory
                    $relativePath = substr($file->getRealPath(), strlen($basePath) + 1);

                    // Normalize path separators for GitHub
                    $relativePath = str_replace('\\', '/', $relativePath);

                    $files[] = $relativePath;
                }
            }
        }
    }

    return $files;
}

// Function to get file SHA from GitHub
function getFileShaFromGitHub($token, $username, $repo, $branch, $filePath) {
    $url = "/repos/$username/$repo/contents/$filePath?ref=$branch";
    $response = githubApiRequest($url, $token);

    if ($response['code'] === 200) {
        return $response['body']['sha'];
    }

    return null;
}

// Function to sync files using Contents API
function syncToGitHub($token, $username, $repo, $branch, $commitMessage) {
    $result = [];

    // Find all files to sync
    $allFiles = findFilesToSync();
    $result['all_files'] = count($allFiles);

    if (count($allFiles) === 0) {
        return ['success' => true, 'message' => 'No files found to sync'];
    }

    $changedFiles = [];
    $skippedFiles = [];

    foreach ($allFiles as $file) {
        if (file_exists($file)) {
            $localContent = file_get_contents($file);
            $localSha = sha1("blob " . strlen($localContent) . "\0" . $localContent);

            // Get the remote SHA
            $remoteSha = getFileShaFromGitHub($token, $username, $repo, $branch, $file);

            // If file doesn't exist in remote or SHA is different, it's changed
            if (!$remoteSha || $remoteSha !== $localSha) {
                $changedFiles[] = $file;
            } else {
                $skippedFiles[] = $file;
            }
        }
    }

    $result['changed_files'] = count($changedFiles);
    $result['skipped_files'] = count($skippedFiles);
    $result['files'] = $changedFiles;

    // If no files changed, return early without creating a commit
    if (count($changedFiles) === 0) {
        return ['success' => true, 'message' => 'No changes to commit', 'action' => 'none'];
    }

    // Update each changed file
    $successCount = 0;
    $errorCount = 0;
    $errors = [];

    foreach ($changedFiles as $file) {
        if (file_exists($file)) {
            $content = file_get_contents($file);
            $sha = getFileShaFromGitHub($token, $username, $repo, $branch, $file);

            $data = [
                'message' => $commitMessage,
                'content' => base64_encode($content),
                'branch' => $branch
            ];

            // If file exists, include the SHA for update
            if ($sha) {
                $data['sha'] = $sha;
            }

            $response = githubApiRequest("/repos/$username/$repo/contents/$file", $token, 'PUT', $data);

            if ($response['code'] === 200 || $response['code'] === 201) {
                $successCount++;
            } else {
                $errorCount++;
                $errors[] = "Failed to update $file: " . json_encode($response['body']);
            }
        }
    }

    if ($errorCount > 0) {
        $result['success'] = false;
        $result['message'] = "Completed with $successCount successes and $errorCount errors";
        $result['errors'] = $errors;
    } else {
        $result['success'] = true;
        $result['message'] = "Sync completed successfully. Updated $successCount files.";
    }

    $result['action'] = 'pushed';
    return $result;
}

// Handle the request
try {
    $startTime = microtime(true);

    // If called via HTTP with a custom message
    if ($isHttp && isset($_GET['message'])) {
        $COMMIT_MESSAGE = $_GET['message'];
    }

    $result = syncToGitHub($GITHUB_TOKEN, $GITHUB_USERNAME, $GITHUB_REPO, $BRANCH, $COMMIT_MESSAGE);
    $result['execution_time'] = round(microtime(true) - $startTime, 2) . ' seconds';

    if ($isHttp) {
        echo json_encode($result, JSON_PRETTY_PRINT);
    } else {
        // CLI output
        if ($result['success']) {
            if ($result['action'] === 'pushed') {
                echo "βœ… Sync completed successfully!\n";
                echo "πŸ“Š All files: " . $result['all_files'] . "\n";
                echo "πŸ“Š Changed files: " . $result['changed_files'] . "\n";
                echo "πŸ“Š Skipped files: " . $result['skipped_files'] . "\n";
                echo "⏱️  Execution time: " . $result['execution_time'] . "\n";

                // List files if there are not too many
                if ($result['changed_files'] > 0 && $result['changed_files'] <= 20) {
                    echo "\nπŸ“„ Files synced:\n";
                    foreach ($result['files'] as $file) {
                        echo "  - $file\n";
                    }
                }
            } else {
                echo "βœ… No changes to commit. Everything is up to date.\n";
                echo "πŸ“Š Files checked: " . $result['all_files'] . "\n";
                echo "⏱️  Execution time: " . $result['execution_time'] . "\n";
            }
        } else {
            echo "❌ Sync completed with errors:\n";
            echo "πŸ“Š Successes: " . ($result['changed_files'] - count($result['errors'])) . "\n";
            echo "πŸ“Š Errors: " . count($result['errors']) . "\n";
            echo "⏱️  Execution time: " . $result['execution_time'] . "\n";

            foreach ($result['errors'] as $error) {
                echo "❌ $error\n";
            }
        }
    }
} catch (Exception $e) {
    $error = ['success' => false, 'message' => 'Exception: ' . $e->getMessage()];

    if ($isHttp) {
        echo json_encode($error, JSON_PRETTY_PRINT);
    } else {
        echo "❌ Exception: " . $e->getMessage() . "\n";
    }
}

// If called via HTTP, add some HTML styling
if ($isHttp) {
    echo <<<HTML
    <!DOCTYPE html>
    <html>
    <head>
        <title>GitHub Sync Result</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                margin: 20px;
                background: #f5f5f5;
            }
            .container {
                max-width: 800px;
                margin: 0 auto;
                background: white;
                padding: 20px;
                border-radius: 5px;
                box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            }
            .success {
                color: #2ecc71;
                font-weight: bold;
            }
            .error {
                color: #e74c3c;
                font-weight: bold;
            }
            .info {
                color: #3498db;
                font-weight: bold;
            }
            pre {
                background: #f8f8f8;
                padding: 15px;
                border-radius: 5px;
                overflow: auto;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <h1>GitHub Sync Result</h1>
            <p>Repository: <a href="https://github.com/$GITHUB_USERNAME/$GITHUB_REPO" target="_blank">$GITHUB_USERNAME/$GITHUB_REPO</a></p>
            <p>Scanning: JSON files in root directory and images in /images directory</p>
            <p>Commit message: $COMMIT_MESSAGE</p>
        </div>
    </body>
    </html>
HTML;
}
Enter fullscreen mode Exit fullscreen mode

`

Top comments (0)