mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 01:54:47 +00:00
* feat: base of instances v2 * feat: use old profiles with compat layer * prototype: instances v2 * fix: install_from using profile * fix: skins migration fix * fix: frontend still using profile path * fix: add update proj multiselect guard * fix: cargo fmt * fix: content missing fields * feat: break up app-lib/api/instance.rs * fix: check_content_updates mismatch * fix: updater modal cleanup w/new structure * feat: better update all handling * fix: remove preview_update_all * fix: feedback on bulk update + lint * fix: rem transitions * fix: change to jsonb * feat: app db backup after update * fix: lint * fix: sqlx prepare + use sqlx macros * fix: lint * fix: bugs * feat: defuck the installing process up * fix: bug of hell * fix: shear * fix: fmt * fix: install progress spacing + change mc/content/overrides to bytes * fix: lint * fix: prepr * fix: navtabs anim not working in app * fix: worlds.vue improvements + browse page fixes * feat: optimise queries + adapter fns * fix: lint * fix: lint * feat: shared modrinth-content-management crate (#6469) * feat: disable warnings setting * feat: add instances shortcuts (#6329) * Add modrinth://launch deep link to start a profile Support external profile launching via modrinth://launch/{profile_path} for integrations such as Stream Deck. * Change route to /launch/profile/{id} for future extensibility * fix: ensure profile path is url decoded * fix: URL-decode profile path from deep link * fix: use urlencoding crate for URL decoding * feat: implement app instance shortcuts * feat: change windows shortcut creation to use windows api instead * feat: implement creating a shortcut launching world/server * format * fmt * fix multiline inline tables * pnpm prepr * feat: move create shortcut to last item * refactor: split up shortcuts.rs for individual platforms * refactor: turn profile launch url into url type * use string literal and add safety comment * pt2 * refactor: rename anything that's profile into instance * update mac shortcut --------- Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com> --------- Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com> Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com>
258 lines
6.9 KiB
Rust
258 lines
6.9 KiB
Rust
use sqlx::ConnectOptions;
|
|
use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection};
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::Duration;
|
|
|
|
const CURRENT_APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
|
|
pub(crate) async fn maybe_backup_existing_app_db(
|
|
db_path: &Path,
|
|
) -> crate::Result<()> {
|
|
if !db_path.try_exists()? {
|
|
tracing::debug!(
|
|
"Skipping pre-migration app database backup because {} does not exist",
|
|
db_path.display()
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
tracing::debug!(
|
|
"Inspecting {} for a pre-migration app database backup",
|
|
db_path.display()
|
|
);
|
|
|
|
let mut conn = match open_read_only_db(db_path).await {
|
|
Ok(conn) => conn,
|
|
Err(err) => {
|
|
tracing::error!(
|
|
"Failed to open {} read-only before migrations: {err}",
|
|
db_path.display()
|
|
);
|
|
return Err(err);
|
|
}
|
|
};
|
|
|
|
let has_user_tables = match has_user_tables(&mut conn).await {
|
|
Ok(has_user_tables) => has_user_tables,
|
|
Err(err) => {
|
|
tracing::error!(
|
|
"Failed to inspect app database tables before migrations: {err}"
|
|
);
|
|
return Err(err);
|
|
}
|
|
};
|
|
if !has_user_tables {
|
|
tracing::debug!(
|
|
"Skipping pre-migration app database backup because {} has no app data tables",
|
|
db_path.display()
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
let stored_version = match read_stored_app_version(&mut conn).await {
|
|
Ok(version) => version,
|
|
Err(err) => {
|
|
tracing::error!(
|
|
"Failed to read stored app database version before migrations: {err}"
|
|
);
|
|
return Err(err);
|
|
}
|
|
};
|
|
if stored_version.as_deref() == Some(CURRENT_APP_VERSION) {
|
|
tracing::debug!(
|
|
"Skipping pre-migration app database backup because app version is already recorded as {CURRENT_APP_VERSION}"
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
let stored_version = stored_version.as_deref().unwrap_or("unknown");
|
|
let backup_dir = match app_db_backup_dir() {
|
|
Ok(path) => path,
|
|
Err(err) => {
|
|
tracing::error!(
|
|
"Failed to resolve app database backup directory before migrations: {err}"
|
|
);
|
|
return Err(err);
|
|
}
|
|
};
|
|
let backup_path = match next_backup_path(
|
|
&backup_dir,
|
|
stored_version,
|
|
CURRENT_APP_VERSION,
|
|
)
|
|
.await
|
|
{
|
|
Ok(path) => path,
|
|
Err(err) => {
|
|
tracing::error!(
|
|
"Failed to choose app database backup path in {} before migrations: {err}",
|
|
backup_dir.display()
|
|
);
|
|
return Err(err);
|
|
}
|
|
};
|
|
|
|
tracing::info!(
|
|
"Creating pre-migration app database backup from version {stored_version} before opening with version {CURRENT_APP_VERSION} at {}",
|
|
backup_path.display()
|
|
);
|
|
|
|
if let Err(err) = create_sqlite_snapshot(&mut conn, &backup_path).await {
|
|
tracing::error!(
|
|
"Failed to create pre-migration app database backup at {}: {err}",
|
|
backup_path.display()
|
|
);
|
|
return Err(err);
|
|
}
|
|
|
|
tracing::info!(
|
|
"Created pre-migration app database backup at {}",
|
|
backup_path.display()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn open_read_only_db(db_path: &Path) -> crate::Result<SqliteConnection> {
|
|
let conn_options = SqliteConnectOptions::new()
|
|
.filename(db_path)
|
|
.busy_timeout(Duration::from_secs(30))
|
|
.read_only(true)
|
|
.create_if_missing(false);
|
|
|
|
Ok(conn_options.connect().await?)
|
|
}
|
|
|
|
pub fn app_db_backup_dir() -> crate::Result<PathBuf> {
|
|
if let Some(path) = std::env::var_os("THESEUS_DB_BACKUP_DIR") {
|
|
return Ok(PathBuf::from(path));
|
|
}
|
|
|
|
let base = dirs::data_local_dir().or_else(dirs::data_dir).ok_or(
|
|
crate::ErrorKind::FSError(
|
|
"Could not find valid data dir for app database backups"
|
|
.to_string(),
|
|
),
|
|
)?;
|
|
|
|
Ok(base.join("Modrinth").join("Backups").join("app-db"))
|
|
}
|
|
|
|
async fn has_user_tables(conn: &mut SqliteConnection) -> crate::Result<bool> {
|
|
let count = sqlx::query_scalar!(
|
|
"
|
|
SELECT COUNT(*)
|
|
FROM sqlite_master
|
|
WHERE type = 'table'
|
|
AND name NOT LIKE 'sqlite_%'
|
|
AND name NOT IN ('_sqlx_migrations', 'app_metadata')
|
|
",
|
|
)
|
|
.fetch_one(&mut *conn)
|
|
.await?;
|
|
|
|
Ok(count > 0)
|
|
}
|
|
|
|
async fn read_stored_app_version(
|
|
conn: &mut SqliteConnection,
|
|
) -> crate::Result<Option<String>> {
|
|
if !has_table(conn, "app_metadata").await? {
|
|
return Ok(None);
|
|
}
|
|
|
|
Ok(sqlx::query_scalar!(
|
|
"SELECT value FROM app_metadata WHERE key = 'app_version'"
|
|
)
|
|
.fetch_optional(&mut *conn)
|
|
.await?)
|
|
}
|
|
|
|
async fn has_table(
|
|
conn: &mut SqliteConnection,
|
|
table_name: &str,
|
|
) -> crate::Result<bool> {
|
|
let count = sqlx::query_scalar!(
|
|
"
|
|
SELECT COUNT(*)
|
|
FROM sqlite_master
|
|
WHERE type = 'table' AND name = ?
|
|
",
|
|
table_name,
|
|
)
|
|
.fetch_one(&mut *conn)
|
|
.await?;
|
|
|
|
Ok(count > 0)
|
|
}
|
|
|
|
async fn next_backup_path(
|
|
backup_dir: &Path,
|
|
stored_version: &str,
|
|
current_version: &str,
|
|
) -> crate::Result<PathBuf> {
|
|
crate::util::io::create_dir_all(backup_dir).await?;
|
|
|
|
let stored_version = sanitize_version_for_filename(stored_version);
|
|
let current_version = sanitize_version_for_filename(current_version);
|
|
|
|
let backup_path = backup_dir.join(format!(
|
|
"app-db-before-{current_version}-from-{stored_version}.db"
|
|
));
|
|
if !backup_path.try_exists()? {
|
|
return Ok(backup_path);
|
|
}
|
|
|
|
for suffix in 2.. {
|
|
let backup_path = backup_dir.join(format!(
|
|
"app-db-before-{current_version}-from-{stored_version}-{suffix}.db"
|
|
));
|
|
if !backup_path.try_exists()? {
|
|
return Ok(backup_path);
|
|
}
|
|
}
|
|
|
|
unreachable!()
|
|
}
|
|
|
|
fn sanitize_version_for_filename(version: &str) -> String {
|
|
let mut sanitized = String::new();
|
|
let mut replaced_last_char = false;
|
|
|
|
for character in version.chars() {
|
|
if character.is_ascii_alphanumeric()
|
|
|| character == '.'
|
|
|| character == '-'
|
|
|| character == '_'
|
|
{
|
|
sanitized.push(character);
|
|
replaced_last_char = false;
|
|
} else if !replaced_last_char {
|
|
sanitized.push('-');
|
|
replaced_last_char = true;
|
|
}
|
|
}
|
|
|
|
let sanitized = sanitized.trim_matches(&['.', '-', '_'][..]);
|
|
if sanitized.is_empty() {
|
|
"unknown".to_string()
|
|
} else {
|
|
sanitized.to_string()
|
|
}
|
|
}
|
|
|
|
async fn create_sqlite_snapshot(
|
|
conn: &mut SqliteConnection,
|
|
backup_path: &Path,
|
|
) -> crate::Result<()> {
|
|
let backup_path = backup_path
|
|
.to_str()
|
|
.ok_or_else(|| crate::ErrorKind::UTFError(backup_path.to_path_buf()))?;
|
|
|
|
sqlx::query!("VACUUM INTO ?", backup_path)
|
|
.execute(&mut *conn)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|