Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sync client #31

Merged
merged 24 commits into from Apr 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
958 changes: 908 additions & 50 deletions Cargo.lock

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "atuin"
version = "0.4.0"
version = "0.5.0"
authors = ["Ellie Huxtable <e@elm.sh>"]
edition = "2018"
license = "MIT"
Expand All @@ -9,20 +9,22 @@ description = "atuin - magical shell history"
[dependencies]
log = "0.4"
fern = "0.6.0"
chrono = "0.4"
chrono = { version = "0.4", features = ["serde"] }
eyre = "0.6"
shellexpand = "2"
structopt = "0.3"
directories = "3"
uuid = { version = "0.8", features = ["v4"] }
indicatif = "0.15.0"
hostname = "0.3.1"
whoami = "1.1.2"
rocket = "0.4.7"
chrono-english = "0.1.4"
cli-table = "0.4"
config = "0.11"
serde_derive = "1.0.125"
serde = "1.0.125"
serde_json = "1.0.64"
rmp-serde = "0.15.4"
tui = "0.14"
termion = "1.5"
unicode-width = "0.1"
Expand All @@ -31,6 +33,12 @@ diesel = { version = "1.4.4", features = ["postgres", "chrono"] }
diesel_migrations = "1.4.0"
dotenv = "0.15.0"
sodiumoxide = "0.2.6"
reqwest = { version = "0.11", features = ["blocking", "json"] }
base64 = "0.13.0"
fork = "0.1.18"
parse_duration = "2.1.1"
rand = "0.8.3"
rust-crypto = "^0.2"

[dependencies.rusqlite]
version = "0.25"
Expand Down
65 changes: 35 additions & 30 deletions config.toml
Expand Up @@ -3,36 +3,41 @@
# This section specifies the config for a local client,
# ie where your shell history is on your local machine
[local]
# (optional)
# where to store your database, default is your system data directory
# mac: ~/Library/Application Support/com.elliehuxtable.atuin/history.db
# linux: ~/.local/share/atuin/history.db
db_path = "~/.history.db"
# (optional, default us)
# date format used, either "us" or "uk"
dialect = "uk"
# (optional, default false)
# whether to enable sync of history. requires authentication
sync = false
# (optional, default 5m)
# how often to sync history. note that this is only triggered when a command is ran, and the last sync was >= this value ago
# set it to 0 to sync after every command
sync_frequency = "5m"
# (optional, default https://atuin.elliehuxtable.com)
# address of the sync server
sync_address = "https://atuin.elliehuxtable.com"
## where to store your database, default is your system data directory
## mac: ~/Library/Application Support/com.elliehuxtable.atuin/history.db
## linux: ~/.local/share/atuin/history.db
# db_path = "~/.history.db"

## where to store your encryption key, default is your system data directory
# key_path = "~/.key"

## where to store your auth session token, default is your system data directory
# session_path = "~/.key"

## date format used, either "us" or "uk"
# dialect = "uk"

## enable or disable automatic sync
# auto_sync = true

## how often to sync history. note that this is only triggered when a command
## is ran, so sync intervals may well be longer
## set it to 0 to sync after every command
# sync_frequency = "5m"

## address of the sync server
# sync_address = "https://api.atuin.sh"

# This section configures the sync server, if you decide to host your own
[remote]
# (optional, default 127.0.0.1)
# host to bind, can also be passed via CLI args
host = "127.0.0.1"
# (optional, default 8888)
# port to bind, can also be passed via CLI args
port = 8888
# (optional, default false)
# whether to allow anyone to register an account
open_registration = false
# (required)
# URI for postgres (using development creds here)
db_uri="postgres://username:password@localhost/atuin"
## host to bind, can also be passed via CLI args
# host = "127.0.0.1"

## port to bind, can also be passed via CLI args
# port = 8888

## whether to allow anyone to register an account
# open_registration = false

## URI for postgres (using development creds here)
# db_uri="postgres://username:password@localhost/atuin"
6 changes: 4 additions & 2 deletions migrations/2021-03-20-151809_create_history/up.sql
Expand Up @@ -4,8 +4,10 @@ create table history (
id bigserial primary key,
client_id text not null unique, -- the client-generated ID
user_id bigserial not null, -- allow multiple users
mac varchar(128) not null, -- store a hashed mac address, to identify machines - more likely to be unique than hostname
hostname text not null, -- a unique identifier from the client (can be hashed, random, whatever)
timestamp timestamp not null, -- one of the few non-encrypted metadatas

data varchar(8192) not null -- store the actual history data, encrypted. I don't wanna know!
data varchar(8192) not null, -- store the actual history data, encrypted. I don't wanna know!

created_at timestamp not null default current_timestamp
);
5 changes: 5 additions & 0 deletions migrations/2021-03-20-171007_create_users/up.sql
@@ -1,6 +1,11 @@
-- Your SQL goes here
create table users (
id bigserial primary key, -- also store our own ID
username varchar(32) not null unique, -- being able to contact users is useful
email varchar(128) not null unique, -- being able to contact users is useful
password varchar(128) not null unique
);

-- the prior index is case sensitive :(
CREATE UNIQUE INDEX email_unique_idx on users (LOWER(email));
CREATE UNIQUE INDEX username_unique_idx on users (LOWER(username));
36 changes: 36 additions & 0 deletions src/api.rs
@@ -0,0 +1,36 @@
use chrono::Utc;

// This is shared between the client and the server, and has the data structures
// representing the requests/responses for each method.
// TODO: Properly define responses rather than using json!

#[derive(Debug, Serialize, Deserialize)]
pub struct RegisterRequest {
pub email: String,
pub username: String,
pub password: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AddHistoryRequest {
pub id: String,
pub timestamp: chrono::DateTime<Utc>,
pub data: String,
pub hostname: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CountResponse {
pub count: i64,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ListHistoryResponse {
pub history: Vec<String>,
}
30 changes: 19 additions & 11 deletions src/command/history.rs
@@ -1,10 +1,13 @@
use std::env;

use eyre::Result;
use fork::{fork, Fork};
use structopt::StructOpt;

use crate::local::database::Database;
use crate::local::history::History;
use crate::local::sync;
use crate::settings::Settings;

#[derive(StructOpt)]
pub enum Cmd {
Expand Down Expand Up @@ -50,21 +53,13 @@ fn print_list(h: &[History]) {
}

impl Cmd {
pub fn run(&self, db: &mut impl Database) -> Result<()> {
pub fn run(&self, settings: &Settings, db: &mut impl Database) -> Result<()> {
match self {
Self::Start { command: words } => {
let command = words.join(" ");
let cwd = env::current_dir()?.display().to_string();

let h = History::new(
chrono::Utc::now().timestamp_nanos(),
command,
cwd,
-1,
-1,
None,
None,
);
let h = History::new(chrono::Utc::now(), command, cwd, -1, -1, None, None);

// print the ID
// we use this as the key for calling end
Expand All @@ -76,10 +71,23 @@ impl Cmd {
Self::End { id, exit } => {
let mut h = db.load(id)?;
h.exit = *exit;
h.duration = chrono::Utc::now().timestamp_nanos() - h.timestamp;
h.duration = chrono::Utc::now().timestamp_nanos() - h.timestamp.timestamp_nanos();

db.update(&h)?;

if settings.local.should_sync()? {
match fork() {
Ok(Fork::Parent(child)) => {
debug!("launched sync background process with PID {}", child);
}
Ok(Fork::Child) => {
debug!("running periodic background sync");
sync::sync(settings, false, db)?;
}
Err(_) => println!("Fork failed"),
}
}

Ok(())
}

Expand Down
48 changes: 48 additions & 0 deletions src/command/login.rs
@@ -0,0 +1,48 @@
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;

use eyre::Result;
use structopt::StructOpt;

use crate::settings::Settings;

#[derive(StructOpt)]
#[structopt(setting(structopt::clap::AppSettings::DeriveDisplayOrder))]
pub struct Cmd {
#[structopt(long, short)]
pub username: String,

#[structopt(long, short)]
pub password: String,

#[structopt(long, short, about = "the encryption key for your account")]
pub key: String,
}

impl Cmd {
pub fn run(&self, settings: &Settings) -> Result<()> {
let mut map = HashMap::new();
map.insert("username", self.username.clone());
map.insert("password", self.password.clone());

let url = format!("{}/login", settings.local.sync_address);
let client = reqwest::blocking::Client::new();
let resp = client.post(url).json(&map).send()?;

let session = resp.json::<HashMap<String, String>>()?;
let session = session["session"].clone();

let session_path = settings.local.session_path.as_str();
let mut file = File::create(session_path)?;
file.write_all(session.as_bytes())?;

let key_path = settings.local.key_path.as_str();
let mut file = File::create(key_path)?;
file.write_all(&base64::decode(self.key.clone())?)?;

println!("Logged in!");

Ok(())
}
}
34 changes: 33 additions & 1 deletion src/command/mod.rs
Expand Up @@ -9,9 +9,12 @@ mod event;
mod history;
mod import;
mod init;
mod login;
mod register;
mod search;
mod server;
mod stats;
mod sync;

#[derive(StructOpt)]
pub enum AtuinCmd {
Expand All @@ -38,6 +41,21 @@ pub enum AtuinCmd {

#[structopt(about = "interactive history search")]
Search { query: Vec<String> },

#[structopt(about = "sync with the configured server")]
Sync {
#[structopt(long, short, about = "force re-download everything")]
force: bool,
},

#[structopt(about = "login to the configured server")]
Login(login::Cmd),

#[structopt(about = "register with the configured server")]
Register(register::Cmd),

#[structopt(about = "print the encryption key for transfer to another machine")]
Key,
}

pub fn uuid_v4() -> String {
Expand All @@ -47,13 +65,27 @@ pub fn uuid_v4() -> String {
impl AtuinCmd {
pub fn run(self, db: &mut impl Database, settings: &Settings) -> Result<()> {
match self {
Self::History(history) => history.run(db),
Self::History(history) => history.run(settings, db),
Self::Import(import) => import.run(db),
Self::Server(server) => server.run(settings),
Self::Stats(stats) => stats.run(db, settings),
Self::Init => init::init(),
Self::Search { query } => search::run(&query, db),

Self::Sync { force } => sync::run(settings, force, db),
Self::Login(l) => l.run(settings),
Self::Register(r) => register::run(
settings,
r.username.as_str(),
r.email.as_str(),
r.password.as_str(),
),
Self::Key => {
let key = std::fs::read(settings.local.key_path.as_str())?;
println!("{}", base64::encode(key));
Ok(())
}

Self::Uuid => {
println!("{}", uuid_v4());
Ok(())
Expand Down