2013-09-04 22:02:17 -04:00
/ *
Copyright ( C ) 2013 Aaron Lindsay < aaron @ aclindsay . com >
* /
2013-09-03 23:33:36 -04:00
package main
import (
"asink"
"asink/util"
"code.google.com/p/goconf/conf"
2013-09-04 23:09:28 -04:00
"errors"
2013-09-03 23:33:36 -04:00
"flag"
"fmt"
"io/ioutil"
"os"
"os/user"
"path"
"path/filepath"
)
type AsinkGlobals struct {
configFileName string
syncDir string
cacheDir string
tmpDir string
rpcSock string
db * AsinkDB
storage Storage
server string
port int
username string
password string
}
var globals AsinkGlobals
var flags * flag . FlagSet
func init ( ) {
asink . SetupCleanExitOnSignals ( )
}
func StartClient ( args [ ] string ) {
const config_usage = "Config File to use"
userHomeDir := "~"
u , err := user . Current ( )
if err == nil {
userHomeDir = u . HomeDir
}
flags := flag . NewFlagSet ( "start" , flag . ExitOnError )
flags . StringVar ( & globals . configFileName , "config" , path . Join ( userHomeDir , ".asink" , "config" ) , config_usage )
flags . StringVar ( & globals . configFileName , "c" , path . Join ( userHomeDir , ".asink" , "config" ) , config_usage + " (shorthand)" )
flags . Parse ( args )
//make sure config file's permissions are read-write only for the current user
if ! util . FileExistsAndHasPermissions ( globals . configFileName , 384 /*0b110000000*/ ) {
fmt . Println ( "Error: Either the file at " + globals . configFileName + " doesn't exist, or it doesn't have permissions such that the current user is the only one allowed to read and write." )
return
}
config , err := conf . ReadConfigFile ( globals . configFileName )
if err != nil {
fmt . Println ( err )
fmt . Println ( "Error reading config file at " , globals . configFileName , ". Does it exist?" )
return
}
globals . storage , err = GetStorage ( config )
if err != nil {
fmt . Println ( err )
return
}
globals . syncDir , err = config . GetString ( "local" , "syncdir" )
globals . cacheDir , err = config . GetString ( "local" , "cachedir" )
globals . tmpDir , err = config . GetString ( "local" , "tmpdir" )
globals . rpcSock , err = config . GetString ( "local" , "socket" ) //TODO make sure this exists
//make sure all the necessary directories exist
err = util . EnsureDirExists ( globals . syncDir )
if err != nil {
panic ( err )
}
err = util . EnsureDirExists ( globals . cacheDir )
if err != nil {
panic ( err )
}
err = util . EnsureDirExists ( globals . tmpDir )
if err != nil {
panic ( err )
}
//TODO check errors on server settings
globals . server , err = config . GetString ( "server" , "host" )
globals . port , err = config . GetInt ( "server" , "port" )
globals . username , err = config . GetString ( "server" , "username" )
globals . password , err = config . GetString ( "server" , "password" )
globals . db , err = GetAndInitDB ( config )
if err != nil {
panic ( err )
}
//spawn goroutine to handle locking file paths
go PathLocker ( globals . db )
//spawn goroutines to handle local events
2013-09-05 23:20:29 -04:00
go SendEvents ( globals )
2013-09-03 23:33:36 -04:00
localFileUpdates := make ( chan * asink . Event )
go StartWatching ( globals . syncDir , localFileUpdates )
//spawn goroutines to receive remote events
remoteFileUpdates := make ( chan * asink . Event )
go GetEvents ( globals , remoteFileUpdates )
go ProcessLocalEvents ( globals , localFileUpdates )
//TODO ensure remote updates wait until all local changes are saved off?
go ProcessRemoteEvents ( globals , remoteFileUpdates )
rpcTornDown := make ( chan int )
go StartRPC ( globals . rpcSock , rpcTornDown )
asink . WaitOnExit ( )
<- rpcTornDown
}
func ProcessLocalEvent ( globals AsinkGlobals , event * asink . Event ) {
2013-09-04 23:09:28 -04:00
StatStartLocalUpdate ( )
defer StatStopLocalUpdate ( )
2013-09-03 23:33:36 -04:00
//make the path relative before we save/send it anywhere
var err error
absolutePath := event . Path
event . Path , err = filepath . Rel ( globals . syncDir , event . Path )
if err != nil {
panic ( err )
}
latestLocal := LockPath ( event . Path , true )
defer UnlockPath ( event )
if latestLocal != nil {
event . Predecessor = latestLocal . Hash
2013-09-08 22:51:36 -04:00
if event . Timestamp < latestLocal . Timestamp {
fmt . Printf ( "trying to send event older than latestLocal:\n" )
fmt . Printf ( "OLD %+v\n" , latestLocal )
fmt . Printf ( "NEW %+v\n" , event )
}
2013-09-03 23:33:36 -04:00
}
if event . IsUpdate ( ) {
//copy to tmp
//TODO upload in chunks and check modification times to make sure it hasn't been changed instead of copying the whole thing off
tmpfilename , err := util . CopyToTmp ( absolutePath , globals . tmpDir )
if err != nil {
//bail out if the file we are trying to upload already got deleted
if util . ErrorFileNotFound ( err ) {
event . LocalStatus |= asink . DISCARDED
return
}
panic ( err )
}
//try to collect the file's permissions
fileinfo , err := os . Stat ( absolutePath )
if err != nil {
//bail out if the file we are trying to upload already got deleted
if util . ErrorFileNotFound ( err ) {
event . LocalStatus |= asink . DISCARDED
return
}
panic ( err )
} else {
event . Permissions = fileinfo . Mode ( )
}
//get the file's hash
hash , err := HashFile ( tmpfilename )
if err != nil {
panic ( err )
}
event . Hash = hash
2013-09-08 23:30:13 -04:00
//If the hash is the same, don't try to upload the event again
2013-09-03 23:33:36 -04:00
if latestLocal != nil && event . Hash == latestLocal . Hash {
os . Remove ( tmpfilename )
2013-09-08 23:30:13 -04:00
//If neither the file contents nor permissions changed, squash this event completely
if event . Permissions == latestLocal . Permissions {
event . LocalStatus |= asink . DISCARDED
return
}
} else {
//rename to local cache w/ filename=hash
cachedFilename := path . Join ( globals . cacheDir , event . Hash )
err = os . Rename ( tmpfilename , cachedFilename )
2013-09-03 23:33:36 -04:00
if err != nil {
2013-09-08 23:30:13 -04:00
err := os . Remove ( tmpfilename )
if err != nil {
panic ( err )
}
2013-09-03 23:33:36 -04:00
panic ( err )
}
2013-09-08 23:30:13 -04:00
//upload file to remote storage
StatStartUpload ( )
err = globals . storage . Put ( cachedFilename , event . Hash )
StatStopUpload ( )
if err != nil {
panic ( err )
}
2013-09-03 23:33:36 -04:00
}
} else {
//if we're trying to delete a file that we thought was already deleted, there's no need to delete it again
if latestLocal != nil && latestLocal . IsDelete ( ) {
event . LocalStatus |= asink . DISCARDED
return
}
}
//finally, send it off to the server
2013-09-04 23:51:28 -04:00
StatStartSending ( )
2013-09-03 23:33:36 -04:00
err = SendEvent ( globals , event )
2013-09-04 23:51:28 -04:00
StatStopSending ( )
2013-09-03 23:33:36 -04:00
if err != nil {
panic ( err ) //TODO handle sensibly
}
}
func ProcessLocalEvents ( globals AsinkGlobals , eventChan chan * asink . Event ) {
for {
event := <- eventChan
go ProcessLocalEvent ( globals , event )
}
}
func ProcessRemoteEvent ( globals AsinkGlobals , event * asink . Event ) {
2013-09-04 23:09:28 -04:00
StatStartRemoteUpdate ( )
defer StatStopRemoteUpdate ( )
2013-09-03 23:33:36 -04:00
latestLocal := LockPath ( event . Path , true )
defer UnlockPath ( event )
//get the absolute path because we may need it later
absolutePath := path . Join ( globals . syncDir , event . Path )
//if we already have this event, or if it is older than our most recent event, bail out
if latestLocal != nil {
2013-09-08 22:13:04 -04:00
if event . Timestamp < latestLocal . Timestamp {
2013-09-03 23:33:36 -04:00
event . LocalStatus |= asink . DISCARDED
return
}
2013-09-08 22:13:04 -04:00
if event . IsSameEvent ( latestLocal ) {
return
}
2013-09-03 23:33:36 -04:00
if latestLocal . Hash != event . Predecessor && latestLocal . Hash != event . Hash {
fmt . Printf ( "conflict:\n" )
fmt . Printf ( "OLD %+v\n" , latestLocal )
fmt . Printf ( "NEW %+v\n" , event )
//TODO handle conflict?
}
}
//Download event
if event . IsUpdate ( ) {
if latestLocal == nil || event . Hash != latestLocal . Hash {
outfile , err := ioutil . TempFile ( globals . tmpDir , "asink" )
if err != nil {
panic ( err ) //TODO handle sensibly
}
tmpfilename := outfile . Name ( )
outfile . Close ( )
2013-09-04 23:09:28 -04:00
StatStartDownload ( )
2013-09-03 23:33:36 -04:00
err = globals . storage . Get ( tmpfilename , event . Hash )
2013-09-04 23:09:28 -04:00
StatStopDownload ( )
2013-09-03 23:33:36 -04:00
if err != nil {
panic ( err ) //TODO handle sensibly
}
//rename to local hashed filename
hashedFilename := path . Join ( globals . cacheDir , event . Hash )
err = os . Rename ( tmpfilename , hashedFilename )
if err != nil {
err := os . Remove ( tmpfilename )
if err != nil {
panic ( err )
}
panic ( err )
}
//copy hashed file to another tmp, then rename it to the actual file.
tmpfilename , err = util . CopyToTmp ( hashedFilename , globals . tmpDir )
if err != nil {
panic ( err )
}
//make sure containing directory exists
err = util . EnsureDirExists ( path . Dir ( absolutePath ) )
if err != nil {
panic ( err )
}
err = os . Rename ( tmpfilename , absolutePath )
if err != nil {
err2 := os . Remove ( tmpfilename )
if err2 != nil {
panic ( err2 )
}
panic ( err )
}
}
2013-09-08 23:30:13 -04:00
if latestLocal == nil || event . Hash != latestLocal . Hash || event . Permissions != latestLocal . Permissions {
2013-09-03 23:33:36 -04:00
err := os . Chmod ( absolutePath , event . Permissions )
if err != nil && ! util . ErrorFileNotFound ( err ) {
panic ( err )
}
}
} else {
//intentionally ignore errors in case this file has been deleted out from under us
os . Remove ( absolutePath )
//delete the directory previously containing this file if its the last file
util . RecursiveRemoveEmptyDirs ( path . Dir ( absolutePath ) )
}
//TODO make sure file being overwritten is either unchanged or already copied off and hashed
}
func ProcessRemoteEvents ( globals AsinkGlobals , eventChan chan * asink . Event ) {
for event := range eventChan {
2013-09-05 19:59:45 -04:00
go ProcessRemoteEvent ( globals , event )
2013-09-03 23:33:36 -04:00
}
}
2013-09-04 23:09:28 -04:00
func getSocketFromArgs ( args [ ] string ) ( string , error ) {
2013-09-03 23:33:36 -04:00
const config_usage = "Config File to use"
userHomeDir := "~"
u , err := user . Current ( )
if err == nil {
userHomeDir = u . HomeDir
}
flags := flag . NewFlagSet ( "stop" , flag . ExitOnError )
flags . StringVar ( & globals . configFileName , "config" , path . Join ( userHomeDir , ".asink" , "config" ) , config_usage )
flags . StringVar ( & globals . configFileName , "c" , path . Join ( userHomeDir , ".asink" , "config" ) , config_usage + " (shorthand)" )
flags . Parse ( args )
config , err := conf . ReadConfigFile ( globals . configFileName )
2013-09-04 23:09:28 -04:00
if err != nil {
return "" , err
}
rpcSock , err := config . GetString ( "local" , "socket" )
if err != nil {
return "" , errors . New ( "Error reading local.socket from config file at " + globals . configFileName )
}
return rpcSock , nil
}
func StopClient ( args [ ] string ) {
rpcSock , err := getSocketFromArgs ( args )
2013-09-03 23:33:36 -04:00
if err != nil {
fmt . Println ( err )
return
}
2013-09-04 23:09:28 -04:00
i := 99
returnCode := 0
err = asink . RPCCall ( rpcSock , "ClientAdmin.StopClient" , & i , & returnCode )
if err != nil {
panic ( err )
}
}
func GetStatus ( args [ ] string ) {
var status string
rpcSock , err := getSocketFromArgs ( args )
2013-09-03 23:33:36 -04:00
if err != nil {
2013-09-04 23:09:28 -04:00
fmt . Println ( err )
return
2013-09-03 23:33:36 -04:00
}
i := 99
2013-09-04 23:09:28 -04:00
err = asink . RPCCall ( rpcSock , "ClientAdmin.GetClientStatus" , & i , & status )
2013-09-03 23:33:36 -04:00
if err != nil {
panic ( err )
}
2013-09-04 23:09:28 -04:00
fmt . Println ( status )
2013-09-03 23:33:36 -04:00
}