(* $Id: ftp_fs.ml 1615 2011-06-09 23:33:05Z gerd $ *)
open Printf
open Ftp_client
class type ftp_stream_fs =
object
inherit Netfs.stream_fs
method ftp_client : Ftp_client.ftp_client
method last_ftp_state : Ftp_client.ftp_state
method close : unit -> unit
end
let ftp_syn =
Hashtbl.find Neturl.common_url_syntax "ftp"
let ftp_schemes =
let ht = Hashtbl.create 5 in
Hashtbl.add ht "ftp" ftp_syn;
ht
class ftp_fs ?(config_client = fun _ -> ())
?tmp_directory
?tmp_prefix
?(get_password = fun () -> "")
?(get_account = fun () -> "")
?(keep_open = false)
base_url_s : ftp_stream_fs =
(* parse base_url: *)
let base_url =
Neturl.parse_url
~schemes:ftp_schemes
~accept_8bits:true
(Neturl.fixup_url_string base_url_s) in
(* create client and log in: *)
let ftp = new Ftp_client.ftp_client () in
let () = config_client ftp in
let last_ftp_state = ref None in
let uerror code path detail =
raise(Unix.Unix_error(code, detail, path)) in
let einval path detail =
raise(Unix.Unix_error(Unix.EINVAL, detail, path)) in
let enosys path detail =
raise(Unix.Unix_error(Unix.ENOSYS, detail, path)) in
let translate_error err path detail =
match err with
| FTP_method_perm_failure(550,_) ->
(* This can mean a lot. ENOENT is only the most frequent reason *)
uerror Unix.ENOENT path (detail ^ " [code 550]")
| FTP_method_perm_failure((500|502) as code,_) ->
enosys path (detail ^ "[ code " ^ string_of_int code ^ "]")
| FTP_method_perm_failure(553, _) ->
uerror Unix.EINVAL path (detail ^ " [code 553]")
| FTP_method_temp_failure(450, _) ->
uerror Unix.EPERM path (detail ^ " [code 450]")
| FTP_method_temp_failure(452, _) ->
uerror Unix.ENOSPC path (detail ^ " [code 452]")
| FTP_method_perm_failure(code,_) ->
uerror Unix.EPERM path (detail ^ " [code " ^ string_of_int code ^ "]")
| FTP_method_temp_failure(code,_) ->
uerror Unix.EPERM path (detail ^ " [code " ^ string_of_int code ^ "]")
| _ ->
err
in
let transaction f path detail =
try
let connected =
try (ftp # pi # ftp_state).ftp_connected
with Failure _ -> false in
if not connected then (
ftp # exec (Ftp_client.connect_method
~host:(Neturl.url_host base_url)
?port:(try Some(Neturl.url_port base_url)
with Not_found -> None)
()
);
ftp # exec (Ftp_client.login_method
~user:(try Neturl.url_user base_url
with Not_found -> "anonymous")
~get_password
~get_account
()
);
);
let r = f() in
last_ftp_state := Some(ftp # pi # ftp_state);
if not keep_open then
ftp # exec (Ftp_client.quit_method());
r
with
| error ->
last_ftp_state := Some(ftp # pi # ftp_state);
if not keep_open then
ftp # reset();
raise (translate_error error path detail) in
let (supports_tvfs, supports_utf8) =
transaction
(fun () ->
( try
ftp # exec (Ftp_client.feat_method())
with
| FTP_method_perm_failure _ -> ()
);
(ftp # pi # supports_tvfs,
ftp # pi # supports_utf8
)
) "" "Ftp_fs" in
let path_encoding =
if supports_utf8 then Some `Enc_utf8 else None in
let translate path =
(* Check path. Also prepend the path from the base URL. The returned
path does not start with /. If just the root dir is means this function
returns the empty string.
*)
if path = "" then
einval path "Ftp_fs: path is empty";
if path.[0] <> '/' then
einval path "Ftp_fs: path is not absolute";
if String.contains path '\000' then
einval path "Ftp_fs: path contains NUL byte";
( match path_encoding with
| None -> ()
| Some pe ->
( try
Netconversion.verify pe path
with
| Netconversion.Malformed_code_at _ ->
einval path "Ftp_fs: path is not properly encoded"
)
);
let npath = Neturl.norm_path(Neturl.split_path path) in
( match npath with
| "" :: ".." :: _ -> (* CHECK: maybe ENOENT? *)
einval path "Ftp_fs: path starts with /.."
| _ -> ()
);
let base_path =
match Neturl.url_path base_url with
| [] -> [ "" ]
| p -> p in
let base_path_n = (* no slash at the end, no at the beginning *)
if List.hd (List.rev base_path) = "" && base_path <> [""] then
List.tl (List.rev (List.tl (List.rev base_path)))
else
List.tl base_path in
let path_trans =
base_path_n @ (List.tl npath) in
let p =
String.concat "/" path_trans in
(* `TVFS "" does not work for accessing the home dir, so switch
to `NVFS in this case
*)
if supports_tvfs && p <> "" then `TVFS p else `NVFS p in
object(self)
method path_encoding = path_encoding
method path_exclusions = [0,0; 47,47]
method nominal_dot_dot = true
method ftp_client = ftp
method last_ftp_state =
match !last_ftp_state with
| None ->
raise Not_found
| Some st ->
st
method close() =
ftp # reset()
method read flags path =
last_ftp_state := None;
let vfs = translate path in
let representation =
if List.mem `Binary flags then `Image else `ASCII None in
let cur_tmp = ref None in
let cleanup() =
match !cur_tmp with
| None -> ()
| Some (tmp_name,inch,outch) ->
(* Success *)
close_in inch;
close_out outch;
( try Unix.unlink tmp_name with _ -> ()); (* CHECK Win32 *)
cur_tmp := None in
try
transaction
(fun () ->
ftp # exec (Ftp_client.get_method
~file:vfs
~representation
~store:(fun _ ->
let (tmp_name, inch, outch) =
Netchannels.make_temporary_file
?tmp_directory ?tmp_prefix () in
cur_tmp := Some (tmp_name, inch, outch);
let obj_outch =
new Netchannels.output_channel outch in
`File_structure obj_outch
)
()
);
) path "Ftp_fs.read";
match !cur_tmp with
| None ->
assert false
| Some (tmp_name,inch,outch) ->
(* Success *)
let skip =
try
Http_fs.find_flag (function `Skip p -> Some p | _ -> None) flags
with Not_found -> 0L in
LargeFile.seek_in inch skip;
new Netchannels.input_channel
~onclose:cleanup
inch
with error ->
cleanup();
raise error
method write flags path =
last_ftp_state := None;
let vfs = translate path in
let representation =
if List.mem `Binary flags then `Image else `ASCII None in
let create_flag = List.mem `Create flags in
let trunc_flag = List.mem `Truncate flags in
let excl_flag = List.mem `Exclusive flags in
if not create_flag && not trunc_flag then
einval path "Ftp_fs.write: you need to request either file creation \
or file truncation";
if create_flag && excl_flag then
einval path "Ftp_fs.write: exclusive file creation not supported";
let req =
if create_flag && not excl_flag && not trunc_flag then
Some false
else
if not create_flag then
Some true
else
None in
let cur_tmp = ref None in
let (tmp_name, inch, outch) =
Netchannels.make_temporary_file
?tmp_directory ?tmp_prefix () in
cur_tmp := Some (tmp_name, inch, outch);
let do_write() =
transaction
(fun () ->
close_out outch;
try
( match req with
| None -> ()
| Some r_exists ->
let exists =
try
ftp # exec (Ftp_client.mlst_method
~file:vfs
~process_result:(fun _ -> ())
()
);
true
with
| FTP_method_perm_failure(550,_) -> false in
if exists <> r_exists then
let ecode =
if r_exists then Unix.ENOENT else Unix.EEXIST in
raise(uerror ecode path "Ftp_fs.write");
);
ftp # exec (Ftp_client.put_method
~file:vfs
~representation
~store:(fun _ ->
let obj_inch =
new Netchannels.input_channel inch in
`File_structure obj_inch
)
()
);
close_in inch;
( try Unix.unlink tmp_name with _ -> () )
with
| error ->
close_in inch;
( try Unix.unlink tmp_name with _ -> () );
raise error
)
path
"Ftp_fs.write"
in
let obj_outch =
new Netchannels.output_channel
~onclose:do_write
outch in
obj_outch
method size flags path =
last_ftp_state := None;
let vfs = translate path in
transaction
(fun () ->
let n = ref 0L in
ftp # exec (Ftp_client.size_method
~file:vfs
~representation:`Image
~process_result:(fun k -> n := k)
());
!n
)
path
"Ftp_fs.size"
method test flags path typ =
List.hd (self # test_list flags path [typ])
method test_list flags path typl =
last_ftp_state := None;
let vfs = translate path in
transaction
(fun () ->
let entries = ref [] in
( try
ftp # exec (Ftp_client.mlst_method
~file:vfs
~process_result:(fun e -> entries := e)
());
with
| FTP_method_perm_failure _
| FTP_method_temp_failure _ -> ()
);
List.map
(fun typ ->
match typ with
| `N -> !entries <> []
| `E -> !entries <> []
| `D -> (List.exists
(fun e ->
try get_type e = `Dir with Not_found -> false)
!entries
)
| `F -> (List.exists
(fun e ->
try get_type e = `File with Not_found -> false)
!entries
)
| `H -> false
| `R -> (List.exists
(fun e ->
let p = try get_perm e with Not_found -> [] in
List.mem `List p || List.mem `Read p
)
!entries
)
| `W -> (List.exists
(fun e ->
let p = try get_perm e with Not_found -> [] in
List.mem `Mkdir p || List.mem `Delete_member p ||
List.mem `Write p
)
!entries
)
| `X -> (List.exists
(fun e ->
let p = try get_perm e with Not_found -> [] in
List.mem `Enter p
)
!entries
)
| `S -> (List.exists
(fun e ->
let s = try get_size e with Not_found -> 0L in
s > 0L
)
!entries
)
)
typl
)
path
"Ftp_fs.test_list"
method remove flags path =
last_ftp_state := None;
if List.mem `Recursive flags then
einval path "Ftp_fs.remove: recursion not supported";
let vfs = translate path in
transaction
(fun () ->
ftp # exec (Ftp_client.delete_method vfs)
)
path
"Ftp_fs.remove"
method rename flags path1 path2 =
last_ftp_state := None;
let vfs1 = translate path1 in
let vfs2 = translate path2 in
transaction
(fun () ->
ftp # exec (Ftp_client.rename_method ~file_from:vfs1 ~file_to:vfs2 ())
)
path1
"Ftp_fs.rename"
method readdir flags path =
last_ftp_state := None;
let vfs = translate path in
transaction
(fun () ->
let b = Buffer.create 500 in
let ch = new Netchannels.output_buffer b in
ftp#exec (Ftp_client.nlst_method
~dir:vfs
~representation:(`ASCII None)
~store:(fun _ -> `File_structure ch)
()
);
List.map
Filename.basename
(parse_nlst_document (Buffer.contents b))
)
path
"Ftp_fs.readdir"
method mkdir flags path =
(* FIXME: flags *)
last_ftp_state := None;
let vfs = translate path in
transaction
(fun () ->
ftp # exec (Ftp_client.mkdir_method vfs)
)
path
"Ftp_fs.mkdir"
method rmdir flags path =
last_ftp_state := None;
let vfs = translate path in
transaction
(fun () ->
ftp # exec (Ftp_client.rmdir_method vfs)
)
path
"Ftp_fs.rmdir"
(* Unsupported *)
method symlink _ path1 path2 = enosys path1 "Ftp_fs.symlink not supported"
method copy flags path1 path2 = enosys path1 "Ftp_fs.copy not supported"
method readlink flags path = enosys path "Ftp_fs.readlink not supported"
end
let ftp_fs = new ftp_fs