%% @doc WebDAV file storage. Can put, get and stream files. %% Uses a job queue which is regulated by "jobs". %% @author Marc Worrell %% @copyright 2022 Marc Worrell %% @end %% Copyright 2022 Marc Worrell %% %% This file is provided to you under the Apache License, %% Version 2.0 (the "License"); you may not use this file %% except in compliance with the License. You may obtain %% a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, %% software distributed under the License is distributed on an %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY %% KIND, either express or implied. See the License for the %% specific language governing permissions and limitations %% under the License. %% -module(webdavfilez). -export([ queue_get/3, queue_get_id/4, queue_put/3, queue_put/4, queue_put/5, queue_put_id/5, queue_delete/2, queue_delete/3, queue_delete_id/4, queue_stream/3, queue_stream_id/4, get/2, delete/2, put/3, put/4, stream/3, create_bucket/2, create_bucket/3 ]). -export([ put_body_file/1 ]). -define(BLOCK_SIZE, 65536). -type config() :: {Username::binary() | string(), Password::binary() | string()}. -type url() :: binary() | string(). -type ready_fun() :: undefined | {atom(),atom(),list()} | fun() | pid(). -type stream_fun() :: {atom(),atom(),list()} | fun() | pid(). -type put_data() :: {data, binary()} | {filename, non_neg_integer(), file:filename_all()} | {filename, file:filename_all()}. -type queue_reply() :: {ok, any(), pid()} | {error, {already_started, pid()}}. -type sync_reply() :: ok | {error, enoent | forbidden | http_code()}. -type http_code() :: 100..600. -type put_opts() :: [ put_opt() ]. -type put_opt() :: {acl, acl_type()} | {content_type, string()}. -type acl_type() :: private | public_read | public_read_write | authenticated_read | bucket_owner_read | bucket_owner_full_control. -export_type([ config/0, url/0, ready_fun/0, stream_fun/0, put_data/0, queue_reply/0, sync_reply/0, http_code/0, put_opts/0, put_opt/0, acl_type/0 ]). %% @doc Queue a file dowloader and call ready_fun when finished. -spec queue_get(config(), url(), ready_fun()) -> queue_reply(). queue_get(Config, Url, ReadyFun) -> webdavfilez_jobs_sup:queue({get, Config, map_url(Url), ReadyFun}). %% @doc Queue a named file dowloader and call ready_fun when finished. %% Names must be unique, duplicates are refused with {error, {already_started, _}}. -spec queue_get_id(any(), config(), url(), ready_fun()) -> queue_reply(). queue_get_id(JobId, Config, Url, ReadyFun) -> webdavfilez_jobs_sup:queue(JobId, {get, Config, map_url(Url), ReadyFun}). %% @doc Queue a file uploader. The data can be a binary or a filename. -spec queue_put(config(), url(), put_data()) -> queue_reply(). queue_put(Config, Url, What) -> queue_put(Config, map_url(Url), What, undefined). %% @doc Queue a file uploader and call ready_fun when finished. -spec queue_put(config(), url(), put_data(), ready_fun()) -> queue_reply(). queue_put(Config, Url, What, ReadyFun) -> queue_put(Config, map_url(Url), What, ReadyFun, []). %% @doc Queue a file uploader and call ready_fun when finished. Options include %% the acl setting and content_type for the file. -spec queue_put(config(), url(), put_data(), ready_fun(), put_opts()) -> queue_reply(). queue_put(Config, Url, What, ReadyFun, Opts) -> webdavfilez_jobs_sup:queue({put, Config, map_url(Url), What, ReadyFun, Opts}). %% @doc Start a named file uploader. Names must be unique, duplicates are refused with %% {error, {already_started, _}}. -spec queue_put_id(any(), config(), url(), put_data(), ready_fun()) -> queue_reply(). queue_put_id(JobId, Config, Url, What, ReadyFun) -> webdavfilez_jobs_sup:queue(JobId, {put, Config, map_url(Url), What, ReadyFun}). %% @doc Async delete a file on WebDAV -spec queue_delete(config(), url()) -> queue_reply(). queue_delete(Config, Url) -> queue_delete(Config, map_url(Url), undefined). %% @doc Async delete a file on WebDAV, call ready_fun when ready. -spec queue_delete(config(), url(), ready_fun()) -> queue_reply(). queue_delete(Config, Url, ReadyFun) -> webdavfilez_jobs_sup:queue({delete, Config, map_url(Url), ReadyFun}). %% @doc Queue a named file deletion process, call ready_fun when ready. -spec queue_delete_id(any(), config(), url(), ready_fun()) -> queue_reply(). queue_delete_id(JobId, Config, Url, ReadyFun) -> webdavfilez_jobs_sup:queue(JobId, {delete, Config, map_url(Url), ReadyFun}). %% @doc Queue a file downloader that will stream chunks to the given stream_fun. The %% default block size for the chunks is 64KB. -spec queue_stream(config(), url(), stream_fun()) -> queue_reply(). queue_stream(Config, Url, StreamFun) -> webdavfilez_jobs_sup:queue({stream, Config, map_url(Url), StreamFun}). %% @doc Queue a named file downloader that will stream chunks to the given stream_fun. The %% default block size for the chunks is 64KB. -spec queue_stream_id(any(), config(), url(), stream_fun()) -> queue_reply(). queue_stream_id(JobId, Config, Url, StreamFun) -> webdavfilez_jobs_sup:queue(JobId, {stream, Config, map_url(Url), StreamFun}). %%% Normal API - blocking on the process %% @doc Fetch the data at the url. -spec get( config(), url() ) -> {ok, ContentType::binary(), Data::binary()} | {error, enoent | forbidden | http_code()}. get(Config, Url) -> Result = jobs:run( webdavfilez_jobs, fun() -> webdavfilez_request:request(Config, get, map_url(Url), [], []) end), case Result of {ok, {{_Http, 200, _Ok}, Headers, Body}} -> {ok, webdavfilez_request:ct(Headers), Body}; Other -> ret_status(Other) end. %% @doc Delete the file at the url. -spec delete( config(), url() ) -> sync_reply(). delete(Config, Url) -> ret_status(jobs:run( webdavfilez_jobs, fun() -> webdavfilez_request:request(Config, delete, map_url(Url), [], []) end)). %% @doc Put a binary or file to the given url. -spec put( config(), url(), put_data() ) -> sync_reply(). put(Config, Url, Payload) -> put(Config, Url, Payload, []). %% @doc Put a binary or file to the given url. Set options for acl and/or content_type. -spec put( config(), url(), put_data(), put_opts() ) -> sync_reply(). put(Config, Url, {data, Data}, Opts) -> Hs = opts_to_headers(Opts), ret_status(webdavfilez_request:request_with_body(Config, put, Url, Hs, Data)); put(Config, Url, {filename, Filename}, Opts) -> Size = filelib:file_size(Filename), put(Config, Url, {filename, Size, Filename}, Opts); put(Config, Url0, {filename, Size, Filename}, Opts) -> Url = map_url(Url0), Hs = [ {"Content-Length", integer_to_list(Size)} | opts_to_headers(Opts) ], Ret = ret_status(webdavfilez_request:request_with_body(Config, put, Url, Hs, {fun ?MODULE:put_body_file/1, {file, Filename}})), case Ret of ok -> ok; {error, 409} -> % Directory might not exist case webdavfilez_mkdir:parent_dir(Url) of {ok, UrlParent} -> case webdavfilez_mkdir:mkdir(Config, UrlParent) of ok -> % Retry ret_status(webdavfilez_request:request_with_body(Config, put, Url, Hs, {fun ?MODULE:put_body_file/1, {file, Filename}})); {error, _} = Error -> Error end; {error, _} -> {error, 409} end; {error, 405} -> {error, epath}; {error, _} = Error -> Error end. put_body_file({file, Filename}) -> {ok, FD} = file:open(Filename, [read,binary]), put_body_file({fd, FD}); put_body_file({fd, FD}) -> case file:read(FD, ?BLOCK_SIZE) of eof -> file:close(FD), eof; {ok, Data} -> {ok, Data, {fd, FD}} end. %% @doc Create a directory (bucket) at the URL. -spec create_bucket( config(), url() ) -> sync_reply(). create_bucket(Config, Url) -> create_bucket(Config, Url, []). %% @doc Create a directory (bucket) at the URL, ignore acl options. -spec create_bucket( config(), url(), put_opts() ) -> sync_reply(). create_bucket(Config, Url, _Opts) -> webdavfilez_mkdir:mkdir(Config, map_url(Url)). opts_to_headers(Opts) -> Hs = lists:foldl( fun({acl, _AclOption}, Hs) -> % Ignore S3 ACL options Hs; ({content_type, CT}, Hs) -> [{"Content-Type", CT} | Hs]; (Unknown, _) -> throw({error, {unknown_option, Unknown}}) end, [], Opts), case proplists:get_value("Content-Type", Hs) of undefined -> [{"Content-Type", "binary/octet-stream"} | Hs]; _ -> Hs end. %%% Stream the contents of the url to the function, callback or to the httpc-streaming option. -spec stream( config(), url(), stream_fun() ) -> sync_reply(). stream(Config, Url, Fun) when is_function(Fun,1) -> webdavfilez_request:stream_to_fun(Config, map_url(Url), Fun); stream(Config, Url, {_M,_F,_A} = MFA) -> webdavfilez_request:stream_to_fun(Config, map_url(Url), MFA); stream(Config, Url, Pid) when is_pid(Pid) -> webdavfilez_request:stream_to_fun(Config, map_url(Url), Pid). ret_status({ok, Rest}) -> webdavfilez_request:http_status(Rest); ret_status({error, _} = Error) -> Error. map_url(<<"webdavs:", Rest/binary>>) -> <<"https:", Rest/binary>>; map_url(<<"webdav:", Rest/binary>>) -> <<"http:", Rest/binary>>; map_url(<<"davs:", Rest/binary>>) -> <<"https:", Rest/binary>>; map_url(<<"dav:", Rest/binary>>) -> <<"http:", Rest/binary>>; map_url("webdavs:" ++ Rest) -> "https:" ++ Rest; map_url("webdav:" ++ Rest) -> "http:" ++ Rest; map_url("davs:" ++ Rest) -> "https:" ++ Rest; map_url("dav:" ++ Rest) -> "http:" ++ Rest; map_url(Url) -> Url.