Add QR codes, link expiry, password protection, previews, audit trail and bulk operations
This commit is contained in:
1 parent
157aa81252
commit
bcbcc90d98
88 files changed
+5354
-815
No files matched your search
@@ -1,6 +1,6 @@
|
||||
use crate::db::Db;
|
||||
use crate::models::ApiKey;
|
||||
use crate::error::AppError;
|
||||
use crate::models::ApiKey;
|
||||
|
||||
pub fn create_api_key(
|
||||
db: &Db,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::db::Db;
|
||||
use crate::models::AuditLog;
|
||||
use crate::error::AppError;
|
||||
use crate::models::AuditLog;
|
||||
|
||||
pub fn log_action(
|
||||
db: &Db,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
use crate::models::Url;
|
||||
use crate::services::qr::{generate_qr_png, generate_qr_svg};
|
||||
use std::io::Write;
|
||||
use zip::write::FileOptions;
|
||||
use zip::ZipWriter;
|
||||
|
||||
/// Export QR codes for the given URLs as a ZIP file.
|
||||
/// `format` can be "png" or "svg".
|
||||
/// `base_url` is used to build the full short URL encoded in the QR.
|
||||
pub fn export_qr_zip(
|
||||
urls: &[Url],
|
||||
format: &str,
|
||||
base_url: &str,
|
||||
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let mut buf = Vec::new();
|
||||
{
|
||||
let mut zip = ZipWriter::new(std::io::Cursor::new(&mut buf));
|
||||
let options =
|
||||
FileOptions::<()>::default().compression_method(zip::CompressionMethod::Deflated);
|
||||
|
||||
let mut csv_content = String::from("code,destination,qr_filename\n");
|
||||
|
||||
for url in urls {
|
||||
let full_url = format!("{}/{}", base_url.trim_end_matches('/'), url.code);
|
||||
let ext = if format == "svg" { "svg" } else { "png" };
|
||||
let filename = format!("{}.{}", url.code, ext);
|
||||
|
||||
let qr_data = if format == "svg" {
|
||||
generate_qr_svg(&full_url)?.into_bytes()
|
||||
} else {
|
||||
generate_qr_png(&full_url, 256)?
|
||||
};
|
||||
|
||||
zip.start_file(&filename, options)?;
|
||||
zip.write_all(&qr_data)?;
|
||||
|
||||
// Escape quotes in destination for CSV formatting
|
||||
let escaped_dest = url.destination.replace('"', "\"\"");
|
||||
csv_content.push_str(&format!("{},\"{}\",{}\n", url.code, escaped_dest, filename));
|
||||
}
|
||||
|
||||
zip.start_file("manifest.csv", options)?;
|
||||
zip.write_all(csv_content.as_bytes())?;
|
||||
|
||||
zip.finish()?;
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::db::Db;
|
||||
use crate::models::LandingPage;
|
||||
use crate::error::AppError;
|
||||
use crate::models::LandingPage;
|
||||
|
||||
pub fn create_landing_page(
|
||||
db: &Db,
|
||||
@@ -11,7 +11,8 @@ pub fn create_landing_page(
|
||||
state: &str,
|
||||
) -> Result<LandingPage, AppError> {
|
||||
let conn = db.content.lock().unwrap();
|
||||
let page = crate::db::content::create_landing_page(&conn, code, slug, title, html_content, state)?;
|
||||
let page =
|
||||
crate::db::content::create_landing_page(&conn, code, slug, title, html_content, state)?;
|
||||
Ok(page)
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -1,4 +1,6 @@
|
||||
pub mod shortener;
|
||||
pub mod landing_pages;
|
||||
pub mod api_keys;
|
||||
pub mod audit;
|
||||
pub mod bulk;
|
||||
pub mod landing_pages;
|
||||
pub mod qr;
|
||||
pub mod shortener;
|
||||
@@ -0,0 +1,58 @@
|
||||
//! QR code generation service.
|
||||
//!
|
||||
//! Generates QR codes on-demand as PNG or SVG. No files are stored on disk.
|
||||
|
||||
use image::Luma;
|
||||
use qrcode::QrCode;
|
||||
use std::io::Cursor;
|
||||
|
||||
/// Generate a QR code as a PNG byte vector.
|
||||
///
|
||||
/// The `url` is encoded into the QR matrix. The `size` parameter controls
|
||||
/// the pixel dimensions of the output image (default: 256).
|
||||
pub fn generate_qr_png(url: &str, size: u32) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let code = QrCode::new(url.as_bytes())?;
|
||||
let image = code.render::<Luma<u8>>().min_dimensions(size, size).build();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut cursor = Cursor::new(&mut buf);
|
||||
image.write_to(&mut cursor, image::ImageFormat::Png)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Generate a QR code as an SVG string.
|
||||
pub fn generate_qr_svg(url: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let code = QrCode::new(url.as_bytes())?;
|
||||
let svg = code
|
||||
.render::<qrcode::render::svg::Color>()
|
||||
.min_dimensions(256, 256)
|
||||
.build();
|
||||
Ok(svg)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_qr_png_generation() {
|
||||
let png = generate_qr_png("https://bzo.in/abc123", 256).unwrap();
|
||||
assert!(!png.is_empty());
|
||||
// PNG magic bytes
|
||||
assert_eq!(&png[..4], &[0x89, b'P', b'N', b'G']);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qr_svg_generation() {
|
||||
let svg = generate_qr_svg("https://bzo.in/abc123").unwrap();
|
||||
assert!(svg.contains("<svg"));
|
||||
assert!(svg.contains("</svg>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qr_long_url() {
|
||||
let long_url = format!("https://example.com/{}", "a".repeat(500));
|
||||
let png = generate_qr_png(&long_url, 512).unwrap();
|
||||
assert!(!png.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::db::Db;
|
||||
use crate::models::Url;
|
||||
use crate::error::AppError;
|
||||
use crate::models::Url;
|
||||
|
||||
pub fn create_url(
|
||||
db: &Db,
|
||||
|
||||
Reference in new issue
Block a user