diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..e82a89b --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$USER_HOME$/AppData/Roaming/ForeverFiles/db/ForeverFiles.db + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..964d803 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..ef5f4f5 --- /dev/null +++ b/db/db.go @@ -0,0 +1,154 @@ +package db + +import ( + "database/sql" + "fmt" + "os" + "path" + + "forever-files/types" + + "github.com/kirsle/configdir" + _ "github.com/mattn/go-sqlite3" +) + +type DB interface { + Close() error + Migrate() error + StoreFile(fileMetadata types.FileMetadata) error + RemoveFile(fileMetadata types.FileMetadata) error +} + +type store struct { + db *sql.DB +} + +type Migrations struct { + name string + query string +} + +var migrations = []Migrations{ + { + name: "001-sourceFiles", + query: `CREATE TABLE IF NOT EXISTS files ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + path TEXT NOT NULL, + size INTEGER NOT NULL, + hash TEXT NOT NULL, + modifiedDate TIMESTAMP NOT NULL, + backedUp BOOLEAN NOT NULL + )`, + }, + { + name: "002-fileUniqueConstraint", + query: `CREATE UNIQUE INDEX IF NOT EXISTS file_unique ON files (name, path, hash)`, + }, +} + +func NewDB(appName string) (DB, error) { + dbPath, err := createDBFileIfNotExist(appName) + if err != nil { + return nil, fmt.Errorf("error creating db file %w", err) + } + dbSQL, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, fmt.Errorf("error opening db | %w", err) + } + return &store{ + db: dbSQL, + }, nil + + return &store{}, nil +} + +func (d *store) StoreFile(fileMetadata types.FileMetadata) error { + query := `INSERT INTO files (name, path, size, hash, modifiedDate, backedUp) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (name, path, hash) DO UPDATE SET size = ?, modifiedDate = ?, backedUp = ?` + _, err := d.db.Exec(query, fileMetadata.Name, fileMetadata.Path, fileMetadata.Size, fileMetadata.Hash, fileMetadata.ModifiedDate, fileMetadata.BackedUp, fileMetadata.Size, fileMetadata.ModifiedDate, fileMetadata.BackedUp) + if err != nil { + return fmt.Errorf("error storing file metadata | %w", err) + } + return nil +} + +func (d *store) RemoveFile(fileMetadata types.FileMetadata) error { + query := `DELETE FROM files WHERE name = ? AND path = ? AND hash = ?` + _, err := d.db.Exec(query, fileMetadata.Name, fileMetadata.Path, fileMetadata.Hash) + if err != nil { + return fmt.Errorf("error removing file metadata | %w", err) + } + return nil +} + +func (d *store) Close() error { + return d.db.Close() +} + +func (d *store) Migrate() error { + // check if migration table exists + var migrationsCheck string + //goland:noinspection SqlResolve + err := d.db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'").Scan(&migrationsCheck) + if err != nil { + if err == sql.ErrNoRows { + _, err := d.db.Exec("CREATE TABLE migrations (name TEXT NOT NULL)") + if err != nil { + return fmt.Errorf("error creating migrations table | %w", err) + } + } else { + return fmt.Errorf("error checking if migrations table exists | %w", err) + } + } + + for _, migration := range migrations { + var migrationInHistory string + err = d.db.QueryRow("SELECT name FROM migrations WHERE name = ?", migration.name).Scan(&migrationInHistory) + if err != nil { + if err == sql.ErrNoRows { + _, err := d.db.Exec(migration.query) + if err != nil { + return fmt.Errorf("error running migration: %s | %w", migration.name, err) + } + _, err = d.db.Exec("INSERT INTO migrations (name) VALUES (?)", migration.name) + if err != nil { + return fmt.Errorf("error inserting migration: %s into migrations table | %w", migration.name, err) + } + } else { + return fmt.Errorf("error checking if migration: %s has been run | %w", migration.name, err) + } + } + } + return nil +} + +func createDBFileIfNotExist(appName string) (string, error) { + configPath := configdir.LocalConfig(appName) + + // set up the config directory + err := configdir.MakePath(configPath) + if err != nil { + panic(err) + } + + dbDirectoryPath := path.Join(configPath, "db") + dbPath := path.Join(configPath, "db", fmt.Sprintf("%v.db", appName)) + // Set up the database + err = configdir.MakePath(dbDirectoryPath) + if err != nil { + panic(err) + } + + // If the file doesn't exist, create it, or append to the file + f, err := os.OpenFile(dbPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return "", fmt.Errorf("error opening file: %v", err) + } + defer func(f *os.File) { + err := f.Close() + if err != nil { + fmt.Println("error closing file") + } + }(f) + return dbPath, nil +} diff --git a/go.mod b/go.mod index 966aa8d..1de410b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module forever-files go 1.19 require ( + github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f + github.com/mattn/go-sqlite3 v1.14.17 github.com/urfave/cli/v2 v2.25.5 google.golang.org/grpc v1.55.0 ) diff --git a/go.sum b/go.sum index 620a2ad..d3c184c 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,10 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= +github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/urfave/cli/v2 v2.25.5 h1:d0NIAyhh5shGscroL7ek/Ya9QYQE0KNabJgiUinIQkc= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ff83c9a --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "forever-files/db" + "forever-files/source" + "forever-files/types" +) + +func main() { + + fmt.Printf("%v\n", types.AppName) + + store, err := db.NewDB(types.AppName) + if err != nil { + panic(fmt.Errorf("error creating db: %w", err)) + } + + err = store.Migrate() + if err != nil { + panic(fmt.Errorf("error migrating db: %w", err)) + } + + source.GatherInfo("C:\\Users\\gomas\\Nextcloud", store) +} diff --git a/source/source.go b/source/source.go new file mode 100644 index 0000000..0bb5bbb --- /dev/null +++ b/source/source.go @@ -0,0 +1,86 @@ +package source + +import ( + "crypto/sha256" + "fmt" + "forever-files/db" + "forever-files/types" + "io" + "log" + "os" + "path" +) + +// the purpose of this package is to gather information about the source files for the backup +// it will store the information in a database +// information to gather: +// - file name +// - file path +// - file size +// - file hash +// - modified date + +func GatherInfo(path string, db db.DB) { + err := walkDir(path, db) + if err != nil { + log.Fatal(err) + } +} + +func walkDir(dirPath string, db db.DB) error { + // get list of files in directory + directoryEntries, err := os.ReadDir(dirPath) + if err != nil { + return fmt.Errorf("error reading directory: %w", err) + } + for _, entry := range directoryEntries { + if entry.IsDir() { + err = walkDir(path.Join(dirPath, entry.Name()), db) + if err != nil { + return fmt.Errorf("error walking directory: %w", err) + } + } else { + // gather info + fileInfo, err := entry.Info() + if err != nil { + log.Default().Printf("error getting file info: %v", err) + continue + } + hash, err := hashFile(path.Join(dirPath, entry.Name())) + if err != nil { + log.Default().Printf("error hashing file: %v", err) + continue + } + // store info + fmt.Printf("Name: %v, Size: %v, Modified Date: %v, Hash: %v\n", fileInfo.Name(), fileInfo.Size(), fileInfo.ModTime(), hash) + err = db.StoreFile(types.FileMetadata{ + Name: fileInfo.Name(), + Path: dirPath, + Size: fileInfo.Size(), + Hash: hash, + ModifiedDate: fileInfo.ModTime(), + BackedUp: false, + }) + if err != nil { + log.Default().Printf("error storing file metadata: %v", err) + continue + } + } + } + return nil +} + +func hashFile(filePath string) ([]byte, error) { + file, err := os.Open(filePath) + if err != nil { + return []byte{}, fmt.Errorf("error opening file for hashing: %w", err) + } + defer file.Close() + + h := sha256.New() + if _, err := io.Copy(h, file); err != nil { + return []byte{}, fmt.Errorf("error hashing file: %w", err) + } + + return h.Sum(nil), nil +} diff --git a/types/types.go b/types/types.go new file mode 100644 index 0000000..465a181 --- /dev/null +++ b/types/types.go @@ -0,0 +1,16 @@ +package types + +import "time" + +const ( + AppName = "ForeverFiles" +) + +type FileMetadata struct { + Name string + Path string + Size int64 + Hash []byte + ModifiedDate time.Time + BackedUp bool +}