I'm working on modernizing Rosetta Code's infrastructure. Starting with communications. Please accept this time-limited open invite to RC's Slack.. --Michael Mol (talk) 20:59, 30 May 2020 (UTC)

URL shortener

From Rosetta Code
URL shortener is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

The job of a URL shortener is to take a long URL (e.g. "https://www.cockroachlabs.com/docs/stable/build-a-go-app-with-cockroachdb.html") and turn it into something shorter (e.g. "https://bit.ly/2QLUQIu").

A simple URL shortener with no special rules is very simple and consists of 2 endpoints:

  • One to generate a short version of a URL given a long version of a URL.
  • One to handle a call to the short version of a URL and redirect the user to the long (original) version of the URL.


Create a simple URL shortening API with the following endpoints:

POST /

A POST endpoint that accepts a JSON body describing the URL to shorten. Your URL shortener should generate a short version of the URL and keep track of the mapping between short and long URLs. For example:

   $ curl -X POST 'localhost:8080' \
   -H 'Content-Type: application/json' \
   -d '{
       "long": "https://www.cockroachlabs.com/docs/stable/build-a-go-app-with-cockroachdb.html"
   }'

Should returning something similar to:

   http://localhost:8080/9eXmFnuj

GET /:short

A GET endpoint that accepts a short version of the URL in the URL path and redirects the user to the original URL. For example:

   $ curl -L http://localhost:3000/9eXmFnuj

Should redirect the user to the original URL:

   <!DOCTYPE html>
   <html lang="en">
   ...

Rules:

  • Store the short -> long mappings in any way you like. In-memory is fine.
  • There are no auth requirements. Your API can be completely open.

Crystal[edit]

require "kemal"
 
CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".chars
entries = Hash(String, String).new
 
post "/" do |env|
short = Random::Secure.random_bytes(8).map{|b| CHARS[b % CHARS.size]}.join
entries[short] = env.params.json["long"].as(String)
"http://localhost:3000/#{short}"
end
 
get "/:short" do |env|
if long = entries[env.params.url["short"]]?
env.redirect long
else
env.response.status_code = 404
end
end
 
error 404 do
"invalid short url"
end
 
Kemal.run

Delphi[edit]

Library: System.Json
Library: IdContext
Library: IdGlobal
Library: Inifiles

Highly inspired in #Go

 
program URLShortenerServer;
 
{$APPTYPE CONSOLE}
 
uses
System.SysUtils,
System.Classes,
System.Json,
IdHTTPServer,
IdContext,
IdCustomHTTPServer,
IdGlobal,
Inifiles;
 
type
TUrlShortener = class
private
Db: TInifile;
Server: TIdHTTPServer;
function GenerateKey(size: integer): string;
procedure Get(Path: string; AResponseInfo: TIdHTTPResponseInfo);
procedure Post(Url: string; AReqBody: TJSONValue; AResponseInfo: TIdHTTPResponseInfo);
function PostBody(Data: TStream): TJSONValue;
function StoreLongUrl(Url: string): string;
public
procedure DoGet(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo;
AResponseInfo: TIdHTTPResponseInfo);
procedure StartListening;
constructor Create;
destructor Destroy; override;
end;
 
const
Host = 'localhost:8080';
CODE_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
SHORTENER_SEASON = 'URL_SHORTER';
 
{ Manager }
 
function TUrlShortener.GenerateKey(size: integer): string;
var
le, i: integer;
begin
SetLength(Result, size);
le := Length(CODE_CHARS)-1;
for i := 1 to size do
Result[i] := CODE_CHARS[Random(le)+1];
end;
 
procedure TUrlShortener.StartListening;
begin
Server.Active := true;
end;
 
function TUrlShortener.StoreLongUrl(Url: string): string;
begin
repeat
Result := GenerateKey(8);
until not Db.ValueExists(SHORTENER_SEASON, Result);
Db.WriteString(SHORTENER_SEASON, Result, Url);
Db.UpdateFile;
end;
 
procedure TUrlShortener.Get(Path: string; AResponseInfo: TIdHTTPResponseInfo);
var
longUrl: string;
begin
if Db.ValueExists(SHORTENER_SEASON, Path) then
begin
longUrl := Db.ReadString(SHORTENER_SEASON, Path, '');
AResponseInfo.ResponseNo := 302;
AResponseInfo.Redirect(longUrl);
end
else
begin
AResponseInfo.ResponseNo := 404;
writeln(format('No such shortened url: http://%s/%s', [host, Path]));
end;
end;
 
procedure TUrlShortener.Post(Url: string; AReqBody: TJSONValue; AResponseInfo:
TIdHTTPResponseInfo);
var
longUrl, shortUrl: string;
begin
if Assigned(AReqBody) then
begin
longUrl := AReqBody.GetValue<string>('long');
shortUrl := StoreLongUrl(longUrl);
AResponseInfo.ResponseNo := 200;
AResponseInfo.ContentText := Host + '/' + shortUrl;
end
else
AResponseInfo.ResponseNo := 422;
end;
 
function TUrlShortener.PostBody(Data: TStream): TJSONValue;
var
body: string;
begin
Result := nil;
if assigned(Data) then
begin
Data.Position := 0;
body := ReadStringFromStream(Data);
 
result := TJSONObject.Create;
try
result := TJSONObject.ParseJSONValue(body);
except
on E: Exception do
FreeAndNil(Result);
end;
end;
end;
 
constructor TUrlShortener.Create;
begin
Db := TInifile.Create(ChangeFileExt(ParamStr(0), '.db'));
Server := TIdHTTPServer.Create(nil);
Server.DefaultPort := 8080;
Server.OnCommandGet := DoGet;
end;
 
destructor TUrlShortener.Destroy;
begin
Server.Active := false;
Server.Free;
Db.Free;
inherited;
end;
 
procedure TUrlShortener.DoGet(AContext: TIdContext; ARequestInfo:
TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
Path: string;
begin
// Default ResponseNo
AResponseInfo.ResponseNo := 404;
 
Path := ARequestInfo.URI.Replace('/', '', []);
 
case ARequestInfo.CommandType of
hcGET:
Get(Path, AResponseInfo);
hcPOST:
Post(Path, PostBody(ARequestInfo.PostStream), AResponseInfo);
else
Writeln('Unsupprted method: ', ARequestInfo.Command);
end;
end;
 
var
Server: TIdHTTPServer;
Manager: TUrlShortener;
 
begin
Manager := TUrlShortener.Create;
Manager.StartListening;
 
Writeln('Running on ', host);
Writeln('Press ENTER to exit');
readln;
 
Manager.Free;
end.
Output:
Running on localhost:8080
Press ENTER to exit

Go[edit]

// shortener.go
package main
 
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net/http"
"time"
)
 
const (
chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
host = "localhost:8000"
)
 
type database map[string]string
 
type shortener struct {
Long string `json:"long"`
}
 
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodPost: // "POST"
body, err := ioutil.ReadAll(req.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest) // 400
return
}
var sh shortener
err = json.Unmarshal(body, &sh)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity) // 422
return
}
short := generateKey(8)
db[short] = sh.Long
fmt.Fprintf(w, "The shortened URL: http://%s/%s\n", host, short)
case http.MethodGet: // "GET"
path := req.URL.Path[1:]
if v, ok := db[path]; ok {
http.Redirect(w, req, v, http.StatusFound) // 302
} else {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "No such shortened url: http://%s/%s\n", host, path)
}
default:
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "Unsupprted method: %s\n", req.Method)
}
}
 
func generateKey(size int) string {
key := make([]byte, size)
le := len(chars)
for i := 0; i < size; i++ {
key[i] = chars[rand.Intn(le)]
}
return string(key)
}
 
func main() {
rand.Seed(time.Now().UnixNano())
db := make(database)
log.Fatal(http.ListenAndServe(host, db))
}
Output:

Sample output (abbreviated) including building and starting the server from Ubuntu 18.04 terminal and entering a valid and then an invalid shortened URL:

$ go build shortener.go

$ ./shortener &

$ curl -X POST 'localhost:8000' \
>    -H 'Content-Type: application/json' \
>    -d '{
>        "long": "https://www.cockroachlabs.com/docs/stable/build-a-go-app-with-cockroachdb.html"
>    }'
The shortened URL: http://localhost:8000/3DOPwhRu

$ curl -L http://localhost:8000/3DOPwhRu
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

<meta name="description" content="Learn how to use CockroachDB from a simple Go application with the Go pq driver.">

....

</html>

$ curl -L http://localhost:8000/3DOPwhRv
No such shortened url: http://localhost:8000/3DOPwhRv

Julia[edit]

Assumes a SQLite database containing a table called LONGNAMESHORTNAME (consisting of two string columns) already exists.

using Base64, HTTP, JSON2, Sockets, SQLite, SHA
 
function processpost(req::HTTP.Request, urilen=8)
json = JSON2.read(String(HTTP.payload(req)))
if haskey(json, :long)
longname = json.long
encoded, shortname = [UInt8(c) for c in base64encode(sha256(longname))], ""
for i in 0:length(encoded)-1
shortname = String(circshift(encoded, i)[1:urilen])
result = SQLite.Query(dbhandle,
"SELECT LONG FROM LONGNAMESHORTNAME WHERE SHORT = \"" * shortname * "\";")
if isempty(result)
SQLite.Query(dbhandle,
"INSERT INTO LONGNAMESHORTNAME VALUES ('" *
longname * "', '" * shortname * "')")
return HTTP.Response(200, JSON2.write(
"$shortname is short name for $longname."))
end
end
end
HTTP.Response(400, JSON2.write("Bad request. Please POST JSON as { long : longname }"))
end
 
function processget(req::HTTP.Request)
shortname = split(req.target, r"[^\w\d\+\\]+")[end]
result = SQLite.Query(dbhandle, "SELECT LONG FROM LONGNAMESHORTNAME WHERE SHORT = \'" *
shortname * "\' ;")
responsebody = isempty(result) ?
"<!DOCTYPE html><html><head></head><body><h2>Not Found</h2></body></html>" :
"<!DOCTYPE html><html><head></head><body>\n<meta http-equiv=\"refresh\"" *
"content = \"0; url = " * first(result).LONG * " /></body></html>"
return HTTP.Response(200, responsebody)
end
 
function run_web_server(server, portnum)
router = HTTP.Router()
[email protected](router, "POST", "", processpost)
[email protected](router, "GET", "/*", processget)
HTTP.serve(router, server, portnum)
end
 
const dbhandle = SQLite.DB("longshort.db")
const serveraddress = Sockets.localhost
const localport = 3000
run_web_server(serveraddress, localport)
 

PicoLisp[edit]

(load "@lib/http.l")
(allowed NIL "!short" u)
(pool "urls.db" (6))
(de short (R)
(ifn *Post
(redirect (fetch NIL (format R)))
(let K (count)
(dbSync)
(store NIL K (get 'u 'http))
(commit 'upd)
(respond (pack "http://127.0.0.1:8080/?" K "\n")) ) ) )
(server 8080 "!short")
Output:
$ nohup pil url-short.l +
$ curl -F 'u=https://reddit.com' 127.0.0.1:8080
http://127.0.0.1:8080/?0
$ curl -F 'u=https://picolisp.com' 127.0.0.1:8080
http://127.0.0.1:8080/?1
$ curl -v http://127.0.0.1:8080/?1
*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /?1 HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.69.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 303 See Other
< Server: PicoLisp
< Location: https://picolisp.com
< Content-Type: text/html
< Content-Length: 89
< 
<HTML>
<HEAD><TITLE>303 See Other</TITLE></HEAD>
<BODY><H1>See Other</H1></BODY>
</HTML>
* Connection #0 to host 127.0.0.1 left intact
$

Raku[edit]

(formerly Perl 6)

Works with: Rakudo version 2019.11

As there is no requirement to obfuscate the shortened urls, I elected to just use a simple base 36 incremental counter starting at "0" to "shorten" the urls.

Sets up a micro web service on the local computer at port 10000. Run the service, then access it with any web browser at address localhost:10000 . Any saved urls will be displayed with their shortened id. Enter a web address in the text field to assign a shortened id. Append that id to the web address to automatically be redirected to that url. The url of this page is entered as id 0, so the address: " localhost:10000/0 " will redirect to here, to this page.

The next saved address would be accessible at localhost:10000/1 . And so on.

Saves the shortened urls in a local json file called urls.json so saved urls will be available from session to session. No provisions to edit or delete a saved url. If you want to edit the saved urls, edit the urls.json file directly with some third party editor then restart the service.

There is NO security or authentication on this minimal app. Not recommended to run this as-is on a public facing server. It would not be too difficult to add appropriate security, but it isn't a requirement of this task. Very minimal error checking and recovery.

# Persistent URL storage
use JSON::Fast;
 
my $urlfile = './urls.json'.IO;
my %urls = ($urlfile.e and $urlfile.f and $urlfile.s) ??
( $urlfile.slurp.&from-json ) !!
( index => 1, url => { 0 => 'http://rosettacode.org/wiki/URL_shortener#Raku' } );
 
$urlfile.spurt(%urls.&to-json);
 
# Setup parameters
my $host = 'localhost';
my $port = 10000;
 
# Micro HTTP service
use Cro::HTTP::Router;
use Cro::HTTP::Server;
 
my $application = route {
post -> 'add_url' {
redirect :see-other, '/';
request-body -> (:$url) {
%urls<url>{ %urls<index>.base(36) } = $url;
++%urls<index>;
$urlfile.spurt(%urls.&to-json);
}
}
 
get -> {
content 'text/html', qq:to/END/;
<form action="http://$host:$port/add_url" method="post">
URL to add:</br><input type="text" name="url"></br>
<input type="submit" value="Submit"></form></br>
Saved URLs:
<div style="background-color:#eeeeee">
{ %urls<url>.sort( +(*.key.parse-base(36)) ).join: '</br>' }
</div>
END
}
 
get -> $short {
if my $link = %urls<url>{$short} {
redirect :permanent, $link
}
else {
not-found
}
}
}
 
my Cro::Service $shorten = Cro::HTTP::Server.new:
:$host, :$port, :$application;
 
$shorten.start;
 
react whenever signal(SIGINT) { $shorten.stop; exit; }