Graal Forums

Graal Forums (https://forums.graalonline.com/forums/index.php)
-   Code Gallery (https://forums.graalonline.com/forums/forumdisplay.php?f=179)
-   -   gsync: one-way folder sync from a Graal server to a web server (https://forums.graalonline.com/forums/showthread.php?t=134265123)

cbk1994 11-27-2011 05:39 PM

gsync: one-way folder sync from a Graal server to a web server
 
gsync is a simple tool for syncing folders from a Graal server to a web server running PHP (although you could implement it in any serverside language, if you wanted to).

Usage Scenarios
For example, I can use gsync to sync all script folders (weapons, scripts, npcs) to my remote server. I can then easily backup scripts with a single command. You can also backup binary files (e.g. images), so you could really sync your entire server if you wanted to.

It's very easy to combine this with some kind of post-sync operation like committing to a Mercurial or Git repository. This makes it very easy to keep track of changes and keep backups.

This script is probably better than GBall or the FTP server I wrote since it allows you to completely automate backups without issues the others have (GBall is slow and uses lots of memory, FTP is a pain to use and also somewhat slow).

How it Works
When running a sync, it will connect to the configured web server, authenticate with a key and IP matching, and then send a list of all files in the sync folders and their mod times. The web server then checks for files that have been deleted and deletes them from its local copy, and determines which files have been updated/added since the last sync, and tells the Graal server that it needs these files. The Graal servers sends these files, and the sync is complete.

Setup
  1. Add func_http to your server.
  2. Decide on a key (ideally a long random string) and put this in the configuration section of both the PHP and the NPC scripts.
  3. Lookup your server's IP and put this in the PHP file.
  4. Upload the PHP file to some web server (tested on a linux server, but ought to work anywhere)
  5. Upload the database NPC to your server. Configure the stuff at the top.
  6. Make sure (npcserver) has read rights to anything you're planning on syncing.
  7. Call the sync function (suggestion: add it to your onRCChat commands).

If you don't want your files to be publicly accessible, you should be sure that your web server doesn't serve them (with the default configuration below, you can just prohibit access to the src folder).

Code
NPC GSync:
PHP Code:

enum LOG {
  
NONE,
  
ERROR,
  
INFO
};

function 
onInitialized() {
  
this.trigger("created");
}

function 
onCreated() {
  
this.join("func_http");
  
  
// options
  
this.server "http://gsync.graalcenter.org/gsync.php"// change this!
  
this.key "PUT_SOMETHING_RANDOM_HERE";
  
this.verbosity LOG.ERROR;
  
this.folders = {
    
"scripts",
    
"npcs",
    
"weapons"
  
};
}

public function 
sync() {
  
report(LOG.INFO"Starting sync");
  
  
// build list of files and their mod times
  
report(LOG.INFO"Building list of files and mod times");
  
temp.fileList "";
  
  for (
temp.folder this.folders) {
    
temp.files.loadFolder(temp.folder "/*"true);
    
    for (
temp.file files) {
      
temp.path temp.folder "/" temp.file;
      
temp.fileList @=
        
temp.path "\t" getFileModTime(temp.path) @ "\n";
    }
  }
  
  
report(LOG.INFO"Built list of files and mod times");
  
  
// send list of files & mod times to the server
  
report(LOG.INFO"Sending list of files and mod times to server");
  
  
temp.data = {
    {
"filelist"temp.fileList}
  };
  
  
temp.returnData this.send("filelist"temp.data);
  
  if (
temp.returnData == null) {
    return 
report(LOG.ERROR"Didn't receive acceptable response from server");
  }
  
  
temp.neededFiles temp.returnData.needed;
  
temp.returnData.destroy();
  
  
report(LOG.INFO"Server responded with needed files (" temp.neededFiles.size() @ ")");
  
  if (
temp.neededFiles.size() > 0) {
    
// send server all the files it wants
    
temp.data null;
    
temp.includedFiles "";
    
    for (
temp.neededFile temp.neededFiles) {
      
temp.contents.loadString(temp.neededFile);
      
temp.data.add({"file-" md5(temp.neededFile), temp.contents});
      
temp.data.add({"filem-" md5(temp.neededFile), getFileModTime(temp.neededFile)});
      
      
temp.includedFiles @= temp.neededFile "\n";
    }
    
    
temp.data.add({"files"temp.includedFiles});
    
    
report(LOG.INFO"Sending server requested files");
    
temp.returnData this.send("store"temp.data);
    
    if (
temp.returnData == null) {
      return 
report(LOG.ERROR"Didn't receive acceptable response from server");
    }
    
    
temp.returnData.destroy();
  }
  
  
report(LOG.INFO"Sync complete");
}

function 
send(temp.actiontemp.data) {
  
temp.data.add({"action"temp.action});
  
temp.data.add({"key"this.key});
  
  
temp.json = new TStaticVar("GSyncResponse" int(timevar2 100));
  
temp.response this.post(this.servertemp.data);
  
  if (
parseJSON(temp.jsontemp.response)) {
    
report(LOG.INFO"Parsed JSON successfully in server's response to " temp.action);
    
    if (
temp.json.status == "ok") {
      return 
temp.json;
    } else if (
temp.json.status == "error") {
      
report(LOG.ERROR"Received error from server for " temp.action ": " temp.json.error);
    } else {
      
report(LOG.ERROR"Received unexpected status code '" temp.json.status "' in response to " temp.action);
    }
  } else {
    
report(LOG.ERROR"Unable to parse JSON in server's response to " temp.action);
  }
  
  
temp.json.destroy();
}

function 
report(temp.logLeveltemp.msg) {
  if (
this.verbosity >= temp.logLevel) {
    echo(
"gsync: " temp.msg);
  }


gsync.php:
PHP Code:

<?php
$folder 
"src"// will be created if it doesn't exist
// make these match your Graal server
$ip "50.23.136.183"// find this on statistics.graal.us
$key "PUT_SOMETHING_RANDOM HERE";

// don't touch below here
// make sure request is coming from a good client
error_reporting(0);

if (
$_SERVER["REMOTE_ADDR"] != $ip || $_POST["key"] != $key) {
    
header("Status: 401 Unauthorized"401);
    
    
$data = array(
        
"error" => "Bad IP or key"
    
);
    
    
sendResponse("error"$data);
    die();
}

// client has successfully validated
// does the source folder exist?
if (! is_dir($folder)) {
    
mkdir($folder);
}

// check what command is being sent
switch ($_POST["action"]) {
    case 
"filelist":
        
$fileList $_POST["filelist"];
        
$fileList explode("\n"$fileList);
        
$pathList = array(); // this will be filled with all paths to check for deleted files later
        
$neededList = array(); // this will be filled with all paths which need to be sent (e.g. updated files)
        
        
foreach ($fileList as $file) {
            
$parts explode("\t"$file);
            
            if (
count($parts) != 2) {
                continue;
            }
            
            
$path $parts[0];
            
$modTime $parts[1];
            
            
$pathList[] = $path// add to path list
            
            // is the mod time from the server newer than the local copy?
            
$localModTime filemtime($folder "/" $path);
            
            if (
$modTime $localModTime) {
                
// the server has a newer version
                
$neededList[] = $path;
            }
        }
        
        
// do I need to delete any files?
        
$localPaths getAllFiles($folder, array());
        
        foreach (
$localPaths as $localPath) {
            if (! 
in_array($localPath$pathList)) { // file has been deleted on server
                
unlink($folder "/" $localPath); // delete file from local copy
            
}
        }
        
        
// tell the server which files, if any, I need
        
$data = array(
            
"needed" => $neededList
        
);
        
        
sendResponse("ok"$data);
    break;
    
    case 
"store":
        
$files explode("\n"$_POST["files"]);
        
        foreach (
$files as $file) {
            if (
strlen($file) <= 0) {
                continue;
            }
            
            
// try to create folders
            
$path substr($file0strrpos($file"/"));
            
mkdir($folder "/" $path0777true);
            
            
$contents $_POST["file-" md5($file)];
            
$modTime $_POST["filem-" md5($file)];
            
file_put_contents($folder "/" $file$contents);
            
touch($folder "/" $file$modTime);
        }
        
        
sendResponse("ok", array());
        
// you could easily add some post-sync script here, e.g. commit changes to a Mercurial repo
    
break;
    
    default:
        
$data = array(
            
"error" => "Unrecognized action"
        
);
        
        
sendResponse("error"$data);
    break;
}

function 
sendResponse($status$data) {
    
$data["status"] = $status;
    echo(
json_encode($data));
}

// recursively get all files in a directory
function getAllFiles($dir$files) {
    global 
$folder;
    
$dh opendir($dir);
    
    while ((
$file readdir($dh)) !== false) {
        if (
substr($file01) == ".") {
            continue; 
// starts with a dot (hidden or . or ..)
        
}
        
        
$fullPath $dir "/" $file;
        if (
is_dir($fullPath)) {
            
$files getAllFiles($fullPath$files);
        } else {
            
$files[] = substr($fullPathstrlen($folder) + 1);
        }
    }
    
    return 
$files;
}
?>

If there are any issues/questions/suggestions I'll be happy to address them.

WhiteDragon 11-27-2011 09:04 PM

Great job! (OT: Never knew we had a parseJSON function.)

Tolnaftate2004 11-27-2011 09:34 PM

Quote:

Originally Posted by WhiteDragon (Post 1675602)
Great job! (OT: Never knew we had a parseJSON function.)

Newly added as of the last release.

Crow 12-11-2011 07:46 PM

Apparently, parseJSON() doesn't return true (anymore?). Not sure if it's only me, but I also tried restarting the server/NPC server. I tried feeding it some sample JSON stuff, and it also worked, but the function returned 0. Which makes gsync fail. Every time.

Edit: And for some strange reason, even when attempting to fix this for me, it doesn't work. The TStaticVar that send() returns is always null around line 51, even though it is not inside send(). What the heck?

Matt 12-11-2011 08:14 PM

Great Job.

cbk1994 12-11-2011 08:17 PM

Quote:

Originally Posted by Crow (Post 1677340)
Apparently, parseJSON() doesn't return true (anymore?). Not sure if it's only me, but I also tried restarting the server/NPC server. I tried feeding it some sample JSON stuff, and it also worked, but the function returned 0. Which makes gsync fail. Every time.

Edit: And for some strange reason, even when attempting to fix this for me, it doesn't work. The TStaticVar that send() returns is always null around line 51, even though it is not inside send(). What the heck?

You're right, very strange. Stefan must have changed something since release since now I'm getting the same errors.

Here's a fixed version (all it's doing is avoiding returning the entire JSON object, which fixes the issue—it also doesn't rely on parseJSON returning the correct value):

PHP Code:

enum LOG {
  
NONE,
  
ERROR,
  
INFO
};

function 
onInitialized() {
  
this.trigger("created");
}

function 
onCreated() {
  
this.join("func_http");
  
  
// options
  
this.server "http://gsync.graalcenter.org/gsync.php";
  
this.key "CHANGE_THIS";
  
this.verbosity LOG.INFO;
  
this.folders = {
    
"scripts",
    
"npcs",
    
"weapons",
    
"data"
  
};
  
  
// temp
  
this.sync();
}

public function 
sync() {
  
report(LOG.INFO"Starting sync");
  
  
// build list of files and their mod times
  
report(LOG.INFO"Building list of files and mod times");
  
temp.fileList "";
  
  for (
temp.folder this.folders) {
    
temp.files.loadFolder(temp.folder "/*"true);
    
    for (
temp.file files) {
      
temp.path temp.folder "/" temp.file;
      
temp.fileList @=
        
temp.path "\t" getFileModTime(temp.path) @ "\n";
    }
  }
  
  
report(LOG.INFO"Built list of files and mod times");
  
  
// send list of files & mod times to the server
  
report(LOG.INFO"Sending list of files and mod times to server");
  
  
temp.data = {
    {
"filelist"temp.fileList}
  };
  
  
temp.returnData this.send("filelist"temp.data"needed");
  
  if (
temp.returnData == null) {
    return 
report(LOG.ERROR"Didn't receive acceptable response from server");
  }
  
  
temp.neededFiles temp.returnData;
  
  
report(LOG.INFO"Server responded with needed files (" temp.neededFiles.size() @ ")");
  
  if (
temp.neededFiles.size() > 0) {
    
// send server all the files it wants
    
temp.data null;
    
temp.includedFiles "";
    
    for (
temp.neededFile temp.neededFiles) {
      
temp.contents.loadString(temp.neededFile);
      
temp.data.add({"file-" md5(temp.neededFile), temp.contents});
      
temp.data.add({"filem-" md5(temp.neededFile), getFileModTime(temp.neededFile)});
      
      
temp.includedFiles @= temp.neededFile "\n";
    }
    
    
temp.data.add({"files"temp.includedFiles});
    
    
report(LOG.INFO"Sending server requested files");
    
temp.returnData this.send("store"temp.data"status");
    
    if (
temp.returnData == null) {
      return 
report(LOG.ERROR"Didn't receive acceptable response from server");
    }
  }
  
  
report(LOG.INFO"Sync complete");
}

function 
send(temp.actiontemp.datatemp.needed) {
  
temp.data.add({"action"temp.action});
  
temp.data.add({"key"this.key});
  
  
temp.json = new TStaticVar("GSyncResponse" int(timevar2 100));
  
temp.response this.post(this.servertemp.data);
  
parseJSON(temp.jsontemp.response);
  
  if (
temp.json.status != null) {
    
report(LOG.INFO"Parsed JSON successfully in server's response to " temp.action);
    
    if (
temp.json.status == "ok") {
      
temp.response temp.json.(@ temp.needed);
      
temp.json.destroy();
      
      return 
temp.response;
    } else if (
temp.json.status == "error") {
      
report(LOG.ERROR"Received error from server for " temp.action ": " temp.json.error);
    } else {
      
report(LOG.ERROR"Received unexpected status code '" temp.json.status "' in response to " temp.action);
    }
  } else {
    
report(LOG.ERROR"Unable to parse JSON in server's response to " temp.action);
  }
  
  
temp.json.destroy();
}

function 
report(temp.logLeveltemp.msg) {
  if (
this.verbosity >= temp.logLevel) {
    echo(
"gsync: " temp.msg);
  }


I don't understand what could be causing this, either, though. Looks like it's probably one of GScript's weird glitches.

Crow 12-11-2011 08:33 PM

Thanks. You still got some temp stuff in your onCreated() though :)

Loriel 12-12-2011 06:24 PM

Wouldn't doing it the other way around be much more useful so you could develop against a git repository that pushes commits to the server?

cbk1994 12-13-2011 12:38 AM

Quote:

Originally Posted by Loriel (Post 1677480)
Wouldn't doing it the other way around be much more useful so you could develop against a git repository that pushes commits to the server?

Not possible, the script folders can't be edited via script. I agree, though. What I'd really like to see is some version control implemented by Stefan. Branching would make debug servers a lot more useful.

Loriel 12-13-2011 03:04 AM

Quote:

Originally Posted by cbk1994 (Post 1677536)
Not possible, the script folders can't be edited via script. I agree, though. What I'd really like to see is some version control implemented by Stefan. Branching would make debug servers a lot more useful.

Can't you funnel it through whatever the scripted RC uses? Having some dude be online for that doesn't seem like the worst limitation, but I'm probably missing something.

cbk1994 12-13-2011 12:26 PM

Quote:

Originally Posted by Loriel (Post 1677568)
Can't you funnel it through whatever the scripted RC uses? Having some dude be online for that doesn't seem like the worst limitation, but I'm probably missing something.

I don't think so. The scripted RC is using special sendtext/receivetext stuff that's limited to privileged (login) scripts. It would have to be implemented on Login and carry over to all the servers.

Crow 12-13-2011 06:00 PM

It really depends how much more secure the scripted RC was being made. I've seen some write code that would change their rights to full access. All that was needed was script access for them (a level NPC would've been enough) and somebody with full rights online.

cbk1994 12-14-2011 05:36 AM

Quote:

Originally Posted by Crow (Post 1677649)
It really depends how much more secure the scripted RC was being made. I've seen some write code that would change their rights to full access. All that was needed was script access for them (a level NPC would've been enough) and somebody with full rights online.

This is no longer possible, although I did that in the past before as well. Scripted RC access is limited to privileged scripts (although it's still possible to send text via sendToRC clientside—I've used this to have globals disconnect people at random, etc.)

cbk1994 06-25-2012 07:49 PM

I've updated gsync to function with the new restrictions on loops (it's not possible to evade the max loop limit via sleep anymore; now we must use a timeout). You can find the new version here. If you're having issues, it might be useful to set the logging level to CRAZY_VERBOSE and see where the problems are.

This should be fairly stable; it's been running every 10 minutes for about 3 months now on Era (scripts only).

fowlplay4 06-26-2013 02:10 PM

This could probably be optimized to use onAllRCChat now.

PHP Code:

function onAllRCChat(msg) {
  
temp.msgs = {
    {
"The script of NPC "" has been updated by""npcs"},
    {
"The npc "" has been deleted by account""npcs"},
    {
"Weapon/GUI-script "" added/updated by account""weapons"},
    {
"Weapon "" deleted by account""weapons"},
    {
"Script "" updated by account""scripts"},
    {
"Script "" deleted by account""scripts"}
  };
  
temp.stype false;
  for (
temp.mtemp.msgs) {
    
temp.mp msg.pos(temp.m[1]);
    if (
msg.starts(temp.m[0]) && temp.mp 0) {
      
temp.ml temp.m[0].length();
      
temp.sname msg.substring(temp.mltemp.mp temp.ml);
      
temp.stype temp.m[2];
      break;
    }
  }
  if (
temp.stype) {
    
temp.stype// script folder
    
temp.sname// script name
    // Queue the files for update, then perhaps push them a minute from now or something.
  
}



cbk1994 06-26-2013 05:07 PM

Quote:

Originally Posted by fowlplay4 (Post 1719808)
This could probably be optimized to use onAllRCChat now.

I was actually working on something similar recently, with the idea being to capture the account of the person making the change as well, so that could be passed to gsync and used to commit the change to source control under their name. Also works for level uploads (which are essentially text, after all). It's a lot messier than yours, but this is what I came up with:

PHP Code:

function onAllRCChat(temp.msg) {  
  if (
temp.msg.starts("Script ") && temp.msg.pos(" updated by ") > (- 1)) {
    
temp.tokens temp.msg.tokenize();
    
temp.acc temp.tokens[temp.tokens.size() - 1];
    
this.handleUpdate(temp.acc, {{"script"temp.tokens[1]}});
  } else if (
temp.msg.starts("Script ") && temp.msg.pos(" deleted by ") > (- 1)) {
    
temp.tokens temp.msg.tokenize();
    
temp.acc temp.tokens[temp.tokens.size() - 1];
    
this.handleUpdate(temp.acc, {{"script"temp.tokens[1]}});
  } else if (
temp.msg.starts("The script of NPC ") && temp.msg.pos(" has been updated by ") > (- 1)) {
    
temp.tokens temp.msg.tokenize();
    
temp.acc temp.tokens[temp.tokens.size() - 1];
    
this.handleUpdate(temp.acc, {{"npc"temp.tokens[4]}});
  } else if (
temp.msg.starts("NPC ") && temp.msg.pos(" has been added by ") > (- 1)) {
    
temp.tokens temp.msg.tokenize();
    
temp.acc temp.tokens[temp.tokens.size() - 1];
    
this.handleUpdate(temp.acc, {{"npc"temp.tokens[1]}});
  } else if (
temp.msg.starts("The npc ") && temp.msg.pos(" has been deleted by ") > (- 1)) {
    
temp.tokens temp.msg.tokenize();
    
temp.acc temp.tokens[temp.tokens.size() - 1];
    
this.handleUpdate(temp.acc, {{"npc"temp.tokens[2]}});
  } else if (
temp.msg.starts("The flags of NPC ") && temp.msg.pos(" have been updated by ") > (- 1)) {
    
temp.tokens temp.msg.tokenize();
    
temp.acc temp.tokens[temp.tokens.size() - 1];
    
this.handleUpdate(temp.acc, {{"npc"temp.tokens[4]}});
  } else if (
temp.msg.starts("Weapon/GUI-script ") && temp.msg.pos(" added/updated by ") > (- 1)) {
    
temp.tokens temp.msg.tokenize();
    
temp.acc temp.tokens[temp.tokens.size() - 1];
    
this.handleUpdate(temp.acc, {{"weapon"temp.tokens[1]}});
  } else if (
temp.msg.starts("Weapon ") && temp.msg.pos(" deleted by ") > (- 1)) {
    
temp.tokens temp.msg.tokenize();
    
temp.acc temp.tokens[temp.tokens.size() - 1];
    
this.handleUpdate(temp.acc, {{"weapon"temp.tokens[1]}});
  }
}

// figure out who uploaded it by checking rclog.txt
function onLevelFileUpdated(temp.file) {
  
temp.file "levels/" temp.file;
  
  if (! 
temp.file.ends(".nw") && ! temp.file.ends(".gmap")) {
    return;
  }
  
  
temp.log.loadLines("logs/rclog.txt");
  
temp.lastLog temp.log[temp.log.size() - 1];
  
  
temp.acc "(npcserver)";
  
  if (
temp.lastLog.pos(temp.file) > (- 1)) {
    
temp.tokens temp.lastLog.tokenize();
    
temp.acc temp.tokens[0];
  }
  
  
this.handleUpdate(temp.acc, {{"file"temp.file}});
}

// public so this can also be hooked into online level/NPC editors
public function handleUpdate(temp.acctemp.files) {
  echo(
temp.acc ": " temp.files);


If I get some free time I might see about finishing this. It would be nice if gsync could work on a single changed file, rather than having to check every file every time. Also, you could store the account of the player, which is nifty to stick in source control.

JohnnyChimpo 12-27-2013 01:29 AM

Ive been trying to get this hooked up to a home server i have but i cant figure out why its giving me a CURL not authorized error. The key is correct the server IP is correct, and i have my apache2 server linking to the folder that the php file is in(all permissions are correct). I just cant figure this out, any help would be much appreciated.

fowlplay4 12-27-2013 02:16 AM

It's probably blocked on the Graal server end.

JohnnyChimpo 12-27-2013 02:41 AM

Must be because i was certain i did everything correct. What is the standard for backing up graal server files now that this script is broken? Or is there a way i can ask Stefan to enable this for my server?

BlueMelon 12-27-2013 02:54 AM

Quote:

Originally Posted by JohnnyChimpo (Post 1724546)
Must be because i was certain i did everything correct. What is the standard for backing up graal server files now that this script is broken? Or is there a way i can ask Stefan to enable this for my server?

It's been a while now that the community has asked for a standard backup... unfortunately nothing has ever been made.

fowlplay4 12-27-2013 03:09 AM

Quote:

Originally Posted by JohnnyChimpo (Post 1724546)
Must be because i was certain i did everything correct. What is the standard for backing up graal server files now that this script is broken? Or is there a way i can ask Stefan to enable this for my server?

Give yourself:

PHP Code:

rw scripts/*
rw npcs/*
rw weapons/*
-r npcs/npclocalnpc* 

Download files manually then zip them or maintain your own git repo.

JohnnyChimpo 12-27-2013 03:23 AM

Okay, i guess that will have to do, thanks for the knowledge =D.

Joshua_P2P 12-27-2013 08:59 AM

So basically the short way is that it won't work anymore? i originally used this for pm logs and now everytime someone pms it sends me this error in rc
The CURL connection to http://aaronjy.zxq.net/graal/pmlist.php is not authorized on this server

cbk1994 12-27-2013 04:36 PM

My understanding is that Stefan has to explicitly whitelist outgoing connections now.

Emera 12-27-2013 04:39 PM

Quote:

Originally Posted by cbk1994 (Post 1724569)
My understanding is that Stefan has to explicitly whitelist outgoing connections now.

Yep, it's been that way for a while. Sad really since there's even more restriction on what we're capable of doing. :/

JohnnyChimpo 12-27-2013 04:40 PM

The sad part is that people like me pay to be restricted from development like this. lol


All times are GMT +2. The time now is 11:48 PM.

Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2025, vBulletin Solutions Inc.
Copyright (C) 1998-2019 Toonslab All Rights Reserved.