--- /dev/null
+// Copyright 2020-2021 Ian Jackson and contributors to Otter
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// There is NO WARRANTY.
+
+#![allow(dead_code)] // todo
+
+use crate::prelude::*;
+
+pub use crate::prelude::Sha512Trunc256 as Digester;
+
+#[derive(Debug,Copy,Clone,Hash,Eq,PartialEq,Serialize,Deserialize)]
+pub struct Hash(pub [u8; 32]);
+
+#[derive(Debug,Copy,Clone,Hash,Eq,PartialEq,Serialize,Deserialize)]
+#[derive(EnumString,Display,Ord,PartialOrd)]
+pub enum Kind {
+ #[strum(to_string="zip")] Zip,
+// #[strum(to_string="game.toml")] GameSpec, // identification problems
+}
+
+pub type Index = u16;
+const BUNDLES_MAX: Index = 64;
+
+#[derive(Copy,Clone,Debug,Hash,PartialEq,Eq,Ord,PartialOrd)]
+#[derive(Serialize,Deserialize)]
+pub struct Id { index: Index, kind: Kind, }
+
+#[derive(Debug,Clone)]
+pub struct InstanceBundles {
+ // todo: this vec is needed during loading only!
+ bundles: Vec<Option<Note>>,
+}
+
+#[derive(Debug,Clone)]
+struct Note {
+ kind: Kind,
+ state: State,
+}
+
+type BadBundle = String; // todo: make this a newtype
+
+#[derive(Debug,Clone)]
+enum State {
+ Uploading,
+ BadBundle(BadBundle),
+ Loaded(Loaded),
+}
+
+#[derive(Debug,Clone)]
+struct Loaded {
+ meta: BundleMeta,
+}
+
+pub fn b_dir(instance: &InstanceName) -> String {
+ savefilename(instance, "b-", "")
+}
+fn b_file<S>(instance: &InstanceName, index: Index, suffix: S) -> String
+where S: Display
+{
+ format!("{}/{:05}.{}",
+ savefilename(instance, "b-", ""),
+ index, suffix)
+}
+
+impl Id {
+ fn path_tmp(&self, instance: &InstanceName) -> String {
+ b_file(instance, self.index, "tmp")
+ }
+
+ fn path(&self, instance: &InstanceName) -> String {
+ b_file(instance, self.index, self.kind)
+ }
+}
+
+#[derive(Error,Debug)]
+enum IncorporateError {
+ NotBundle(&'static str),
+ IE(#[from] IE),
+}
+display_as_debug!{IncorporateError}
+impl From<&'static str> for IncorporateError {
+ fn from(why: &'static str) -> Self { Self::NotBundle(why) }
+}
+
+pub struct Uploading {
+ instance: Arc<InstanceName>,
+ id: Id,
+ file: DigestWrite<Digester, BufWriter<fs::File>>,
+}
+
+#[throws(IE)]
+fn load_bundle(ib: &mut InstanceBundles, ig: &mut Instance,
+ id: Id, path: &str) {
+ #[derive(Error,Debug)]
+ enum LoadError {
+ BadBundle(BadBundle),
+ IE(#[from] IE),
+ }
+ display_as_debug!{LoadError}
+
+ let iu: usize = id.index.into();
+
+ match ib.bundles.get(iu) {
+ None => ib.bundles.resize_with(iu+1, default),
+ Some(None) => { },
+ Some(Some(Note { kind:_, state: State::Uploading })) => { },
+ Some(Some(ref note)) => throw!(IE::DuplicateBundle {
+ index: id.index,
+ kinds: [note.kind, id.kind],
+ })
+ };
+ let slot = &mut ib.bundles[iu];
+
+ #[throws(LoadError)]
+ fn inner(_ig: &mut Instance,
+ _id: Id, _path: &str) -> Loaded {
+ Loaded { meta: BundleMeta { title: "title!".to_owned() } }
+ // todo: find zipfile, read metdata toml
+ // todo:: show in UI for download
+ // todo: do actual things, eg libraries and specs
+ }
+
+ let state = match inner(ig,id,path) {
+ Ok(loaded) => State::Loaded(loaded),
+ Err(LoadError::BadBundle(bad)) => State::BadBundle(bad),
+ Err(LoadError::IE(ie)) => throw!(ie),
+ };
+
+ *slot = Some(Note { kind: id.kind, state })
+}
+
+#[throws(IncorporateError)]
+fn incorporate_bundle(ib: &mut InstanceBundles, ig: &mut Instance,
+ fpath: &str) {
+ let fleaf = fpath.rsplitn(2, '/').next().unwrap();
+
+ let [lhs, rhs] = fleaf.splitn(2, '.')
+ .collect::<ArrayVec<[&str;2]>>()
+ .into_inner().map_err(|_| "no dot")?;
+
+ let index = lhs.parse().map_err(|_| "bad index")?;
+ let kind = rhs.parse().map_err(|_| "bad extension")?;
+
+ load_bundle(ib, ig, Id { index, kind }, fpath)?;
+}
+
+impl InstanceBundles {
+ pub fn new() -> Self { InstanceBundles{ bundles: default() } }
+
+ #[throws(IE)]
+ pub fn load_game_bundles(ig: &mut Instance) -> Self {
+ let bd = b_dir(&ig.name);
+ let mo = glob::MatchOptions {
+ require_literal_leading_dot: true,
+ ..default()
+ };
+ let mut ib = InstanceBundles::new();
+ for fpath in
+ glob::glob_with(&format!("{}/*", bd), mo)
+ .context("pattern for bundle glob")?
+ {
+ let fpath = fpath.context("bundle glob")?;
+ let fpath = fpath
+ .to_str().ok_or_else(|| anyhow!("glob unicode conversion"))?;
+ match incorporate_bundle(&mut ib, ig, fpath) {
+ Ok(()) => { },
+ Err(IncorporateError::NotBundle(why)) => {
+ debug!("bundle file {:?} skippping {}", &fpath, why);
+ }
+ Err(IncorporateError::IE(ie)) => throw!(ie),
+ }
+ }
+ ib
+ }
+
+ #[throws(MgmtError)]
+ pub fn start_upload(&mut self, ig: &mut Instance, kind: Kind)
+ -> Uploading {
+ let state = State::Uploading;
+ let slot = Some(Note { kind, state });
+ let index = self.bundles.len();
+ if index >= usize::from(BUNDLES_MAX) { throw!(ME::TooManyBundles) }
+ let index = index.try_into().unwrap();
+ self.bundles.push(slot);
+ let id = Id { kind, index };
+ let tmp = id.path_tmp(&ig.name);
+ let file = fs::File::create(&tmp)
+ .with_context(|| tmp.clone()).context("create").map_err(IE::from)?;
+ let file = BufWriter::new(file);
+ let file = DigestWrite::new(file);
+ let instance = ig.name.clone();
+ Uploading { file, instance, id }
+ }
+}
+
+impl Uploading {
+ #[throws(MgmtError)]
+ pub fn bulk<R>(&mut self, data: &mut R)
+ where R: Read
+ {
+ io::copy(data, &mut self.file)
+ .with_context(|| self.id.path_tmp(&*self.instance))
+ .context("copy").map_err(IE::from)?
+ }
+}
+
+impl InstanceBundles {
+ #[throws(MgmtError)]
+ pub fn finish_upload(&mut self, ig: &mut Instance,
+ Uploading { instance:_, id, file }: Uploading,
+ expected: &Hash) {
+ let (hash, mut file) = file.finish();
+ let tmp = id.path_tmp(&ig.name);
+ let install = id.path(&ig.name);
+ file.flush()
+ .with_context(|| tmp.clone()).context("flush").map_err(IE::from)?;
+ if hash.as_slice() != &expected.0[..] { throw!(ME::UploadCorrupted) }
+ load_bundle(self, ig, id, &tmp)?;
+ match self.bundles.get(usize::from(id.index)) {
+ Some(Some(Note { state: State::Loaded(..), .. })) => {
+ fs::rename(&tmp, &install)
+ .with_context(||install.clone())
+ .context("install").map_err(IE::from)?;
+ }
+ Some(Some(Note { state: State::BadBundle(ref bad), .. })) => {
+ throw!(ME::BadBundle(bad.clone()))
+ }
+ ref x => panic!("unexpected {:?}", x),
+ };
+ }
+}
#[derive(Debug)]
pub struct InstanceOuter {
c: Mutex<InstanceContainer>,
+ b: Mutex<InstanceBundles>,
}
#[derive(Debug,Clone,Serialize,Deserialize,Default)]
pub fn downgrade_to_weak(&self) -> InstanceWeakRef {
InstanceWeakRef(Arc::downgrade(&self.0))
}
+
+ pub fn lock_bundles(&self) -> MutexGuard<'_, InstanceBundles> {
+ self.0.b.lock()
+ }
}
impl InstanceWeakRef {
gref: must_not_escape.clone(),
})
}
+
+ pub fn lock_bundles<'r>(&'r self) -> Unauthorised<MutexGuard<'r, InstanceBundles>, A> {
+ let must_not_escape = self.by_ref(Authorisation::authorise_any());
+ Unauthorised::of(must_not_escape.lock_bundles())
+ }
}
impl Instance {
};
let c = Mutex::new(c);
- let gref = InstanceRef(Arc::new(InstanceOuter { c }));
+ let b = Mutex::new(InstanceBundles::new());
+ let gref = InstanceRef(Arc::new(InstanceOuter { c, b }));
let mut ig = gref.lock()?;
let entry = games.entry(name);
mut g: MutexGuard<InstanceContainer>,
_: Authorisation<InstanceName>) {
let a_savefile = savefilename(&g.g.name, "a-", "");
+ let b_dir = bundles::b_dir(&g.g.name);
let g_file = savefilename(&g.g.name, "g-", "");
fs::remove_file(&g_file).context("remove").context(g_file)?;
);
}
best_effort(|f| fs::remove_file(f), &a_savefile, "auth file");
+ best_effort(|f| fs::remove_dir_all(f), &b_dir, "bundles dir");
})(); // <- No ?, ensures that IEFE is infallible (barring panics)
}
// ---------- save/load ----------
#[derive(Copy,Clone,Debug)]
-enum SaveFileOrDir { File, #[allow(dead_code)]/*xxx*/ Dir }
+enum SaveFileOrDir { File, Dir }
impl SaveFileOrDir {
#[throws(io::Error)]
use SavefilenameParseResult::*;
if leaf.starts_with(b"a-") { return Auxiliary(SaveFileOrDir::File) }
+ if leaf.starts_with(b"b-") { return Auxiliary(SaveFileOrDir::Dir ) }
let rhs = match leaf.strip_prefix(b"g-") {
Some(rhs) => rhs,
None => return NotGameFile,
let name = InstanceName::from_str(&rhs)?;
- let aux_leaves = [ b"a-" ].iter().map(|prefix| {
+ let aux_leaves = [ b"a-", b"b-" ].iter().map(|prefix| {
let mut s: Vec<_> = (prefix[..]).into(); s.extend(after_ftype_prefix); s
}).collect();
GameFile { name, aux_leaves }
(tokens, accounts.bulk_check(&acctids))
};
- let g = Instance {
+ let mut g = Instance {
gs, iplayers, links,
acl: acl.into(),
ipieces: IPieces(ipieces),
tokens_clients: default(),
tokens_players: default(),
};
+
+ let b = InstanceBundles::load_game_bundles(&mut g)?;
+ let b = Mutex::new(b);
+
let c = InstanceContainer {
live: true,
game_dirty: false,
g,
};
let c = Mutex::new(c);
- let gref = InstanceRef(Arc::new(InstanceOuter { c }));
+ let gref = InstanceRef(Arc::new(InstanceOuter { c, b }));
let mut g = gref.lock().unwrap();
let ig = &mut *g;