Repository: Flexget/Flexget

Issue:
sftp_download fails to download file with ';' in the file name
<!---
Before opening an issue, verify:

- Is this a feature request? Post it on https://feathub.com/Flexget/Flexget
- Did you recently upgrade? Look at the Change Log and Upgrade Actions to make sure that you don't need to make any changes to your config https://flexget.com/ChangeLog https://flexget.com/UpgradeActions
- Are you running FlexGet as a daemon? Stop it completely and then start it again https://flexget.com/CLI/daemon
- Did you search to see if the issue already exists? https://github.com/Flexget/Flexget/issues
- Did you fill out the issue template as completely as possible?

The issue template is here because it helps to ensure you submitted all the necessary information the first time, and allows us to more quickly review issues. Please fill it out correctly and do not ignore it, no matter how irrelevant you think it may be. Thanks in advance for your help with this!
--->
### Expected behaviour:
<!---
Please don't just say "it doesn't crash" or "it works". Explain what the expected result is.
--->

download file via sftp_download plugin

### Actual behaviour:

does not download the file due to filename containing ';' character
all other files download normally (alphanumeric with some containing dashes)

### Steps to reproduce:
- Step 1: ...

have a config with sftp_download task to accept all files
run `flexget execute --tasks sync-series`
series name has a ';' in the name (Steins;Gate 0)

#### Config:
```
paste FULL config and remove any personal info
if config is too long, attach the file to the ticket
```
 
```
variables: variables.yml

templates:

  global:
    no_entries_ok: yes

  anchors:
    _sftp_list: &sftp_list
      host: '{? sftp.host ?}'
      username: '{? sftp.username ?}'
      private_key: '{? sftp.private_key ?}'
      private_key_pass: '{? sftp.private_key_pass ?}'

  notify-sync:
    notify:
      entries:
        title: 'FlexGet'
        message: "Downloaded {{title|replace('.mkv','')|replace('.mp4','')}}"
        via:
          - pushbullet:
              api_key: '{? pushbullet.apikey ?}'

tasks:

  sync-series:
    sftp_list:
      <<: *sftp_list
      dirs:
        - '{? remote.downloads ?}/series/'
    accept_all: yes
    sftp_download:
      to: '{? local.downloads ?}/series/'
    template: notify-sync

  sort-series:
    filesystem: '{? local.downloads ?}/series/'
    configure_series:
      from:
        filesystem: '{? local.videos ?}/series/'
    move:
      to: "{? local.videos ?}/series/{{series_name}}/"

schedules:
  - tasks: sync-series
    schedule:
      minute: "*/30"
      hour: 8-23
  - tasks: sort-series
    schedule:
      minute: 10,40
      hour: 8-23
```
 
#### Log:
```
paste log output
if log output is too long, attach the file to the ticket
```

from flexget.log: Remote path does not exist: /home/$user/downloads/flexget/series/Steins

### Additional information:

- FlexGet version: 2.13.3
- Python version: 2.7.12
- Installation method: pip install inside a virtualenv based on python2
- Using daemon (yes/no): yes
- OS and version: ubuntu 16.04.3 LTS 64 bit
- Link to crash log:

<!---
In config and debug/crash logs, remember to redact any personal or sensitive information such as passwords, API keys, private URLs and so on.

Please verify that the following data is present before submitting your issue:

- Link to a paste service or paste above the relevant config (preferably full config, including templates if present). Please make sure the paste does not expire, if possible.
- Link to a paste service or paste above debug-level logs of the relevant task/s (use `flexget -L debug execute --tasks <Task_name>`).
- FlexGet version (use `flexget -V` to get it).
- Full Python version, for example `2.7.11` (use `python -V` to get it). Note that FlexGet is not supported for use with Python v3.0, 3.1, 3.2 or 3.6.
- Installation method (pip, git install, etc).
- Whether or not you're running FlexGet as a daemon.
- OS and version.
- Attach crash log if one was generated, in addition to the debug-level log. It can be found in the directory with your config file.
--->



Discussion:
Tutturuu~~
It seems the issue may stem from the remote path being fed through `urlparse()`, which reads ; as [part of the URL syntax](https://docs.python.org/2/library/urlparse.html). The second half of the folder name is put into `params` instead of `path`, [here](https://github.com/Flexget/Flexget/blob/eb05875cede67d525de9920aeb6452b90cf71838/flexget/plugins/clients/sftp.py#L396).

Not sure it's wise to change this, however. It's "working as intended", and perhaps the folder name should be changed to not include characters known to be used as delimiters?
that's what I've done as a workaround. just removed it from the folder name. and on the other end, made sure files were stripping the semicolon when being renamed.
This is valid issue
Yup, I certainly didn't mean for it to behave that way. 😆 I don't see any danger in changing it.

Resolving patch:
diff --git a/flexget/plugins/clients/sftp.py b/flexget/plugins/clients/sftp.py
--- a/flexget/plugins/clients/sftp.py
+++ b/flexget/plugins/clients/sftp.py
@@ -1,6 +1,6 @@
 from __future__ import unicode_literals, division, absolute_import
 from builtins import *  # noqa pylint: disable=unused-import, redefined-builtin
-from future.moves.urllib.parse import urljoin, urlparse
+from future.moves.urllib.parse import urljoin, urlparse, quote, unquote
 
 import logging
 import os
@@ -231,7 +231,7 @@ def handle_node(path, size_handler, is_dir):
             if is_dir and files_only:
                 return
 
-            url = urljoin(url_prefix, sftp.normalize(path))
+            url = urljoin(url_prefix, quote(sftp.normalize(path)))
             title = remotepath.basename(path)
 
             entry = Entry(title, url)
@@ -393,7 +393,7 @@ def download_entry(self, entry, config, sftp):
         Downloads the file(s) described in entry
         """
 
-        path = urlparse(entry['url']).path or '.'
+        path = unquote(urlparse(entry['url']).path) or '.'
         delete_origin = config['delete_origin']
         recursive = config['recursive']
 


Test patch: