Skip to content

Latest commit

 

History

History
494 lines (411 loc) · 15.2 KB

File metadata and controls

494 lines (411 loc) · 15.2 KB

[logs 1]

[startup idea]

[title]: Blazing Fast URL Shortener. [techstack]: Made using some of the fastest frameworks and tools, including Rust, Actix, Diesel, Shuttle, PostgreSQL, Inferno, and Typescript. [short_description]: I am building url shortener using Rust, Actix-web, Diesel, Shuttle.dev and shared Database from Shuttle.dev PostgreSQL. [backend]: Three main features of this project are:

  1. I am using Diesel for database operations.
  2. I am using Rust and Actix-web for backend.
  3. I am using Shuttle.dev for shared database and deployment.

[TODO]:

  1. integrate Diesel with Actix-web.
  2. create database schema.
  3. make a functions for database operations.
  4. create routes for url shortener.
  5. create a function to generate random 6 digit string. For example: 3d4f5g. https://example.com/3d4f5g.
  6. create a function to redirect to original url.
  7. make sure there is no duplicate url in database.
  8. separate the functions in different files. make sure they are properly organized for efficient code management.
  9. create a functions to handle any errors.

[How it works]: it's really simply. user enters the long url, it can be linkedin post, twitter post, or instagram reel, whatever. our platform will shorten the url and provide a new url: https://example.com/3d4f5g. "3d4f5g" this is the random 6 digit string. it's unique for each url (e.g., no duplicate urls). when user enters the new url in browser, it will redirect to the original url.

[requirements] make sure Database operations are pretty efficient /or enough efficient. for example , 1) when we shorten the url, or 2) checking for duplicate urls, or 3) redirecting to original url. separate the tasks, works, functions in different files. make sure they are properly organized for efficient code management. make sure the code is clean and readable.

If you have any questions. refer to the above provided: " fresh and updated knowledge base on those tech stacks and tools. ".

[project structure]: my current project structure is:

  • src/
    • main.rs: ``` fn main() { println!("Hello, world!"); }
    - lib.rs: ```
        // Bring our module into scope
        mod keys;
    
        // Re-export the things you want to share
        pub use keys::get_pg_url;
    
    • keys.rs: ``` use std::env; use dotenv::dotenv; /// Load tokens from the .env file/environment variable. pub fn get_pg_url() -> String { dotenv().ok(); // Initializes .env reading (ignore any load errors). env::var("DATABASE_URL").expect("DATABASE_URL environment variable not set") }
    
    
  • .env: ``` DATABASE_URL
- Cargo.toml: ```
    [package]
    name = "notlong"
    version = "0.1.0"
    edition = "2021"

    [dependencies]
    actix-web = "4"
    reqwest = { version = "0.12.12", features = ["json"] }
    serde = { version = "1", features = ["derive"] }
    serde_json = "1"
    tokio = { version = "1", features = ["full"] }
    dotenv = "0.15"
    chrono = "0.4"

Go ahead. Good Luck!

PS: If you can't produce in one breath. and tokens are limited do the following: Ask me follow up questions. for example: " write a function to generate random 6 digit string. For example: 3d4f5g. https://example.com/3d4f5g. ".

[follow up]

1. Database Migrations & Schema:

  • I am using Diesel CLI to run migrations.
  • Single table enough for now.
  • Yes, please add created_at.

2. Uniqueness Handling:

  • i want Both: 1) Prevent the same original_url from having multiple short codes and
  • Prevent the same random short code from ever being generated twice.

3. Actix-Web Configuration

  • I prefer to keep your routes in a dedicated routes.rs (or multiple files like routes/create_short_url.rs, routes/redirect.rs, etc.).

4. Shuttle.dev Deployment:

  • I am planning to configure your ShuttleActixWeb service in main.rs.
  • The use shuttle deploy to deploy it. [Here's an example]:
use actix_files::Files;
use actix_web::web::ServiceConfig;
use shuttle_actix_web::ShuttleActixWeb;

#[shuttle_runtime::main]
async fn main() -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
    let config = move |cfg: &mut ServiceConfig| {
        cfg.service(Files::new("/", "assets"));
    };

    Ok(config.into())
}

5. Generating the Random Code:

  • Use whatever technique that makes retrieving the data from databse comparing duplicates and etc. things pretty efficient.

6. Error Handling:

  • Yes, you can use Actix-Web’s default error responses (e.g., using HttpResponse::InternalServerError(), etc.).

7. Crate Versions

  • Yes, of course i am okey!
  • Use dotenvy: Modern Alternative: dotenvy is a newer library that aims to provide enhanced features and performance over the original dotenv.

Go ahead. Good Luck!

[follow up]

Next Steps:

  1. Encrypt stored URLs to enhance security and protect user data. Use any promising Rust encryption library. Make sure it's efficient also. Because we're making blazingly fast url shortener ever. Make sure only encrypt the original URL. Make sure call encrupt funtion only if encrypted=true on the table. Alter the table to add a new column encrypted with default value false. On redirect check if encrypted=true then decrypt the URL and redirect to the original URL. Find all the codes below.

[// src/db/models.rs]:

// src/db/models.rs
use diesel::prelude::*;
use chrono::NaiveDateTime;
use crate::db::schema::short_urls;
use crate::db::schema::connected_wallets;

// The main struct that mirrors the DB table.
// (The `Selectable` derive requires Diesel >= 2.0, it helps with typed queries)
#[derive(Queryable, Selectable, Debug)]
#[derive(serde::Serialize)]
#[diesel(table_name = short_urls)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct ShortUrl {
    pub id: i32,
    pub original_url: String,
    pub short_code: String,
    #[diesel(sql_type = diesel::sql_types::Timestamp)]
    pub created_at: NaiveDateTime,
}

// For inserting new rows
#[derive(Insertable)]
#[diesel(table_name = short_urls)]
pub struct NewShortUrl<'a> {
    pub original_url: &'a str,
    pub short_code: &'a str,
}

[// src/routes/redirect.rs]:

// src/routes/redirect.rs
use actix_web::{get, web, HttpResponse, http::header};
use crate::db::operations::find_by_short_code;
use crate::Pool;

#[get("/{short_code}")]
pub async fn redirect_short(
    pool: web::Data<Pool>,
    path: web::Path<String>,
) -> actix_web::Result<HttpResponse> {
    let code = path.into_inner();

    let mut conn = pool.get().map_err(|e| {
        actix_web::error::ErrorInternalServerError(format!("DB pool error: {e}"))
    })?;

    let result = find_by_short_code(&mut conn, &code);
    match result {
        Ok(record) => {
            // 301/302 redirect to the original_url
            Ok(HttpResponse::Found()
                .append_header((header::LOCATION, record.original_url))
                .finish())
        }
        Err(_) => Ok(HttpResponse::NotFound().body("Short URL not found.")),
    }
}

[// src/db/operations.rs]:

// src/db/operations.rs
use diesel::prelude::*;
use diesel::result::Error as DieselError;
use diesel::dsl::sql;

use crate::db::models::{ShortUrl, NewShortUrl};
use crate::db::schema::short_urls::dsl::*;

use crate::db::schema::connected_wallets::dsl::*;
use crate::db::models::{ConnectedWallet, NewConnectedWallet};

// short urls
pub fn find_by_short_code(conn: &mut PgConnection, code: &str) -> Result<ShortUrl, DieselError> {
    short_urls
        .filter(short_code.eq(code))
        .select(ShortUrl::as_select())
        .first(conn)
        
}
pub fn find_by_original_url(conn: &mut PgConnection, url: &str) -> Result<ShortUrl, DieselError> {
    short_urls
        .filter(original_url.eq(url))
        .select(ShortUrl::as_select())
        .first(conn)
}

pub fn insert_short_url(
    conn: &mut PgConnection,
    new_short: NewShortUrl
) -> Result<ShortUrl, DieselError> {
    diesel::insert_into(short_urls)
        .values(&new_short)
        .get_result::<ShortUrl>(conn)
}

[// src/routes/create_short_url.rs]:

// src/routes/create_short_url.rs

use actix_web::{post, web, HttpResponse};
use serde::Deserialize;

use crate::db::operations::{find_by_original_url, insert_short_url};
use crate::db::models::NewShortUrl;
use crate::utils::random_short_code; // We'll define a "utils" mod for random code
use crate::Pool; // We'll define a type alias for DB pool in main or lib

#[derive(Deserialize)]
pub struct ShortenRequest {
    pub original_url: String,
}

#[post("/shorten")]
pub async fn create_short_url(
    pool: web::Data<Pool>,
    req: web::Json<ShortenRequest>,
) -> actix_web::Result<HttpResponse> {
    let mut conn = pool.get().map_err(|e| {
        actix_web::error::ErrorInternalServerError(format!("DB pool error: {e}"))
    })?;

    let original_url = req.original_url.trim();

    // 1. Check if original_url exists
    if let Ok(existing) = find_by_original_url(&mut conn, original_url) {
        // If found, just return the existing short_code
        return Ok(HttpResponse::Ok().json(existing));
    }

    // 2. Otherwise, generate short_code
    let code = random_short_code(6);

    // 3. Insert new record into DB
    let new_short = NewShortUrl {
        original_url,
        short_code: &code,
    };

    match insert_short_url(&mut conn, new_short) {
        Ok(saved) => Ok(HttpResponse::Ok().json(saved)),
        Err(e) => {
            // If there's a duplicate key error, you could keep trying or return an error
            Err(actix_web::error::ErrorBadRequest(e.to_string()))
        }
    }
}

[follow up]

Next Steps:

Expiration Dates:

  1. Enable URLs to expire after 24 hours.
  2. we need to add a new column expired with default value false.
  3. expired should automatically set to true after 24 hours. (ps: we can use created_at for this case).
  4. when any user enters the short url in browser, it should check if expired is true then redirect to the main page.

[// src/routes/redirect.rs]:

// src/routes/redirect.rs
use actix_web::{get, web, HttpResponse, http::header};
use crate::db::operations::find_by_short_code;
use crate::Pool;
use crate::utils::crypto::decrypt_url;

#[get("/{short_code}")]
pub async fn redirect_short(
    pool: web::Data<Pool>,
    path: web::Path<String>,
) -> actix_web::Result<HttpResponse> {
    let code = path.into_inner();

    let mut conn = pool.get().map_err(|e| {
        actix_web::error::ErrorInternalServerError(format!("DB pool error: {e}"))
    })?;

    let result = find_by_short_code(&mut conn, &code);
    match result {
        Ok(record) => {
            // 2) If record is encrypted, we decrypt
            let target_url = if record.encrypted {
                match decrypt_url(&record.original_url) {
                    Ok(decrypted) => decrypted,
                    Err(_) => {
                        // If decrypt fails, you can decide how to respond
                        return Ok(HttpResponse::BadRequest().body("Invalid encrypted data"));
                    }
                }
            } else {
                record.original_url
            };
            // 301/302 redirect to the original_url
            Ok(HttpResponse::Found()
                .append_header((header::LOCATION, target_url))
                .finish())
        }
        Err(_) => Ok(HttpResponse::NotFound().body("Short URL not found.")),
    }
}

[// src/routes/create_short_url.rs]:

// src/routes/create_short_url.rs

use actix_web::{post, web, HttpResponse};
use serde::Deserialize;

use crate::db::operations::{find_by_original_url, insert_short_url};
use crate::db::models::NewShortUrl;
use crate::utils::rcode::random_short_code; // We'll define a "utils" mod for random code
use crate::Pool; // We'll define a type alias for DB pool in main or lib

#[derive(Deserialize)]
pub struct ShortenRequest {
    pub original_url: String,
    pub encrypt: bool,
}

#[post("/shorten")]
pub async fn create_short_url(
    pool: web::Data<Pool>,
    req: web::Json<ShortenRequest>,
) -> actix_web::Result<HttpResponse> {
    let mut conn = pool.get().map_err(|e| {
        actix_web::error::ErrorInternalServerError(format!("DB pool error: {e}"))
    })?;

    let original_url = req.original_url.trim();

    // 1. Check if original_url exists (in plain or cipher?)
    //    * If your logic expects them to match the plain text,
    //      you might do find_by_original_url(...) only if encrypt == false
    //    * or skip the check if you want unique encrypted vs non-encrypted
    if !req.encrypt {
        if let Ok(existing) = find_by_original_url(&mut conn, original_url) {
            // If found, just return the existing
            return Ok(HttpResponse::Ok().json(existing));
        }
    }

    // 2. Otherwise, generate short_code
    let code = random_short_code(6);

    // 3. Insert new record into DB
    let mut new_short = NewShortUrl {
        original_url,
        short_code: &code,
        encrypted: req.encrypt,
    };

    match insert_short_url(&mut conn, &mut new_short) {
        Ok(saved) => Ok(HttpResponse::Ok().json(saved)),
        Err(e) => {
            // If there's a duplicate key error, you could keep trying or return an error
            Err(actix_web::error::ErrorBadRequest(e.to_string()))
        }
    }
}

[// src/db/models.rs]:

// src/db/models.rs
use diesel::prelude::*;
use chrono::NaiveDateTime;
use crate::db::schema::short_urls;
use crate::db::schema::connected_wallets;

// The main struct that mirrors the DB table.
// (The `Selectable` derive requires Diesel >= 2.0, it helps with typed queries)
#[derive(Queryable, Selectable, Debug)]
#[derive(serde::Serialize)]
#[diesel(table_name = short_urls)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct ShortUrl {
    pub id: i32,
    pub original_url: String,
    pub short_code: String,
    #[diesel(sql_type = diesel::sql_types::Timestamp)]
    pub created_at: NaiveDateTime,
    pub encrypted: bool
}

// For inserting new rows
#[derive(Insertable)]
#[diesel(table_name = short_urls)]
pub struct NewShortUrl<'a> {
    pub original_url: &'a str,
    pub short_code: &'a str,
    pub encrypted: bool,
}

[// src/db/operations.rs]:

// src/db/operations.rs
use diesel::prelude::*;
use diesel::result::Error as DieselError;
use diesel::dsl::sql;

use crate::db::models::{ShortUrl, NewShortUrl};
use crate::db::schema::short_urls::dsl::*;

use crate::db::schema::connected_wallets::dsl::*;
use crate::db::models::{ConnectedWallet, NewConnectedWallet};

use crate::utils::crypto::encrypt_url;

// short urls
pub fn find_by_short_code(conn: &mut PgConnection, code: &str) -> Result<ShortUrl, DieselError> {
    short_urls
        .filter(short_code.eq(code))
        .select(ShortUrl::as_select())
        .first(conn)
        
}
pub fn find_by_original_url(conn: &mut PgConnection, url: &str) -> Result<ShortUrl, DieselError> {
    short_urls
        .filter(original_url.eq(url))
        .select(ShortUrl::as_select())
        .first(conn)
}

pub fn insert_short_url(
    conn: &mut PgConnection,
    new_short: &mut NewShortUrl
) -> Result<ShortUrl, DieselError> {
    // If the caller wants encryption
    if new_short.encrypted {
        if let Ok(cipher_b64) = encrypt_url(new_short.original_url) {
            new_short.original_url = Box::leak(cipher_b64.into_boxed_str());
        }
    }
    diesel::insert_into(short_urls)
        .values(&*new_short)
        .get_result::<ShortUrl>(conn)
}