Plasma GitLab Archive
Projects Blog Knowledge

(* $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

This web site is published by Informatikbüro Gerd Stolpmann
Powered by Caml