Selaa lähdekoodia

Add CLI action to delete remote file

timvisee 7 vuotta sitten
vanhempi
commit
92f6387422

+ 1 - 2
IDEAS.md

@@ -1,6 +1,4 @@
 # Ideas
-- Endpoints:
-  - delete
 - allow creating non existent directories with the `-f` flag 
 - only allow file extension renaming on upload with `-f` flag
 - no interact flag
@@ -12,6 +10,7 @@
 - Implement error handling everywhere properly
 - Quick upload/download without `upload` or `download` subcommands?
 - Flag to explicitly delete file after download
+- Allow file deletion by consuming all download slots
 - Check remote version and heartbeat using `/__version__`
 - Check whether the file still exists everywhere
 - API actions contain duplicate code, create centralized functions

+ 74 - 0
cli/src/action/delete.rs

@@ -0,0 +1,74 @@
+use ffsend_api::action::delete::{
+    Error as DeleteError,
+    Delete as ApiDelete,
+};
+use ffsend_api::file::remote_file::{
+    FileParseError,
+    RemoteFile,
+};
+use ffsend_api::reqwest::Client;
+
+use cmd::cmd_delete::CmdDelete;
+use error::ActionError;
+use util::print_success;
+
+/// A file delete action.
+pub struct Delete<'a> {
+    cmd: &'a CmdDelete<'a>,
+}
+
+impl<'a> Delete<'a> {
+    /// Construct a new delete action.
+    pub fn new(cmd: &'a CmdDelete<'a>) -> Self {
+        Self {
+            cmd,
+        }
+    }
+
+    /// Invoke the delete action.
+    // TODO: create a trait for this method
+    pub fn invoke(&self) -> Result<(), ActionError> {
+        // Get the share URL
+        let url = self.cmd.url();
+
+        // Create a reqwest client
+        let client = Client::new();
+
+        // Parse the remote file based on the share URL, get the password
+        let file = RemoteFile::parse_url(url, self.cmd.owner())?;
+
+        // TODO: show an informative error if the owner token isn't set
+
+        // Send the file deletion request
+        ApiDelete::new(&file, None).invoke(&client)?;
+
+        // Print a success message
+        print_success("File deleted");
+
+        Ok(())
+    }
+}
+
+#[derive(Debug, Fail)]
+pub enum Error {
+    /// Failed to parse a share URL, it was invalid.
+    /// This error is not related to a specific action.
+    #[fail(display = "Invalid share URL")]
+    InvalidUrl(#[cause] FileParseError),
+
+    /// An error occurred while deleting the remote file.
+    #[fail(display = "Failed to delete the shared file")]
+    Delete(#[cause] DeleteError),
+}
+
+impl From<FileParseError> for Error {
+    fn from(err: FileParseError) -> Error {
+        Error::InvalidUrl(err)
+    }
+}
+
+impl From<DeleteError> for Error {
+    fn from(err: DeleteError) -> Error {
+        Error::Delete(err)
+    }
+}

+ 0 - 1
cli/src/action/info.rs

@@ -105,7 +105,6 @@ pub enum Error {
     Info(#[cause] InfoError),
 
     /// The given Send file has expired, or did never exist in the first place.
-    // TODO: do not return an error, but write to stdout that the file does not exist
     #[fail(display = "The file has expired or did never exist")]
     Expired,
 }

+ 1 - 0
cli/src/action/mod.rs

@@ -1,3 +1,4 @@
+pub mod delete;
 pub mod download;
 pub mod info;
 pub mod params;

+ 81 - 0
cli/src/cmd/cmd_delete.rs

@@ -0,0 +1,81 @@
+use ffsend_api::url::{ParseError, Url};
+
+use super::clap::{App, Arg, ArgMatches, SubCommand};
+
+use util::quit_error_msg;
+
+/// The delete command.
+pub struct CmdDelete<'a> {
+    matches: &'a ArgMatches<'a>,
+}
+
+impl<'a: 'b, 'b> CmdDelete<'a> {
+    /// Build the sub command definition.
+    pub fn build<'y, 'z>() -> App<'y, 'z> {
+        // Build the subcommand
+        let cmd = SubCommand::with_name("delete")
+            .about("Delete a shared file.")
+            .visible_alias("d")
+            .visible_alias("del")
+            .alias("r")
+            .alias("rem")
+            .alias("remove")
+            .arg(Arg::with_name("URL")
+                .help("The share URL")
+                .required(true)
+                .multiple(false))
+            .arg(Arg::with_name("owner")
+                .long("owner")
+                .short("o")
+                .alias("own")
+                .alias("owner-token")
+                .alias("token")
+                .value_name("TOKEN")
+                .help("File owner token"));
+
+        cmd
+    }
+
+    /// Parse CLI arguments, from the given parent command matches.
+    pub fn parse(parent: &'a ArgMatches<'a>) -> Option<CmdDelete<'a>> {
+        parent.subcommand_matches("delete")
+            .map(|matches| CmdDelete { matches })
+    }
+
+    /// Get the owner token.
+    pub fn owner(&'a self) -> Option<String> {
+        // TODO: validate the owner token if set
+        self.matches.value_of("owner")
+            .map(|token| token.to_owned())
+    }
+
+    /// Get the file share URL.
+    ///
+    /// This method parses the URL into an `Url`.
+    /// If the given URL is invalid,
+    /// the program will quit with an error message.
+    pub fn url(&'a self) -> Url {
+        // Get the host
+        let url = self.matches.value_of("URL")
+            .expect("missing URL");
+
+        // Parse the URL
+        // TODO: improve these error messages
+        match Url::parse(url) {
+            Ok(url) => url,
+            Err(ParseError::EmptyHost) =>
+                quit_error_msg("Emtpy host given"),
+            Err(ParseError::InvalidPort) =>
+                quit_error_msg("Invalid host port"),
+            Err(ParseError::InvalidIpv4Address) =>
+                quit_error_msg("Invalid IPv4 address in host"),
+            Err(ParseError::InvalidIpv6Address) =>
+                quit_error_msg("Invalid IPv6 address in host"),
+            Err(ParseError::InvalidDomainCharacter) =>
+                quit_error_msg("Host domains contains an invalid character"),
+            Err(ParseError::RelativeUrlWithoutBase) =>
+                quit_error_msg("Host domain doesn't contain a host"),
+            _ => quit_error_msg("The given host is invalid"),
+        }
+    }
+}

+ 11 - 4
cli/src/cmd/handler.rs

@@ -2,6 +2,7 @@ use super::clap::{App, ArgMatches};
 
 use app::*;
 
+use super::cmd_delete::CmdDelete;
 use super::cmd_download::CmdDownload;
 use super::cmd_info::CmdInfo;
 use super::cmd_params::CmdParams;
@@ -21,11 +22,12 @@ impl<'a: 'b, 'b> Handler<'a> {
             .version(APP_VERSION)
             .author(APP_AUTHOR)
             .about(APP_ABOUT)
-            .subcommand(CmdUpload::build().display_order(1))
+            .subcommand(CmdDelete::build())
             .subcommand(CmdDownload::build().display_order(2))
             .subcommand(CmdInfo::build())
             .subcommand(CmdParams::build())
             .subcommand(CmdPassword::build())
+            .subcommand(CmdUpload::build().display_order(1))
     }
 
     /// Parse CLI arguments.
@@ -36,9 +38,9 @@ impl<'a: 'b, 'b> Handler<'a> {
         }
     }
 
-    /// Get the upload sub command, if matched.
-    pub fn upload(&'a self) -> Option<CmdUpload<'a>> {
-        CmdUpload::parse(&self.matches)
+    /// Get the delete sub command, if matched.
+    pub fn delete(&'a self) -> Option<CmdDelete<'a>> {
+        CmdDelete::parse(&self.matches)
     }
 
     /// Get the download sub command, if matched.
@@ -60,4 +62,9 @@ impl<'a: 'b, 'b> Handler<'a> {
     pub fn password(&'a self) -> Option<CmdPassword<'a>> {
         CmdPassword::parse(&self.matches)
     }
+
+    /// Get the upload sub command, if matched.
+    pub fn upload(&'a self) -> Option<CmdUpload<'a>> {
+        CmdUpload::parse(&self.matches)
+    }
 }

+ 1 - 0
cli/src/cmd/mod.rs

@@ -1,5 +1,6 @@
 extern crate clap;
 
+pub mod cmd_delete;
 pub mod cmd_download;
 pub mod cmd_info;
 pub mod cmd_params;

+ 15 - 4
cli/src/error.rs

@@ -1,3 +1,4 @@
+use ffsend_api::action::delete::Error as DeleteError;
 use ffsend_api::action::download::Error as DownloadError;
 use ffsend_api::action::params::Error as ParamsError;
 use ffsend_api::action::password::Error as PasswordError;
@@ -27,10 +28,9 @@ impl From<ActionError> for Error {
 
 #[derive(Debug, Fail)]
 pub enum ActionError {
-    /// An error occurred while invoking the upload action.
-    // TODO: bind the upload cause here
-    #[fail(display = "Failed to upload the specified file")]
-    Upload(#[cause] UploadError),
+    /// An error occurred while invoking the delete action.
+    #[fail(display = "Failed to delete the file")]
+    Delete(#[cause] DeleteError),
 
     /// An error occurred while invoking the download action.
     #[fail(display = "Failed to download the requested file")]
@@ -48,12 +48,23 @@ pub enum ActionError {
     #[fail(display = "Failed to change the password")]
     Password(#[cause] PasswordError),
 
+    /// An error occurred while invoking the upload action.
+    // TODO: bind the upload cause here
+    #[fail(display = "Failed to upload the specified file")]
+    Upload(#[cause] UploadError),
+
     /// Failed to parse a share URL, it was invalid.
     /// This error is not related to a specific action.
     #[fail(display = "Invalid share URL")]
     InvalidUrl(#[cause] FileParseError),
 }
 
+impl From<DeleteError> for ActionError {
+    fn from(err: DeleteError) -> ActionError {
+        ActionError::Delete(err)
+    }
+}
+
 impl From<DownloadError> for ActionError {
     fn from(err: DownloadError) -> ActionError {
         ActionError::Download(err)

+ 10 - 3
cli/src/main.rs

@@ -11,6 +11,7 @@ mod error;
 mod progress;
 mod util;
 
+use action::delete::Delete;
 use action::download::Download;
 use action::info::Info;
 use action::params::Params;
@@ -36,9 +37,9 @@ fn main() {
 /// If no proper action is selected, the program will quit with an error
 /// message.
 fn invoke_action(handler: &Handler) -> Result<(), Error> {
-    // Match the upload command
-    if let Some(cmd) = handler.upload() {
-        return Upload::new(&cmd).invoke()
+    // Match the delete command
+    if let Some(cmd) = handler.delete() {
+        return Delete::new(&cmd).invoke()
             .map_err(|err| err.into());
     }
 
@@ -66,6 +67,12 @@ fn invoke_action(handler: &Handler) -> Result<(), Error> {
             .map_err(|err| err.into());
     }
 
+    // Match the upload command
+    if let Some(cmd) = handler.upload() {
+        return Upload::new(&cmd).invoke()
+            .map_err(|err| err.into());
+    }
+
     // No subcommand was selected, show general help
     Handler::build()
         .print_help()