存储 URL 到文件

序言

在前两篇文章中,URL 以及短链的 ID 都是存储在内存中的,程序重启后,已经绑定好的 URL 和 ID 就都丢失了。本章介绍如何将短链的信息存储到文件中,并在程序启动时加载进内存中。

写入文件

在之前的文章中,url_to_id是一个动态谓词,可以将其简单地理解为一个既可以根据Url查询出Id、也可以根据Id查询出Url的双向哈希表。为了可以实现将短链信息存储到文件中,将会把url_to_id修改为一个预先定义好的谓词。

url_to_id仍然接受UrlId两个参数,但这两个参数在实际使用时,不会同时都有值、总有一个是变量:

  • 如果Url是字符串、Id是整数,那么实现的是将新的 URL 存储起来的功能;
  • 如果Url是变量、Id是整数,那么它会实例化Url,这样实现的是根据 ID 查找原始 URL 的功能。

用于存储短链信息的文件格式为 CSV,第一列为 ID,第二列为原始 URL,而两者之间通过逗号分隔。这样的一个短链服务的代码可能是下面这样子的

:- initialization(main, main).

:- use_module(library(http/http_dispatch), [http_404/2, http_handler/3, http_redirect/3]).
:- use_module(library(http/http_parameters), [http_parameters/2]).
:- use_module(library(http/http_server), [http_server/1]).
:- use_module(library(lists), [nth0/3]).
:- use_module(library(readutil), [read_file_to_string/3]).

find_url_by_id(_, [], _) :- fail.
find_url_by_id(Id, [Line|Lines], Url) :-
    split_string(Line, ",", "", SubStrings),  % 将一行内容按照逗号切割,放到 SubStrings 中。
    nth0(0, SubStrings, IdString),  % 取出第一列的数据,放到 IdString 中。
    (  format(string(IdString), "~d", [Id])  % 检查将 Id 转换为字符串的话,是否与 IdString 相同。
    -> nth0(1, SubStrings, Url)  % 如果相同,说明第二列的数据就是所需要的链接,放入变量 Url。
    ;  find_url_by_id(Id, Lines, Url)).  % 否则,递归处理 Lines,直至为空、查询失败。

% 读写文件来管理短链。
url_to_id(Url, Id) :-
    string(Url),
    integer(Id),
    open("/tmp/url.csv", append, Stream),
    format(string(Line), '~d,~s~n', [Id, Url]),
    write(Stream, Line),
    close(Stream).
url_to_id(Url, Id) :-
    read_file_to_string("/tmp/url.csv", String, []),
    split_string(String, "\n", "", Lines),
    find_url_by_id(Id, Lines, Url).

% 辅助函数。
reply_by_id(Id, Request) :-
    (  url_to_id(Url, Id)
    -> http_redirect(moved, Url, Request)
    ;  http_404([], Request)).

lengthen_url(Request) :-
    http_parameters(Request, [
        id(Id, [integer])
    ]),
    reply_by_id(Id, Request).

shorten_url(Request) :-
    http_parameters(Request, [
        url(Url, [string])  % 因为 url_to_id 中有类型要求。
    ]),
    % 姑且用当前时间戳来作为 ID。
    get_time(TimeStamp),
    Id is truncate(TimeStamp),
    url_to_id(Url, Id),
    format('Content-Type: application/json~n~n'),
    format('{"id": ~d}', [Id]).

main(_) :-
    http_handler('/api/lengthen', lengthen_url, [methods([get])]),
    http_handler('/api/shorten', shorten_url, [methods([post])]),
    http_server([port(8082)]),
    sleep(100000000).

确保文件流关闭

url_to_id的定义中,并没有机制可以确保close一定会被调用来关闭文件流。要实现类似 Python 中的with open、或者 Common Lisp 中的with-open-file的效果的话,可以用setup_call_cleanup。因此,url_to_id的第一个分支可以修改为

url_to_id(Url, Id) :-
    string(Url),
    integer(Id),
    setup_call_cleanup(
        open("/tmp/url.csv", append, Stream), 
        (
            format(string(Line), '~d,~s~n', [Id, Url]),
            write(Stream, Line)
        ),
        close(Stream)
    ).

分隔源文件

到目前为止,这个 HTTP 服务的源文件已经有 60 行左右了,而且其中的谓词url_to_id也是一个相对独立的功能——它与处理 HTTP 请求并没有太大关系,因此很适合将其剥离到另一个源文件中。新的文件结构将会是这样

/
|- http_server.pl
`- url_to_id.pl

其中,在文件url_to_id.pl中,使用指令:- module定义出一个模块,并将其中的谓词url_to_id/2暴露出来、供http_server.pl使用,后者则使用指令:- use_module来加载这个自定义模块。最终url_to_id.plhttp_server.pl的内容分别如下

:- module(url_to_id, [url_to_id/2]).

:- use_module(library(lists), [nth0/3]).
:- use_module(library(readutil), [read_file_to_string/3]).

find_url_by_id(_, [], _) :- fail.
find_url_by_id(Id, [Line|Lines], Url) :-
    split_string(Line, ",", "", SubStrings),  % 将一行内容按照逗号切割,放到 SubStrings 中。
    nth0(0, SubStrings, IdString),  % 取出第一列的数据,放到 IdString 中。
    (  format(string(IdString), "~d", [Id])  % 检查将 Id 转换为字符串的话,是否与 IdString 相同。
    -> nth0(1, SubStrings, Url)  % 如果相同,说明第二列的数据就是所需要的链接,放入变量 Url。
    ;  find_url_by_id(Id, Lines, Url)).  % 否则,递归处理 Lines,直至为空、查询失败。

% 读写文件来管理短链。
url_to_id(Url, Id) :-
    string(Url),
    integer(Id),
    setup_call_cleanup(
        open("/tmp/url.csv", append, Stream), 
        (
            format(string(Line), '~d,~s~n', [Id, Url]),
            write(Stream, Line)
        ),
        close(Stream)
    ).
url_to_id(Url, Id) :-
    read_file_to_string("/tmp/url.csv", String, []),
    split_string(String, "\n", "", Lines),
    find_url_by_id(Id, Lines, Url).
:- initialization(main, main).

:- use_module(library(http/http_dispatch), [http_404/2, http_handler/3, http_redirect/3]).
:- use_module(library(http/http_parameters), [http_parameters/2]).
:- use_module(library(http/http_server), [http_server/1]).
:- use_module(url_to_id, [url_to_id/2]).

% 辅助函数。
reply_by_id(Id, Request) :-
    (  url_to_id(Url, Id)
    -> http_redirect(moved, Url, Request)
    ;  http_404([], Request)).

lengthen_url(Request) :-
    http_parameters(Request, [
        id(Id, [integer])
    ]),
    reply_by_id(Id, Request).

shorten_url(Request) :-
    http_parameters(Request, [
        url(Url, [string])  % 因为 url_to_id 中有类型要求。
    ]),
    % 姑且用当前时间戳来作为 ID。
    get_time(TimeStamp),
    Id is truncate(TimeStamp),
    url_to_id(Url, Id),
    format('Content-Type: application/json~n~n'),
    format('{"id": ~d}', [Id]).

main(_) :-
    http_handler('/api/lengthen', lengthen_url, [methods([get])]),
    http_handler('/api/shorten', shorten_url, [methods([post])]),
    http_server([port(8082)]),
    sleep(100000000).