Files

208 lines
7.9 KiB
Go

package synccommon
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/global"
)
// ANSI color constants used only in this file
const (
AnsiDelete = "\033[38;5;1m"
)
// FetchRespT defines the structure of responses from `libmuttonserver fetch`.
type FetchRespT struct {
ErrMsg *string `json:"errMsg"` // nil if no error occurred
ServerTime int64 `json:"serverTime"`
Deletions []Deletion `json:"deletions"`
Entries EntryMapT `json:"entries"`
}
type Deletion struct {
VanityPath string `json:"vanityPath"`
IsAgeFile bool `json:"isAgeFile"`
}
type EntryMapT map[string]EntryT // map vanity paths to containing folders and mod+age timestamps
type EntryT struct {
ContainingFolder string `json:"containingFolder"`
ModTime int64 `json:"modTime"`
AgeTimestamp *int64 `json:"ageTimestamp"` // nil if no age file is present (non-password entry)
}
// RegisterRespT defines the structure of responses from `libmuttonserver register`
type RegisterRespT struct {
ErrMsg *string `json:"errMsg"` // nil if no error occurred
EntryRoot string `json:"entryRoot"`
AgeDir string `json:"ageDir"`
IsWindows bool `json:"isWindows"`
}
type RegisterReqT struct {
NewDeviceID string `json:"newDeviceID"`
OldDeviceID *string `json:"oldDeviceID"` // nil if not replacing an existing device ID
}
// GetAllEntryData returns a map of all vanity paths to
// their respective containing folders and mod+age timestamps.
func GetAllEntryData() (EntryMapT, error) {
var err error
entryList, _, err := WalkEntryDir()
if err != nil {
return nil, errors.New("unable to walk entry directory: " + err.Error())
}
// initialize vanityPath keys in map
outputEntries := make(EntryMapT)
var modInfo, ageInfo os.FileInfo
for _, vanityPath := range entryList {
containingFolder := vanityPath[:strings.LastIndex(vanityPath, "/")]
modInfo, err = os.Stat(global.GetRealPath(vanityPath))
if err != nil {
return nil, errors.New("unable to read mod time for " + vanityPath + ": " + err.Error())
}
ageInfo, err = os.Stat(global.GetRealAgePath(vanityPath))
var ageTimestamp *int64
if err == nil {
ageTimestamp = new(ageInfo.ModTime().Unix())
} else if !os.IsNotExist(err) {
return nil, errors.New("unable to read age time for " + vanityPath + ": " + err.Error())
}
outputEntries[vanityPath] = EntryT{ContainingFolder: containingFolder, ModTime: modInfo.ModTime().Unix(), AgeTimestamp: ageTimestamp}
}
return outputEntries, nil
}
// ShearLocal removes the target file or directory from the local system.
// Returns: deviceID (only on client; for use in ShearRemoteFromClient),
// isDir (only on client; for use in ShearRemoteFromClient).
// If the local system is a server, it will also add the target to the deletions list for all clients (except the requesting client).
// This function should only be used directly by the server binary.
func ShearLocal(vanityPath, clientDeviceID string, onlyShearAgeFile bool) (string, bool, error) {
// determine if running on a server
var onServer bool
if clientDeviceID != "" {
onServer = true
}
deviceIDList, err := global.GenDeviceIDList()
if err != nil {
return "", false, errors.New("unable to generate device ID list: " + err.Error())
}
// add the sheared vanityPath to the deletions list (if running on a server)
if onServer {
for i := range deviceIDList {
if deviceIDList[i].Name() != clientDeviceID {
if !onlyShearAgeFile {
f, err := os.OpenFile(global.CfgDir+global.PathSeparator+"deletions"+global.PathSeparator+deviceIDList[i].Name()+global.FSSpace+"entry"+global.FSSpace+strings.ReplaceAll(vanityPath, "/", global.FSPath), os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
// failure to add the target to the deletions list will exit the program and result in a client re-uploading the target (non-critical)
return "", false, err
}
_ = f.Close() // error ignored; if the file could be created, it can probably be closed
}
f, err := os.OpenFile(global.CfgDir+global.PathSeparator+"deletions"+global.PathSeparator+deviceIDList[i].Name()+global.FSSpace+"age"+global.FSSpace+strings.ReplaceAll(vanityPath, "/", global.FSPath), os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
// failure to add the target to the deletions list will exit the program and result in a client re-uploading the target (non-critical)
return "", false, err
}
_ = f.Close() // error ignored; if the file could be created, it can probably be closed
}
}
}
// remove the target locally
realPath := global.GetRealPath(vanityPath)
var isFile bool
if !onServer { // error if target does not exist on client, needed because os.RemoveAll does not return an error if target does not exist
isAccessible, err := back.TargetIsFile(realPath, true)
if !isAccessible {
return "", false, err
}
if err == nil { // fails if target is a directory, so no error indicates a file
isFile = true
}
}
if !onlyShearAgeFile {
if err = os.RemoveAll(realPath); err != nil {
return "", false, errors.New("unable to remove local entry (" + vanityPath + "): " + err.Error())
}
}
if err = ShearAgeFileLocal(vanityPath); err != nil {
return "", false, err
}
if !onServer && len(deviceIDList) > 0 { // return the device ID if running on the client and a device ID exists (online mode)
return (deviceIDList)[0].Name(), !isFile, nil
}
return "", true, nil
// do not exit program, as this function is used as part of ShearRemoteFromClient
}
// ShearAgeFileLocal removes the age file for a vanity path.
// This function should only be used directly by the server binary.
func ShearAgeFileLocal(vanityPath string) error {
if err := os.RemoveAll(global.AgeDir + global.PathSeparator + strings.ReplaceAll(vanityPath, "/", global.FSPath)); err != nil {
return errors.New("unable to remove age file for " + vanityPath + ": " + err.Error())
}
return nil
}
// RenameLocal renames oldLocationIncomplete to newLocationIncomplete on the local system.
// This function should only be used directly by the server binary.
func RenameLocal(oldVanityPath, newVanityPath string) error {
// get full paths for both locations
oldRealPath := global.GetRealPath(oldVanityPath)
oldRealAgePath := global.GetRealAgePath(oldVanityPath)
newRealPath := global.GetRealPath(newVanityPath)
newRealAgePath := global.GetRealAgePath(newVanityPath)
// ensure newLocation does not exist
isAccessible, _ := back.TargetIsFile(newRealPath, true) // error is ignored because dir/file status is irrelevant
if isAccessible {
return errors.New("new target (" + newRealPath + ") already exists")
}
// rename oldLocation to newLocation
if err := os.Rename(oldRealPath, newRealPath); err != nil {
return errors.New("unable to rename: " + err.Error())
}
// do the same for the age file (if one exists) - also back up timestamp first
fileInfo, err := os.Stat(oldRealAgePath)
if err == nil { // assume age file does not exist if os.Stat errors
if err = os.Rename(oldRealAgePath, newRealAgePath); err != nil {
return errors.New("unable to rename: " + err.Error())
}
if err = os.Chtimes(newRealAgePath, time.Now(), fileInfo.ModTime()); err != nil {
return errors.New("unable to set timestamp on age file for " + newVanityPath + ": " + err.Error())
}
}
return nil
// do not exit program, as this function is used as part of RenameRemoteFromClient
}
// AddFolderLocal creates a new entry-containing directory on the local system.
// This function should only be used directly by the server binary.
func AddFolderLocal(vanityPath string) error {
// create the target locally
realPath := global.GetRealPath(vanityPath)
if err := os.Mkdir(realPath, 0700); err != nil {
if os.IsExist(err) {
fmt.Println(back.AnsiBlue + "Directory already exists - libmutton will still ensure it exists on the server")
} else {
return errors.New("unable to create directory: " + err.Error())
}
}
return nil
// do not exit program, as this function is used as part of AddFolderRemoteFromClient
}