changeset 8:a5de18a5e99e

Implement GeoJSON retrieval and per-point protection
author Lewin Bormann <lbo@spheniscida.de>
date Tue, 01 Dec 2020 21:22:08 +0100
parents ebcf9edce874
children b8b99af28199
files Cargo.lock Cargo.toml pgsql_schema.sql src/main.rs
diffstat 4 files changed, 208 insertions(+), 24 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.lock	Tue Dec 01 20:28:22 2020 +0100
+++ b/Cargo.lock	Tue Dec 01 21:22:08 2020 +0100
@@ -199,6 +199,7 @@
  "libc",
  "num-integer",
  "num-traits",
+ "serde",
  "time",
  "winapi 0.3.9",
 ]
@@ -271,7 +272,7 @@
 checksum = "066ceb7928ca93a9bedc6d0e612a8a0424048b0ab1f75971b203d01420c055d7"
 dependencies = [
  "devise_core",
- "quote",
+ "quote 0.6.13",
 ]
 
 [[package]]
@@ -281,9 +282,9 @@
 checksum = "cf41c59b22b5e3ec0ea55c7847e5f358d340f3a8d6d53a5cf4f1564967f96487"
 dependencies = [
  "bitflags",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "syn 0.15.44",
 ]
 
 [[package]]
@@ -395,6 +396,8 @@
  "postgres",
  "rocket",
  "rocket_contrib",
+ "serde",
+ "serde_json",
 ]
 
 [[package]]
@@ -559,6 +562,12 @@
 ]
 
 [[package]]
+name = "itoa"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
+
+[[package]]
 name = "kernel32-sys"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -797,9 +806,9 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bfc1c836fdc3d1ef87c348b237b5b5c4dff922156fb2d968f57734f9669768ca"
 dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "syn 0.15.44",
  "version_check 0.9.2",
  "yansi",
 ]
@@ -902,7 +911,16 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
 dependencies = [
- "unicode-xid",
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
+dependencies = [
+ "unicode-xid 0.2.1",
 ]
 
 [[package]]
@@ -911,7 +929,16 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
 dependencies = [
- "proc-macro2",
+ "proc-macro2 0.4.30",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
+dependencies = [
+ "proc-macro2 1.0.24",
 ]
 
 [[package]]
@@ -1060,7 +1087,7 @@
  "devise",
  "glob",
  "indexmap",
- "quote",
+ "quote 0.6.13",
  "rocket_http",
  "version_check 0.9.2",
  "yansi",
@@ -1088,7 +1115,7 @@
 checksum = "fd30b50723c98c7ba6eea01cc5416e273397fa09ac45569538758ae24590dd7d"
 dependencies = [
  "devise",
- "quote",
+ "quote 0.6.13",
  "version_check 0.9.2",
  "yansi",
 ]
@@ -1107,10 +1134,16 @@
  "smallvec",
  "state",
  "time",
- "unicode-xid",
+ "unicode-xid 0.1.0",
 ]
 
 [[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
 name = "safemem"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1151,6 +1184,31 @@
 version = "1.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
+dependencies = [
+ "proc-macro2 1.0.24",
+ "quote 1.0.7",
+ "syn 1.0.53",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.59"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
 
 [[package]]
 name = "sha2"
@@ -1240,9 +1298,20 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
 dependencies = [
- "proc-macro2",
- "quote",
- "unicode-xid",
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68"
+dependencies = [
+ "proc-macro2 1.0.24",
+ "quote 1.0.7",
+ "unicode-xid 0.2.1",
 ]
 
 [[package]]
@@ -1332,6 +1401,12 @@
 checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
 
 [[package]]
+name = "unicode-xid"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
+
+[[package]]
 name = "universal-hash"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
--- a/Cargo.toml	Tue Dec 01 20:28:22 2020 +0100
+++ b/Cargo.toml	Tue Dec 01 21:22:08 2020 +0100
@@ -9,7 +9,9 @@
 [dependencies]
 rocket = "~0.4"
 postgres = { version = "~0.15", features = ["with-chrono"] }
-chrono = "^0.4"
+chrono = { version = "^0.4", features = ["serde"] }
+serde = { version = "~1.0", features = ["derive"] }
+serde_json = "~1.0"
 
 [dependencies.rocket_contrib]
 version = "~0.4"
--- a/pgsql_schema.sql	Tue Dec 01 20:28:22 2020 +0100
+++ b/pgsql_schema.sql	Tue Dec 01 21:22:08 2020 +0100
@@ -32,7 +32,7 @@
     lat double precision,
     long double precision,
     spd double precision,
-    t timestamp with time zone,
+    t timestamp with time zone not null,
     ele double precision
 );
 
--- a/src/main.rs	Tue Dec 01 20:28:22 2020 +0100
+++ b/src/main.rs	Tue Dec 01 21:22:08 2020 +0100
@@ -31,9 +31,118 @@
     None
 }
 
+/// Fetch geodata as JSON.
+///
+#[derive(serde::Serialize, Debug)]
+struct GeoProperties {
+    time: chrono::DateTime<chrono::Utc>,
+    altitude: Option<f64>,
+    speed: Option<f64>,
+}
+
+#[derive(serde::Serialize, Debug)]
+struct GeoGeometry {
+    #[serde(rename = "type")]
+    typ: String, // always "Point"
+    coordinates: Vec<f64>, // always [long, lat]
+}
+
+#[derive(serde::Serialize, Debug)]
+struct GeoFeature {
+    #[serde(rename = "type")]
+    typ: String, // always "Feature"
+    properties: GeoProperties,
+    geometry: GeoGeometry,
+}
+
+fn geofeature_from_row(
+    ts: chrono::DateTime<chrono::Utc>,
+    lat: Option<f64>,
+    long: Option<f64>,
+    spd: Option<f64>,
+    ele: Option<f64>,
+) -> GeoFeature {
+    GeoFeature {
+        typ: "Feature".into(),
+        properties: GeoProperties {
+            time: ts,
+            altitude: ele,
+            speed: spd,
+        },
+        geometry: GeoGeometry {
+            typ: "Point".into(),
+            coordinates: vec![long.unwrap_or(0.), lat.unwrap_or(0.)],
+        },
+    }
+}
+
+#[derive(serde::Serialize, Debug)]
+struct GeoJSON {
+    #[serde(rename = "type")]
+    typ: String, // always "FeatureCollection"
+    features: Vec<GeoFeature>,
+}
+
+/// Retrieve GeoJSON data.
+#[rocket::get("/geo/<name>/retrieve/json?<secret>&<from>&<to>&<max>")]
+fn retrieve_json(
+    db: DBConn,
+    name: String,
+    secret: Option<String>,
+    from: Option<String>,
+    to: Option<String>,
+    max: Option<i64>,
+) -> rocket::response::content::Json<String> {
+    let mut returnable = GeoJSON {
+        typ: "FeatureCollection".into(),
+        features: vec![],
+    };
+
+    let from_ts = from
+        .and_then(flexible_timestamp_parse)
+        .unwrap_or(chrono::DateTime::from_utc(
+            chrono::NaiveDateTime::from_timestamp(0, 0),
+            chrono::Utc,
+        ));
+    let to_ts = to
+        .and_then(flexible_timestamp_parse)
+        .unwrap_or(chrono::Utc::now());
+    let max = max.unwrap_or(16384);
+    //println!("from {:?} to {:?}", from_ts, to_ts);
+    //println!("secret {:?}", secret);
+
+    let stmt = db.0.prepare_cached(
+        r"SELECT t, lat, long, spd, ele FROM geohub.geodata
+        WHERE (id = $1) and (t between $2 and $3) AND (secret = public.digest($4, 'sha256') or secret is null)
+        LIMIT $5").unwrap(); // Must succeed.
+    let rows = stmt
+        .query(&[&name, &from_ts, &to_ts, &secret, &max])
+        .unwrap();
+    {
+        println!("got {} rows", rows.len());
+        returnable.features = Vec::with_capacity(rows.len());
+        for row in rows.iter() {
+            let (ts, lat, long, spd, ele): (
+                chrono::DateTime<chrono::Utc>,
+                Option<f64>,
+                Option<f64>,
+                Option<f64>,
+                Option<f64>,
+            ) = (row.get(0), row.get(1), row.get(2), row.get(3), row.get(4));
+            returnable
+                .features
+                .push(geofeature_from_row(ts, lat, long, spd, ele));
+        }
+    }
+
+    rocket::response::content::Json(serde_json::to_string(&returnable).unwrap())
+}
+
+/// Ingest geo data.
+
 /// time is like 2020-11-30T20:12:36.444Z (ISO 8601). By default, server time is set.
 /// secret can be used to protect points.
-#[rocket::get("/geo/<name>/log?<lat>&<longitude>&<time>&<s>&<ele>&<secret>")]
+#[rocket::post("/geo/<name>/log?<lat>&<longitude>&<time>&<s>&<ele>&<secret>")]
 fn log(
     db: DBConn,
     name: String,
@@ -51,17 +160,15 @@
     if let Some(time) = time {
         ts = flexible_timestamp_parse(time).unwrap_or(ts);
     }
-    db.0.execute(
-        "INSERT INTO geohub.geodata (id, lat, long, spd, t, ele, secret) VALUES ($1, $2, $3, $4, $5, $6, $7)",
-        &[&name, &lat, &longitude, &s, &ts, &ele, &secret],
-    )
-    .unwrap();
+    let stmt = db.0.prepare_cached("INSERT INTO geohub.geodata (id, lat, long, spd, t, ele, secret) VALUES ($1, $2, $3, $4, $5, $6, public.digest($7, 'sha256'))").unwrap();
+    stmt.execute(&[&name, &lat, &longitude, &s, &ts, &ele, &secret])
+        .unwrap();
     rocket::http::Status::Ok
 }
 
 fn main() {
     rocket::ignite()
         .attach(DBConn::fairing())
-        .mount("/", rocket::routes![log])
+        .mount("/", rocket::routes![log, retrieve_json])
         .launch();
 }