/*  Part of SWISH

    Author:        Jan Wielemaker
    E-mail:        J.Wielemaker@vu.nl
    WWW:           http://www.swi-prolog.org
    Copyright (c)  2018, VU University Amsterdam
		         CWI, Amsterdam
    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions
    are met:

    1. Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.

    2. Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in
       the documentation and/or other materials provided with the
       distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.
*/

:- module(download,
	  [ download_button/2			% +Data, +Options
	  ]).
:- use_module(library(pengines)).
:- use_module(library(option)).
:- use_module(library(settings)).
:- use_module(library(apply)).
:- use_module(library(http/mimetype)).
:- use_module(library(http/http_dispatch)).
:- use_module(library(http/http_parameters)).

/** <module> Provide a button for downloading data

This module allows a button to be  inserted into the Pengine output that
allows for downloading data. Originally this   used the `data` type URL.
This has been disabled in recent   browsers. Also considering the length
limitations on URLs on some browsers we   now store the data server-side
and make the link simply download the  data.   The  data  is kept on the
server for `keep_downloads_time` seconds, default 24 hours.
*/

:- setting(keep_downloads_time, number, 86400,
	   "Seconds to keep a downloaded file").

%!	download_button(+Data:string, +Options)
%
%	Emit a button in the SWISH   output window for downloading Data.
%	The provided data  is  stored  on   the  server.
%
%	Options:
%
%	  - filename(+Name)
%	    (Base-)Name of the file created (default:
%	    `swish-download.dat`),
%	  - content_type(+Type)
%	    Full content type.  By default this is derived from the
%	    extension of the filename and the encoding.
%	  - encoding(+Enc)
%	    Encoding to use. One of `utf8` or `octet`. default is
%	    `utf8`.
%
%	@see https://en.wikipedia.org/wiki/Data_URI_scheme

download_button(Data, Options) :-
	option(filename(FileName), Options, 'swish-download.dat'),
	option(encoding(Enc), Options, utf8),
	(   option(content_type(ContentType), Options)
	->  true
	;   file_mime_type(FileName, Major/Minor),
	    atomics_to_string([Major, Minor], /, ContentType0),
	    add_charset(Enc, ContentType0, ContentType)
	),
	save_download_data(Data, UUID, Enc),
	pengine_output(
	    json{action:downloadButton,
		 content_type:ContentType,
		 encoding: Enc,
		 uuid:UUID,
		 filename:FileName
		}).

add_charset(utf8, Enc0, Enc) :- !,
	atom_concat(Enc0, '; charset=UTF-8', Enc).
add_charset(_, Enc, Enc).


		 /*******************************
		 *	      SERVER		*
		 *******************************/

:- http_handler(swish(download), download, [id(download), prefix, method(get)]).

%!	download(+Request)
%
%	Handle a download request.

download(Request) :-
	http_parameters(Request,
			[ uuid(UUID, []),
			  content_type(Type, [])
			]),
	download_file(UUID, File),
	http_reply_file(File,
			[ mime_type(Type),
			  unsafe(true)
			],
			Request).


		 /*******************************
		 *	       STORE		*
		 *******************************/

%!	save_download_data(+Data, -UUID, +Encoding) is det.
%
%	Save the string Data in the download store and return a UUID to
%	retreive it.

save_download_data(Data, UUID, Encoding) :-
	download_file(UUID, Path),
	ensure_parents(Path),
	setup_call_cleanup(
	    open(Path, write, Out, [encoding(Encoding)]),
	    write(Out, Data),
	    close(Out)),
	prune_downloads.


%!	download_file(?UUID, -Path)
%
%	Path is the full file from which to download Name.
%
%	@tbd We could use the SHA1 of the  data. In that case we need to
%	_touch_ the file if it exists and we   need  a way to ensure the
%	file is completely saved by a   concurrent  thread that may save
%	the same file.

download_file(UUID, Path) :-
	(   var(UUID)
	->  uuid(UUID)
	;   true
	),
	variant_sha1(UUID, SHA1),
	sub_atom(SHA1, 0, 2, _, Dir0),
	sub_atom(SHA1, 2, 2, _, Dir1),
	sub_atom(SHA1, 4, _, 0, File),
	download_dir(Dir),
	atomic_list_concat([Dir, Dir0, Dir1, File], /, Path).


%!	download_dir(-Dir) is det.
%
%	Find the download base directory.

:- dynamic download_dir_cache/1.
:- volatile download_dir_cache/1.

download_dir(Dir) :-
	download_dir_cache(Dir),
	!.
download_dir(Dir) :-
	absolute_file_name(data(download), Dir,
			   [ file_type(directory),
			     access(write),
			     file_errors(fail)
			   ]),
	!,
	asserta(download_dir_cache(Dir)).
download_dir(Dir) :-
	absolute_file_name(data(download), Dir,
			   [ solutions(all)
			   ]),
	catch(make_directory(Dir), error(_,_), fail),
	!,
	asserta(download_dir_cache(Dir)).

ensure_parents(Path) :-
	file_directory_name(Path, Dir1),
	file_directory_name(Dir1, Dir0),
	ensure_directory(Dir0),
	ensure_directory(Dir1).

ensure_directory(Dir) :-
	exists_directory(Dir),
	!.
ensure_directory(Dir) :-
	make_directory(Dir).


%!	prune_downloads
%
%	Prune old download files. This is actually executed every 1/4th
%	of the time we keep the files.  This makes this call fast.

:- dynamic pruned_at/1.
:- volatile pruned_at/1.

prune_downloads :-
	E = error(_,_),
	with_mutex(download,
		   catch(prune_downloads_sync, E,
			 print_message(warning, E))).

prune_downloads_sync :-
	pruned_at(Last),
	setting(keep_downloads_time, Time),
	get_time(Now),
	Now < Last + Time/4,
	!.
prune_downloads_sync :-
	thread_create(do_prune_downloads, _,
		      [ alias(prune_downloads),
			detached(true)
		      ]),
	get_time(Now),
	retractall(pruned_at(_)),
	asserta(pruned_at(Now)).

do_prune_downloads :-
	get_time(Now),
	setting(keep_downloads_time, Time),
	Before is Now - Time,
	download_dir(Dir),
	prune_dir(Dir, Before, false).

%!	prune_dir(+Dir, +Time, +PruneDir) is det.
%
%	Find all files older than Time and  delete them as well as empty
%	directories.

prune_dir(Dir, Time, PruneDir) :-
	directory_files(Dir, Files0),
	exclude(reserved, Files0, Files),
	exclude(clean_entry(Dir, Time), Files, Rest),
	(   Rest == [],
	    PruneDir == true
	->  E = error(_,_),
	    catch(delete_directory(Dir), E,
		  print_message(warning, E))
	;   true
	).

reserved(.).
reserved(..).

%!	clean_entry(+Dir, +Time, +File) is semidet.
%
%	True when Dir/File has been cleaned and is removed.

clean_entry(Dir, Time, File) :-
	directory_file_path(Dir, File, Path),
	(   exists_directory(Path)
	->  prune_dir(Path, Time, true),
	    \+ exists_directory(Path)
	;   time_file(Path, FTime),
	    FTime < Time
	->  E = error(_,_),
	    catch(delete_file(Path), E,
		  ( print_message(warning, E),
		    fail
		  ))
	).