new functions and fixes

This commit is contained in:
Mateusz Gruszczyński
2026-08-05 22:13:04 +02:00
parent 9fc32d0d31
commit e842b26978
34 changed files with 2505 additions and 115 deletions
+123 -6
View File
@@ -130,7 +130,7 @@ pub(super) fn content_references_file(content: &str, filename: &str, url: &str)
if content.contains(url) {
return true;
}
for marker in ["[file=", "[image=", "[img="] {
for marker in ["[file=", "[image=", "[img=", "[video="] {
let mut remaining = content;
while let Some(index) = remaining.find(marker) {
let after = &remaining[index + marker.len()..];
@@ -513,17 +513,38 @@ async fn require_upload_permission(
.ok_or_else(|| ApiError::forbidden("File uploads are disabled for guests."))
}
#[derive(Debug, Default, Deserialize)]
pub struct FileDownloadQuery {
#[serde(default)]
download: Option<String>,
}
impl FileDownloadQuery {
fn force_download(&self) -> bool {
self.download.as_deref().is_some_and(|value| {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "download"
)
})
}
}
pub async fn download_file(
State(state): State<SharedState>,
headers: HeaderMap,
Path((token, filename)): Path<(String, String)>,
Query(query): Query<FileDownloadQuery>,
) -> Result<Response, ApiError> {
serve_token_file(&state, &token, &filename).await
serve_token_file(&state, &headers, &token, &filename, query.force_download()).await
}
async fn serve_token_file(
state: &SharedState,
headers: &HeaderMap,
token: &str,
filename: &str,
force_download: bool,
) -> Result<Response, ApiError> {
let safe = sanitize_filename(filename);
if safe != filename {
@@ -543,15 +564,44 @@ async fn serve_token_file(
.get_local_with_legacy(&key, &legacy_key)
.await
.map_err(|_| ApiError::not_found_file())?;
let total_len = bytes.len();
let guessed_mime = mime_guess::from_path(&safe).first_or_octet_stream();
let inline_image = is_safe_inline_image_mime(guessed_mime.essence_str());
let served_mime = if inline_image {
let safe_inline = is_safe_inline_image_mime(guessed_mime.essence_str())
|| is_safe_inline_video_mime(guessed_mime.essence_str());
let served_mime = if safe_inline {
guessed_mime.as_ref()
} else {
"application/octet-stream"
};
let disposition = if inline_image { "inline" } else { "attachment" };
let mut response = bytes.into_response();
let disposition = if safe_inline && !force_download {
"inline"
} else {
"attachment"
};
let requested_range = headers
.get(header::RANGE)
.and_then(|value| value.to_str().ok());
let range = match requested_range {
Some(value) => match parse_byte_range(value, total_len) {
Ok(range) => range,
Err(()) => return Ok(range_not_satisfiable(total_len)),
},
None => None,
};
let (body, status, content_range) = if let Some((start, end)) = range {
(
bytes.slice(start..end),
StatusCode::PARTIAL_CONTENT,
Some(format!("bytes {start}-{}/{}", end - 1, total_len)),
)
} else {
(bytes, StatusCode::OK, None)
};
let body_len = body.len();
let mut response = body.into_response();
*response.status_mut() = status;
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_str(served_mime)
@@ -562,6 +612,20 @@ async fn serve_token_file(
HeaderValue::from_str(&format!("{disposition}; filename=\"{safe}\""))
.expect("sanitized attachment filename"),
);
response.headers_mut().insert(
header::ACCEPT_RANGES,
HeaderValue::from_static("bytes"),
);
response.headers_mut().insert(
header::CONTENT_LENGTH,
HeaderValue::from_str(&body_len.to_string()).expect("valid content length"),
);
if let Some(content_range) = content_range {
response.headers_mut().insert(
header::CONTENT_RANGE,
HeaderValue::from_str(&content_range).expect("valid content range"),
);
}
response.headers_mut().insert(
HeaderName::from_static("content-security-policy"),
HeaderValue::from_static("default-src 'none'; sandbox"),
@@ -584,6 +648,52 @@ async fn serve_token_file(
Ok(response)
}
fn range_not_satisfiable(total_len: usize) -> Response {
let mut response = StatusCode::RANGE_NOT_SATISFIABLE.into_response();
response.headers_mut().insert(
header::CONTENT_RANGE,
HeaderValue::from_str(&format!("bytes */{total_len}")).expect("valid content range"),
);
response.headers_mut().insert(
header::ACCEPT_RANGES,
HeaderValue::from_static("bytes"),
);
response
}
fn parse_byte_range(value: &str, total_len: usize) -> Result<Option<(usize, usize)>, ()> {
if total_len == 0 {
return Err(());
}
let range = value.trim().strip_prefix("bytes=").ok_or(())?;
if range.contains(',') {
return Err(());
}
let (start, end) = range.split_once('-').ok_or(())?;
if start.is_empty() {
let suffix_len = end.parse::<usize>().map_err(|_| ())?;
if suffix_len == 0 {
return Err(());
}
let start = total_len.saturating_sub(suffix_len);
return Ok(Some((start, total_len)));
}
let start = start.parse::<usize>().map_err(|_| ())?;
if start >= total_len {
return Err(());
}
let end_inclusive = if end.is_empty() {
total_len - 1
} else {
end.parse::<usize>().map_err(|_| ())?.min(total_len - 1)
};
if end_inclusive < start {
return Err(());
}
Ok(Some((start, end_inclusive + 1)))
}
fn is_safe_inline_image_mime(value: &str) -> bool {
matches!(
value,
@@ -597,6 +707,13 @@ fn is_safe_inline_image_mime(value: &str) -> bool {
)
}
fn is_safe_inline_video_mime(value: &str) -> bool {
matches!(
value,
"video/mp4" | "video/webm" | "video/ogg" | "video/quicktime" | "video/x-m4v"
)
}
fn sanitize_filename(value: &str) -> String {
let name = std::path::Path::new(value)
.file_name()
+8
View File
@@ -432,6 +432,7 @@ pub struct NoteInfo {
editor_line_numbers: bool,
preview_line_numbers: bool,
line_links: bool,
toolbar_collapsed: bool,
font_family: String,
font_size: i64,
personal_editor_settings: bool,
@@ -461,6 +462,8 @@ pub struct EditorSettingsRequest {
#[serde(default)]
line_links: Option<bool>,
#[serde(default)]
toolbar_collapsed: Option<bool>,
#[serde(default)]
font_family: Option<String>,
#[serde(default)]
font_size: Option<i64>,
@@ -502,6 +505,7 @@ async fn save_editor_settings(
|| payload.editor_line_numbers.is_some()
|| payload.preview_line_numbers.is_some()
|| payload.line_links.is_some()
|| payload.toolbar_collapsed.is_some()
|| payload.font_family.is_some()
|| payload.font_size.is_some();
let wants_global_update = payload.authorship_mode.is_some() || payload.colors_enabled.is_some();
@@ -553,6 +557,9 @@ async fn save_editor_settings(
if let Some(value) = payload.line_links {
preferences.line_links = value;
}
if let Some(value) = payload.toolbar_collapsed {
preferences.toolbar_collapsed = value;
}
if let Some(value) = payload.font_family {
preferences.font_family = match value.as_str() {
"mono" | "system" | "serif" | "arial" | "georgia" => value,
@@ -1084,6 +1091,7 @@ pub async fn note_info(
editor_line_numbers: editor_preferences.editor_line_numbers,
preview_line_numbers: editor_preferences.preview_line_numbers,
line_links: editor_preferences.line_links,
toolbar_collapsed: editor_preferences.toolbar_collapsed,
font_family: editor_preferences.font_family,
font_size: editor_preferences.font_size,
personal_editor_settings,
+2
View File
@@ -54,6 +54,7 @@ pub struct PadInfo {
editor_line_numbers: bool,
preview_line_numbers: bool,
line_links: bool,
toolbar_collapsed: bool,
font_family: String,
font_size: i64,
personal_editor_settings: bool,
@@ -172,6 +173,7 @@ pub async fn pad_info(
editor_line_numbers: editor_preferences.editor_line_numbers,
preview_line_numbers: editor_preferences.preview_line_numbers,
line_links: editor_preferences.line_links,
toolbar_collapsed: editor_preferences.toolbar_collapsed,
font_family: editor_preferences.font_family,
font_size: editor_preferences.font_size,
personal_editor_settings,
+21 -7
View File
@@ -38,6 +38,7 @@ const MODULES: &[&str] = &[
"toast",
"theme",
"url-state",
"vendor-libs",
"security",
];
@@ -51,10 +52,20 @@ pub fn render_html(
entrypoint: &str,
) -> Response {
let urls = AssetUrls::new(asset_version);
let frontend_config = frontend_config(frontend_log_level, upload_max_size_bytes, external_auth);
let frontend_config = frontend_config(
frontend_log_level,
upload_max_size_bytes,
external_auth,
asset_version,
);
let app_stylesheets = format!(
"{}{}",
urls.stylesheet("styles"),
urls.stylesheet_path("libs/rustpad-player/player.css"),
);
let html = template
.replace("__APP_THEME_BOOTSTRAP__", theme_bootstrap())
.replace("__APP_STYLESHEET__", &urls.stylesheet("styles"))
.replace("__APP_STYLESHEET__", &app_stylesheets)
.replace("__APP_IMPORT_MAP__", &urls.import_map())
.replace("__APP_ENTRYPOINT__", &urls.entrypoint(entrypoint))
.replace(
@@ -92,12 +103,14 @@ fn frontend_config(
frontend_log_level: &str,
upload_max_size_bytes: usize,
external_auth: bool,
asset_version: &str,
) -> String {
format!(
r#"<script>window.__RUSTPAD_CONFIG__=Object.freeze({{frontendLogLevel:"{}",uploadMaxSizeBytes:{},externalAuth:{}}});</script>"#,
r#"<script>window.__RUSTPAD_CONFIG__=Object.freeze({{frontendLogLevel:"{}",uploadMaxSizeBytes:{},externalAuth:{},assetVersion:"{}"}});</script>"#,
escape_js_string(frontend_log_level),
upload_max_size_bytes,
external_auth,
escape_js_string(asset_version),
)
}
@@ -115,10 +128,11 @@ impl<'a> AssetUrls<'a> {
}
fn stylesheet(&self, name: &str) -> String {
format!(
r#"<link rel="stylesheet" href="{}">"#,
self.url(&format!("css/{name}.css"))
)
self.stylesheet_path(&format!("css/{name}.css"))
}
fn stylesheet_path(&self, path: &str) -> String {
format!(r#"<link rel="stylesheet" href="{}">"#, self.url(path))
}
fn entrypoint(&self, name: &str) -> String {
+6 -2
View File
@@ -15,6 +15,7 @@ pub struct EditorPreferences {
pub editor_line_numbers: bool,
pub preview_line_numbers: bool,
pub line_links: bool,
pub toolbar_collapsed: bool,
pub font_family: String,
pub font_size: i64,
}
@@ -26,6 +27,7 @@ impl Default for EditorPreferences {
editor_line_numbers: true,
preview_line_numbers: false,
line_links: false,
toolbar_collapsed: false,
font_family: "mono".into(),
font_size: 14,
}
@@ -92,8 +94,9 @@ pub async fn load_editor_preferences(
editor_line_numbers: row.try_get::<i64, _>(1)? != 0,
preview_line_numbers: row.try_get::<i64, _>(2)? != 0,
line_links: row.try_get::<i64, _>(3)? != 0,
font_family: crate::row_decode::text(&row, 4)?,
font_size: row.try_get(5)?,
toolbar_collapsed: row.try_get::<i64, _>(4)? != 0,
font_family: crate::row_decode::text(&row, 5)?,
font_size: row.try_get(6)?,
}))
}
@@ -117,6 +120,7 @@ pub async fn save_editor_configuration(
.bind(preferences.editor_line_numbers)
.bind(preferences.preview_line_numbers)
.bind(preferences.line_links)
.bind(preferences.toolbar_collapsed)
.bind(&preferences.font_family)
.bind(preferences.font_size)
.execute(&mut *tx)
+4 -4
View File
@@ -59,16 +59,16 @@ pub fn get(query: Query) -> &'static str {
r#"DELETE FROM resource_editor_settings WHERE resource_kind = ? AND resource_slug = ?"#
}
Query::EDITOR_PREFERENCES_SELECT_PAD => {
r#"SELECT CAST(CASE WHEN compact_view THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN editor_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN preview_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN line_links THEN 1 ELSE 0 END AS SIGNED), CAST(font_family AS CHAR CHARACTER SET utf8mb4), font_size FROM user_editor_preferences WHERE user_id = ? AND pad_id = ?"#
r#"SELECT CAST(CASE WHEN compact_view THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN editor_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN preview_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN line_links THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END AS SIGNED), CAST(font_family AS CHAR CHARACTER SET utf8mb4), font_size FROM user_editor_preferences WHERE user_id = ? AND pad_id = ?"#
}
Query::EDITOR_PREFERENCES_SELECT_NOTE => {
r#"SELECT CAST(CASE WHEN compact_view THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN editor_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN preview_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN line_links THEN 1 ELSE 0 END AS SIGNED), CAST(font_family AS CHAR CHARACTER SET utf8mb4), font_size FROM user_editor_preferences WHERE user_id = ? AND note_id = ?"#
r#"SELECT CAST(CASE WHEN compact_view THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN editor_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN preview_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN line_links THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END AS SIGNED), CAST(font_family AS CHAR CHARACTER SET utf8mb4), font_size FROM user_editor_preferences WHERE user_id = ? AND note_id = ?"#
}
Query::EDITOR_PREFERENCES_UPSERT_PAD => {
r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, font_family, font_size, updated_at) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE compact_view = VALUES(compact_view), editor_line_numbers = VALUES(editor_line_numbers), preview_line_numbers = VALUES(preview_line_numbers), line_links = VALUES(line_links), font_family = VALUES(font_family), font_size = VALUES(font_size), updated_at = CURRENT_TIMESTAMP"#
r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, font_family, font_size, updated_at) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE compact_view = VALUES(compact_view), editor_line_numbers = VALUES(editor_line_numbers), preview_line_numbers = VALUES(preview_line_numbers), line_links = VALUES(line_links), toolbar_collapsed = VALUES(toolbar_collapsed), font_family = VALUES(font_family), font_size = VALUES(font_size), updated_at = CURRENT_TIMESTAMP"#
}
Query::EDITOR_PREFERENCES_UPSERT_NOTE => {
r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, font_family, font_size, updated_at) VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE compact_view = VALUES(compact_view), editor_line_numbers = VALUES(editor_line_numbers), preview_line_numbers = VALUES(preview_line_numbers), line_links = VALUES(line_links), font_family = VALUES(font_family), font_size = VALUES(font_size), updated_at = CURRENT_TIMESTAMP"#
r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, font_family, font_size, updated_at) VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE compact_view = VALUES(compact_view), editor_line_numbers = VALUES(editor_line_numbers), preview_line_numbers = VALUES(preview_line_numbers), line_links = VALUES(line_links), toolbar_collapsed = VALUES(toolbar_collapsed), font_family = VALUES(font_family), font_size = VALUES(font_size), updated_at = CURRENT_TIMESTAMP"#
}
Query::EDITOR_PREFERENCES_DELETE_BY_USER => {
r#"DELETE FROM user_editor_preferences WHERE user_id = ?"#
+4 -4
View File
@@ -59,16 +59,16 @@ pub fn get(query: Query) -> &'static str {
r#"DELETE FROM resource_editor_settings WHERE resource_kind = $1 AND resource_slug = $2"#
}
Query::EDITOR_PREFERENCES_SELECT_PAD => {
r#"SELECT (CASE WHEN compact_view THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN editor_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN preview_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN line_links THEN 1 ELSE 0 END)::BIGINT, font_family, font_size FROM user_editor_preferences WHERE user_id = $1 AND pad_id = $2"#
r#"SELECT (CASE WHEN compact_view THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN editor_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN preview_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN line_links THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END)::BIGINT, font_family, font_size FROM user_editor_preferences WHERE user_id = $1 AND pad_id = $2"#
}
Query::EDITOR_PREFERENCES_SELECT_NOTE => {
r#"SELECT (CASE WHEN compact_view THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN editor_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN preview_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN line_links THEN 1 ELSE 0 END)::BIGINT, font_family, font_size FROM user_editor_preferences WHERE user_id = $1 AND note_id = $2"#
r#"SELECT (CASE WHEN compact_view THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN editor_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN preview_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN line_links THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END)::BIGINT, font_family, font_size FROM user_editor_preferences WHERE user_id = $1 AND note_id = $2"#
}
Query::EDITOR_PREFERENCES_UPSERT_PAD => {
r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, font_family, font_size, updated_at) VALUES ($1, $2, NULL, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP::text) ON CONFLICT (user_id, pad_id) DO UPDATE SET compact_view = EXCLUDED.compact_view, editor_line_numbers = EXCLUDED.editor_line_numbers, preview_line_numbers = EXCLUDED.preview_line_numbers, line_links = EXCLUDED.line_links, font_family = EXCLUDED.font_family, font_size = EXCLUDED.font_size, updated_at = CURRENT_TIMESTAMP::text"#
r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, font_family, font_size, updated_at) VALUES ($1, $2, NULL, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP::text) ON CONFLICT (user_id, pad_id) DO UPDATE SET compact_view = EXCLUDED.compact_view, editor_line_numbers = EXCLUDED.editor_line_numbers, preview_line_numbers = EXCLUDED.preview_line_numbers, line_links = EXCLUDED.line_links, toolbar_collapsed = EXCLUDED.toolbar_collapsed, font_family = EXCLUDED.font_family, font_size = EXCLUDED.font_size, updated_at = CURRENT_TIMESTAMP::text"#
}
Query::EDITOR_PREFERENCES_UPSERT_NOTE => {
r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, font_family, font_size, updated_at) VALUES ($1, NULL, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP::text) ON CONFLICT (user_id, note_id) DO UPDATE SET compact_view = EXCLUDED.compact_view, editor_line_numbers = EXCLUDED.editor_line_numbers, preview_line_numbers = EXCLUDED.preview_line_numbers, line_links = EXCLUDED.line_links, font_family = EXCLUDED.font_family, font_size = EXCLUDED.font_size, updated_at = CURRENT_TIMESTAMP::text"#
r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, font_family, font_size, updated_at) VALUES ($1, NULL, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP::text) ON CONFLICT (user_id, note_id) DO UPDATE SET compact_view = EXCLUDED.compact_view, editor_line_numbers = EXCLUDED.editor_line_numbers, preview_line_numbers = EXCLUDED.preview_line_numbers, line_links = EXCLUDED.line_links, toolbar_collapsed = EXCLUDED.toolbar_collapsed, font_family = EXCLUDED.font_family, font_size = EXCLUDED.font_size, updated_at = CURRENT_TIMESTAMP::text"#
}
Query::EDITOR_PREFERENCES_DELETE_BY_USER => {
r#"DELETE FROM user_editor_preferences WHERE user_id = $1"#
+4 -4
View File
@@ -59,16 +59,16 @@ pub fn get(query: Query) -> &'static str {
r#"DELETE FROM resource_editor_settings WHERE resource_kind = ? AND resource_slug = ?"#
}
Query::EDITOR_PREFERENCES_SELECT_PAD => {
r#"SELECT CASE WHEN compact_view THEN 1 ELSE 0 END, CASE WHEN editor_line_numbers THEN 1 ELSE 0 END, CASE WHEN preview_line_numbers THEN 1 ELSE 0 END, CASE WHEN line_links THEN 1 ELSE 0 END, font_family, font_size FROM user_editor_preferences WHERE user_id = ? AND pad_id = ?"#
r#"SELECT CASE WHEN compact_view THEN 1 ELSE 0 END, CASE WHEN editor_line_numbers THEN 1 ELSE 0 END, CASE WHEN preview_line_numbers THEN 1 ELSE 0 END, CASE WHEN line_links THEN 1 ELSE 0 END, CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END, font_family, font_size FROM user_editor_preferences WHERE user_id = ? AND pad_id = ?"#
}
Query::EDITOR_PREFERENCES_SELECT_NOTE => {
r#"SELECT CASE WHEN compact_view THEN 1 ELSE 0 END, CASE WHEN editor_line_numbers THEN 1 ELSE 0 END, CASE WHEN preview_line_numbers THEN 1 ELSE 0 END, CASE WHEN line_links THEN 1 ELSE 0 END, font_family, font_size FROM user_editor_preferences WHERE user_id = ? AND note_id = ?"#
r#"SELECT CASE WHEN compact_view THEN 1 ELSE 0 END, CASE WHEN editor_line_numbers THEN 1 ELSE 0 END, CASE WHEN preview_line_numbers THEN 1 ELSE 0 END, CASE WHEN line_links THEN 1 ELSE 0 END, CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END, font_family, font_size FROM user_editor_preferences WHERE user_id = ? AND note_id = ?"#
}
Query::EDITOR_PREFERENCES_UPSERT_PAD => {
r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, font_family, font_size, updated_at) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT (user_id, pad_id) DO UPDATE SET compact_view = excluded.compact_view, editor_line_numbers = excluded.editor_line_numbers, preview_line_numbers = excluded.preview_line_numbers, line_links = excluded.line_links, font_family = excluded.font_family, font_size = excluded.font_size, updated_at = CURRENT_TIMESTAMP"#
r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, font_family, font_size, updated_at) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT (user_id, pad_id) DO UPDATE SET compact_view = excluded.compact_view, editor_line_numbers = excluded.editor_line_numbers, preview_line_numbers = excluded.preview_line_numbers, line_links = excluded.line_links, toolbar_collapsed = excluded.toolbar_collapsed, font_family = excluded.font_family, font_size = excluded.font_size, updated_at = CURRENT_TIMESTAMP"#
}
Query::EDITOR_PREFERENCES_UPSERT_NOTE => {
r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, font_family, font_size, updated_at) VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT (user_id, note_id) DO UPDATE SET compact_view = excluded.compact_view, editor_line_numbers = excluded.editor_line_numbers, preview_line_numbers = excluded.preview_line_numbers, line_links = excluded.line_links, font_family = excluded.font_family, font_size = excluded.font_size, updated_at = CURRENT_TIMESTAMP"#
r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, font_family, font_size, updated_at) VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT (user_id, note_id) DO UPDATE SET compact_view = excluded.compact_view, editor_line_numbers = excluded.editor_line_numbers, preview_line_numbers = excluded.preview_line_numbers, line_links = excluded.line_links, toolbar_collapsed = excluded.toolbar_collapsed, font_family = excluded.font_family, font_size = excluded.font_size, updated_at = CURRENT_TIMESTAMP"#
}
Query::EDITOR_PREFERENCES_DELETE_BY_USER => {
r#"DELETE FROM user_editor_preferences WHERE user_id = ?"#
+31 -1
View File
@@ -7,7 +7,10 @@
* See LICENSE file in repository root for details.
*/
use super::{content_references_file, content_references_stored_file, is_safe_inline_image_mime};
use super::{
content_references_file, content_references_stored_file, is_safe_inline_image_mime,
is_safe_inline_video_mime, parse_byte_range,
};
#[test]
fn only_raster_images_are_inline() {
@@ -48,3 +51,30 @@ fn attachment_references_survive_origin_changes() {
Some("https://new-files.example.com"),
));
}
#[test]
fn common_video_formats_are_inline() {
assert!(is_safe_inline_video_mime("video/mp4"));
assert!(is_safe_inline_video_mime("video/webm"));
assert!(!is_safe_inline_video_mime("text/html"));
assert!(!is_safe_inline_video_mime("application/javascript"));
}
#[test]
fn video_alias_is_still_attached() {
assert!(content_references_file(
"[video=clip.mp4,Product demo]",
"clip.mp4",
"/f/token/clip.mp4",
));
}
#[test]
fn byte_ranges_support_video_seeking() {
assert_eq!(parse_byte_range("bytes=0-99", 1_000), Ok(Some((0, 100))));
assert_eq!(parse_byte_range("bytes=500-", 1_000), Ok(Some((500, 1_000))));
assert_eq!(parse_byte_range("bytes=-100", 1_000), Ok(Some((900, 1_000))));
assert_eq!(parse_byte_range("bytes=900-2000", 1_000), Ok(Some((900, 1_000))));
assert_eq!(parse_byte_range("bytes=1000-", 1_000), Err(()));
assert_eq!(parse_byte_range("bytes=0-1,4-5", 1_000), Err(()));
}
+2 -2
View File
@@ -14,8 +14,8 @@ fn boolean_projections_are_normalized_for_sqlx_any() {
// MySQL BOOLEAN is TINYINT(1), which sqlx::Any 0.8 cannot map directly.
for (query, expected_casts) in [
(Query::RESOURCE_EDITOR_SETTINGS_SELECT, 1),
(Query::EDITOR_PREFERENCES_SELECT_PAD, 4),
(Query::EDITOR_PREFERENCES_SELECT_NOTE, 4),
(Query::EDITOR_PREFERENCES_SELECT_PAD, 5),
(Query::EDITOR_PREFERENCES_SELECT_NOTE, 5),
(Query::AUTH_USER_BY_EXTERNAL_ID, 1),
(Query::AUTH_USER_BY_SESSION, 1),
(Query::AUTH_USER_BY_NICKNAME, 1),