Repository: avocado-framework/avocado

Issue:
[BUG] When the asset name parameter is not a full URL, relative directory hash is evaluated from an empty string.
When an Asset object is created using the `name` parameter without a full URL, the method `_get_relative_dir` returns the hash of an empty string because `base_url = os.path.dirname(self.parsed_name.geturl())` evaluates to an empty string, as in the example below:

```
>>> os.path.dirname(urllib.parse.urlparse('foo.tgz').geturl())
''
>>> 
```

Previous code evaluated the hash of `:///` as it was "manually" building the URL.

This is not a consistent behavior, nor expected.


Discussion:
My suggestion to fix this problem is to move each piece of the URL to its correct place and do not mix asset name with locations.

With this suggestion, if an Asset object is created like:

```
self.hello = self.fetch_asset(
    name='https://mirrors.peers.community/mirrors/gnu/hello/hello-2.9.tar.gz',
    locations=['https://mirrors.kernel.org/gnu/hello/hello-2.9.tar.gz',
        'http://gnu.c3sl.ufpr.br/ftp/hello-2.9.tar.gz',
        'ftp://ftp.funet.fi/pub/gnu/prep/hello/hello-2.9.tar.gz']
    )
```

- First, the file name should be removed from all the URLs in the `locations` parameter. 
- Second, the URL `https://mirrors.peers.community/mirrors/gnu/hello/` from `name` should be moved to the first index of `locations`.

It also means the relative directory built from the URL should match the first location where the `fetch` method was able to download the file.
Before I start the implementation, I think some discussion may be valid here. 

Just to add the context on how it works today, if the parameter `name` is **NOT** a URI and there is a `hash` parameter, the relative asset destination is `by_name`. Otherwise, the relative asset destination is `by_location` plus a subdirectory, created as the hash of URI in the `name` parameter without the asset name.

Let me describe some use cases for the discussion:

1. If the `name` parameter is a URI, the relative directory will be `by_location`, and the subdirectory into it will be the hash of the URI in the `name` parameter without the file name. The following code represents this:

```
self.hello = self.fetch_asset(
    name='https://mirrors.peers.community/mirrors/gnu/hello/hello-2.9.tar.gz',
    )
```

2. If the `name` parameter is a URI, but there are mirrors listed in the `location` parameter, the relative directory will be `by_location`, and the subdirectory into it will be the hash of the URI in the `name` parameter without the file name. The following code represents this:

```
self.hello = self.fetch_asset(
    name='https://mirrors.peers.community/mirrors/gnu/hello/hello-2.9.tar.gz',
    locations=['https://mirrors.kernel.org/gnu/hello/hello-2.9.tar.gz',
        'http://gnu.c3sl.ufpr.br/ftp/hello-2.9.tar.gz',
        'ftp://ftp.funet.fi/pub/gnu/prep/hello/hello-2.9.tar.gz']
    )
```

In this case, if the URI in the `name` parameter is unreachable and the asset is fetched from one of the other mirrors from `location`, the destination hash into `by_location` won't reflect the correct location. This problem impacts the `find_asset_file` method in the Asset class because it will also look for the asset in the directory representing the hash of the URI in the `name` parameter.

3. If the `name` parameter is **NOT** a URI, and there are mirrors listed in the `location` parameter, the relative directory will be `by_location`, and the subdirectory into it will be the hash of an empty string. The following code represents this:

```
self.hello = self.fetch_asset(
    name='hello-2.9.tar.gz',
    locations=['https://mirrors.kernel.org/gnu/hello/hello-2.9.tar.gz',
        'http://gnu.c3sl.ufpr.br/ftp/hello-2.9.tar.gz',
        'ftp://ftp.funet.fi/pub/gnu/prep/hello/hello-2.9.tar.gz']
    )
```

Again, we will have the same problem of an incorrect destination hash for the asset into `by_location` relative directory.

This third scenario may still be valid for a user fetching a package from a project that builds the same package name for its latest repository and send it to a couple of mirrors. In this case, every time the package is updated, the hash changes, so having the hash in the fetch command won't have the desired effect for the user.

The questions I have so far, and I think, deserve a discussion are:

1. Should we accept the use case described in **2**? If we agree it is a valid use case, I would like to use the suggestion from https://github.com/avocado-framework/avocado/issues/3659#issuecomment-599728305 to fix it.

2. Fixing the problem of having a hash for an empty string, use case **3** may lead to multiple destination subdirectories into `by_location` in case of the asset not being fetched from the first mirror in every attempt. Does it make sense to send files that have more than 1 location defined in the `locations` parameter to `by_name` relative directory, independent of having a hash for it?

Opinions and suggestions here are appreciated and welcome @clebergnu, @apahim!
> My suggestion to fix this problem is to move each piece of the URL to its correct place and do not mix asset name with locations.
> 
> With this suggestion, if an Asset object is created like:
> 
> ```
> self.hello = self.fetch_asset(
>     name='https://mirrors.peers.community/mirrors/gnu/hello/hello-2.9.tar.gz',
>     locations=['https://mirrors.kernel.org/gnu/hello/hello-2.9.tar.gz',
>         'http://gnu.c3sl.ufpr.br/ftp/hello-2.9.tar.gz',
>         'ftp://ftp.funet.fi/pub/gnu/prep/hello/hello-2.9.tar.gz']
>     )
> ```
> 

Even though this scenario is currently supported (but IMO broken), I'd suggest we implement a clearer supported usage.  My suggestions:

* If `name` is *not* a URL, at least one entry in `locations` would be *required*
   * In this case, where name is a file name that was clearly given, the intended behavior is to get the file from any of the available location, so the location in the cache (currently implemented by `_get_relative_dir()` should *not* include the location. The goal is that the local cache location would not be different, and the file would be found in the cache, if the first location used once, and then another location is used later 
  * Note: there may be a caveat here, with odd cases when the last component of a location will not match the `name`, for instance, `https://github.com/avocado-framework/avocado/archive/77.0.tar.gz`.  In this case, should we add name to the location? We have to decide whether to support this or not

> * First, the file name should be removed from all the URLs in the `locations` parameter.

I generally agree, but see my previous comment.

> * Second, the URL `https://mirrors.peers.community/mirrors/gnu/hello/` from `name` should be moved to the first index of `locations`.
> 

Yes, I also agree this is simpler, but there's the caveat I noticed before.

> It also means the relative directory built from the URL should match the first location where the `fetch` method was able to download the file.

This point I don't agree.  The relative directory is just a tool to make sure we store and find the files in cache correctly.  So, if the file is the same, but available on multiple locations, the relative directory should be the same (because the file is the same) even if it's acquired from different locations.
> Before I start the implementation, I think some discussion may be valid here.
> 
> Just to add the context on how it works today, if the parameter `name` is **NOT** a URI and there is a `hash` parameter, the relative asset destination is `by_name`. Otherwise, the relative asset destination is `by_location` plus a subdirectory, created as the hash of URI in the `name` parameter without the asset name.
> 
> Let me describe some use cases for the discussion:
> 
> 1. If the `name` parameter is a URI, the relative directory will be `by_location`, and the subdirectory into it will be the hash of the URI in the `name` parameter without the file name. The following code represents this:
> 
> ```
> self.hello = self.fetch_asset(
>     name='https://mirrors.peers.community/mirrors/gnu/hello/hello-2.9.tar.gz',
>     )
> ```
> 

So this use case is for once you have or know of a single possible location for a file. Given that the relative directory's goal is simply to store and find the file in the cache appropriately, I wonder why the location hash does *not* include the "file name" in the URI. I don't see why it shouldn't... can you think of a reason?

> 1. If the `name` parameter is a URI, but there are mirrors listed in the `location` parameter, the relative directory will be `by_location`, and the subdirectory into it will be the hash of the URI in the `name` parameter without the file name. The following code represents this:
> 
> ```
> self.hello = self.fetch_asset(
>     name='https://mirrors.peers.community/mirrors/gnu/hello/hello-2.9.tar.gz',
>     locations=['https://mirrors.kernel.org/gnu/hello/hello-2.9.tar.gz',
>         'http://gnu.c3sl.ufpr.br/ftp/hello-2.9.tar.gz',
>         'ftp://ftp.funet.fi/pub/gnu/prep/hello/hello-2.9.tar.gz']
>     )
> ```
> 
> In this case, if the URI in the `name` parameter is unreachable and the asset is fetched from one of the other mirrors from `location`, the destination hash into `by_location` won't reflect the correct location. This problem impacts the `find_asset_file` method in the Asset class because it will also look for the asset in the directory representing the hash of the URI in the `name` parameter.

I see, you're right. I can only think of changing the allowed uses of the `fetch_asset` asset method so that if multiple locations are given, a name should be just that, a file name.  Do you have other ideas?

> 
> 1. If the `name` parameter is **NOT** a URI, and there are mirrors listed in the `location` parameter, the relative directory will be `by_location`, and the subdirectory into it will be the hash of an empty string. The following code represents this:
> 

This sounds wrong to me, I mean, instead of `by_location` + `empty_hash`, it should just be `by_name`, which amounts to the same level of separation.

> ```
> self.hello = self.fetch_asset(
>     name='hello-2.9.tar.gz',
>     locations=['https://mirrors.kernel.org/gnu/hello/hello-2.9.tar.gz',
>         'http://gnu.c3sl.ufpr.br/ftp/hello-2.9.tar.gz',
>         'ftp://ftp.funet.fi/pub/gnu/prep/hello/hello-2.9.tar.gz']
>     )
> ```
> 
> Again, we will have the same problem of an incorrect destination hash for the asset into `by_location` relative directory.
> 

Yeah, I don't think that's intended.

> This third scenario may still be valid for a user fetching a package from a project that builds the same package name for its latest repository and send it to a couple of mirrors. In this case, every time the package is updated, the hash changes, so having the hash in the fetch command won't have the desired effect for the user.
> 
> The questions I have so far, and I think, deserve a discussion are:
> 
> 1. Should we accept the use case described in **2**? If we agree it is a valid use case, I would like to use the suggestion from [#3659 (comment)](https://github.com/avocado-framework/avocado/issues/3659#issuecomment-599728305) to fix it.

I think that is broken.  Like I said elsewhere, I think that use case should be one that accepts `name` is a file name *only* and requires one or more locations.

> 2. Fixing the problem of having a hash for an empty string, use case **3** may lead to multiple destination subdirectories into `by_location` in case of the asset not being fetched from the first mirror in every attempt. Does it make sense to send files that have more than 1 location defined in the `locations` parameter to `by_name` relative directory, independent of having a hash for it?
> 

That's what I would expect!

> Opinions and suggestions here are appreciated and welcome @clebergnu, @apahim!


> > Before I start the implementation, I think some discussion may be valid here.
> > Just to add the context on how it works today, if the parameter `name` is **NOT** a URI and there is a `hash` parameter, the relative asset destination is `by_name`. Otherwise, the relative asset destination is `by_location` plus a subdirectory, created as the hash of URI in the `name` parameter without the asset name.
> > Let me describe some use cases for the discussion:
> > 
> > 1. If the `name` parameter is a URI, the relative directory will be `by_location`, and the subdirectory into it will be the hash of the URI in the `name` parameter without the file name. The following code represents this:
> > 
> > ```
> > self.hello = self.fetch_asset(
> >     name='https://mirrors.peers.community/mirrors/gnu/hello/hello-2.9.tar.gz',
> >     )
> > ```
> 
> So this use case is for once you have or know of a single possible location for a file. Given that the relative directory's goal is simply to store and find the file in the cache appropriately, I wonder why the location hash does _not_ include the "file name" in the URI. I don't see why it shouldn't... can you think of a reason?

There would be just one reason I can think of if I am a user using this interface. In case I was fetching multiple files from the same URI, I would expect all the fetched files to be located in the same cache directory. It means that all files fetched from, for example, `https://mirrors.kernel.org/gnu/hello/`, reside on the same cache location, but files fetched from `https://mirrors.kernel.org/gnu/git/` reside on its cache location. In my understanding, the local hash for the location should be the full URI, except the file name. Does that make sense?

> 
> > 1. If the `name` parameter is a URI, but there are mirrors listed in the `location` parameter, the relative directory will be `by_location`, and the subdirectory into it will be the hash of the URI in the `name` parameter without the file name. The following code represents this:
> > 
> > ```
> > self.hello = self.fetch_asset(
> >     name='https://mirrors.peers.community/mirrors/gnu/hello/hello-2.9.tar.gz',
> >     locations=['https://mirrors.kernel.org/gnu/hello/hello-2.9.tar.gz',
> >         'http://gnu.c3sl.ufpr.br/ftp/hello-2.9.tar.gz',
> >         'ftp://ftp.funet.fi/pub/gnu/prep/hello/hello-2.9.tar.gz']
> >     )
> > ```
> > 
> > 
> > In this case, if the URI in the `name` parameter is unreachable and the asset is fetched from one of the other mirrors from `location`, the destination hash into `by_location` won't reflect the correct location. This problem impacts the `find_asset_file` method in the Asset class because it will also look for the asset in the directory representing the hash of the URI in the `name` parameter.
> 
> I see, you're right. I can only think of changing the allowed uses of the `fetch_asset` asset method so that if multiple locations are given, a name should be just that, a file name. Do you have other ideas?

It should not be difficult to move the URI in the `name` parameter to the `locations` list instead of forcing the location to the in the correct parameter. It keeps the API flexible. Or we can just restrict the use cases of `name` and `locations` as I listed at the end of this comment.

> 
> > 1. If the `name` parameter is **NOT** a URI, and there are mirrors listed in the `location` parameter, the relative directory will be `by_location`, and the subdirectory into it will be the hash of an empty string. The following code represents this:
> 
> This sounds wrong to me, I mean, instead of `by_location` + `empty_hash`, it should just be `by_name`, which amounts to the same level of separation.

Agree!

> 
> > ```
> > self.hello = self.fetch_asset(
> >     name='hello-2.9.tar.gz',
> >     locations=['https://mirrors.kernel.org/gnu/hello/hello-2.9.tar.gz',
> >         'http://gnu.c3sl.ufpr.br/ftp/hello-2.9.tar.gz',
> >         'ftp://ftp.funet.fi/pub/gnu/prep/hello/hello-2.9.tar.gz']
> >     )
> > ```
> > 
> > 
> > Again, we will have the same problem of an incorrect destination hash for the asset into `by_location` relative directory.
> 
> Yeah, I don't think that's intended.
> 
> > This third scenario may still be valid for a user fetching a package from a project that builds the same package name for its latest repository and send it to a couple of mirrors. In this case, every time the package is updated, the hash changes, so having the hash in the fetch command won't have the desired effect for the user.
> > The questions I have so far, and I think, deserve a discussion are:
> > 
> > 1. Should we accept the use case described in **2**? If we agree it is a valid use case, I would like to use the suggestion from [#3659 (comment)](https://github.com/avocado-framework/avocado/issues/3659#issuecomment-599728305) to fix it.
> 
> I think that is broken. Like I said elsewhere, I think that use case should be one that accepts `name` is a file name _only_ and requires one or more locations.

So, in a nutshell we have 2 possibilities:

1. `name` should be a full URI and `locations` should be empty.
2. `name` should be a single file name and `locations` should have one or more entries.

> 
> > 1. Fixing the problem of having a hash for an empty string, use case **3** may lead to multiple destination subdirectories into `by_location` in case of the asset not being fetched from the first mirror in every attempt. Does it make sense to send files that have more than 1 location defined in the `locations` parameter to `by_name` relative directory, independent of having a hash for it?
> 
> That's what I would expect!

Agree!

> 
> > Opinions and suggestions here are appreciated and welcome @clebergnu, @apahim!
> > > Before I start the implementation, I think some discussion may be valid here.
> > > Just to add the context on how it works today, if the parameter `name` is **NOT** a URI and there is a `hash` parameter, the relative asset destination is `by_name`. Otherwise, the relative asset destination is `by_location` plus a subdirectory, created as the hash of URI in the `name` parameter without the asset name.
> > > Let me describe some use cases for the discussion:
> > > 
> > > 1. If the `name` parameter is a URI, the relative directory will be `by_location`, and the subdirectory into it will be the hash of the URI in the `name` parameter without the file name. The following code represents this:
> > > 
> > > ```
> > > self.hello = self.fetch_asset(
> > >     name='https://mirrors.peers.community/mirrors/gnu/hello/hello-2.9.tar.gz',
> > >     )
> > > ```
> > 
> > 
> > So this use case is for once you have or know of a single possible location for a file. Given that the relative directory's goal is simply to store and find the file in the cache appropriately, I wonder why the location hash does _not_ include the "file name" in the URI. I don't see why it shouldn't... can you think of a reason?
> 
> There would be just one reason I can think of if I am a user using this interface. In case I was fetching multiple files from the same URI, I would expect all the fetched files to be located in the same cache directory. It means that all files fetched from, for example, `https://mirrors.kernel.org/gnu/hello/`, reside on the same cache location, but files fetched from `https://mirrors.kernel.org/gnu/git/` reside on its cache location. In my understanding, the local hash for the location should be the full URI, except the file name. Does that make sense?
> 

About all files coming from the same URL being stored in the same directory, I have the feeling that this is much more of an implementation detail. It seems nice to have that indeed, allowing users eventually to navigate that directory and its contents, but it seems like a secondary goal. With regards to removing the file name from the hash and using only the base location (such as `https://mirrors.kernel.org/gnu/hello/`) I can not think of any problem, given that we also expect that the remaining URL components (the "file name") will also be unique.  So, to summarize the answer, I think it does make sense. Hopefully we won't be disproved by the end result! :) 

> > > 1. If the `name` parameter is a URI, but there are mirrors listed in the `location` parameter, the relative directory will be `by_location`, and the subdirectory into it will be the hash of the URI in the `name` parameter without the file name. The following code represents this:
> > > 
> > > ```
> > > self.hello = self.fetch_asset(
> > >     name='https://mirrors.peers.community/mirrors/gnu/hello/hello-2.9.tar.gz',
> > >     locations=['https://mirrors.kernel.org/gnu/hello/hello-2.9.tar.gz',
> > >         'http://gnu.c3sl.ufpr.br/ftp/hello-2.9.tar.gz',
> > >         'ftp://ftp.funet.fi/pub/gnu/prep/hello/hello-2.9.tar.gz']
> > >     )
> > > ```
> > > 
> > > 
> > > In this case, if the URI in the `name` parameter is unreachable and the asset is fetched from one of the other mirrors from `location`, the destination hash into `by_location` won't reflect the correct location. This problem impacts the `find_asset_file` method in the Asset class because it will also look for the asset in the directory representing the hash of the URI in the `name` parameter.
> > 
> > 
> > I see, you're right. I can only think of changing the allowed uses of the `fetch_asset` asset method so that if multiple locations are given, a name should be just that, a file name. Do you have other ideas?
> 
> It should not be difficult to move the URI in the `name` parameter to the `locations` list instead of forcing the location to the in the correct parameter. It keeps the API flexible. Or we can just restrict the use cases of `name` and `locations` as I listed at the end of this comment.
> 
> > > 1. If the `name` parameter is **NOT** a URI, and there are mirrors listed in the `location` parameter, the relative directory will be `by_location`, and the subdirectory into it will be the hash of an empty string. The following code represents this:
> > 
> > 
> > This sounds wrong to me, I mean, instead of `by_location` + `empty_hash`, it should just be `by_name`, which amounts to the same level of separation.
> 
> Agree!
> 
> > > ```
> > > self.hello = self.fetch_asset(
> > >     name='hello-2.9.tar.gz',
> > >     locations=['https://mirrors.kernel.org/gnu/hello/hello-2.9.tar.gz',
> > >         'http://gnu.c3sl.ufpr.br/ftp/hello-2.9.tar.gz',
> > >         'ftp://ftp.funet.fi/pub/gnu/prep/hello/hello-2.9.tar.gz']
> > >     )
> > > ```
> > > 
> > > 
> > > Again, we will have the same problem of an incorrect destination hash for the asset into `by_location` relative directory.
> > 
> > 
> > Yeah, I don't think that's intended.
> > > This third scenario may still be valid for a user fetching a package from a project that builds the same package name for its latest repository and send it to a couple of mirrors. In this case, every time the package is updated, the hash changes, so having the hash in the fetch command won't have the desired effect for the user.
> > > The questions I have so far, and I think, deserve a discussion are:
> > > 
> > > 1. Should we accept the use case described in **2**? If we agree it is a valid use case, I would like to use the suggestion from [#3659 (comment)](https://github.com/avocado-framework/avocado/issues/3659#issuecomment-599728305) to fix it.
> > 
> > 
> > I think that is broken. Like I said elsewhere, I think that use case should be one that accepts `name` is a file name _only_ and requires one or more locations.
> 
> So, in a nutshell we have 2 possibilities:
> 
> 1. `name` should be a full URI and `locations` should be empty.
> 2. `name` should be a single file name and `locations` should have one or more entries.
> 

I think that's fair enough, and removes a good deal of confusion on the expected behavior.

> > > 1. Fixing the problem of having a hash for an empty string, use case **3** may lead to multiple destination subdirectories into `by_location` in case of the asset not being fetched from the first mirror in every attempt. Does it make sense to send files that have more than 1 location defined in the `locations` parameter to `by_name` relative directory, independent of having a hash for it?
> > 
> > 
> > That's what I would expect!
> 
> Agree!
> 
> > > Opinions and suggestions here are appreciated and welcome @clebergnu, @apahim!



Resolving patch:
diff --git a/avocado/plugins/assets.py b/avocado/plugins/assets.py
--- a/avocado/plugins/assets.py
+++ b/avocado/plugins/assets.py
@@ -18,6 +18,7 @@
 
 import ast
 import os
+import urllib.parse
 
 from avocado.core import data_dir
 from avocado.core import exit_codes
@@ -197,11 +198,32 @@ def fetch_assets(test_file, klass=None, method=None, logger=None):
     :returns: list of names that were successfully fetched and list of
     fails.
     """
+    def validate_parameters(call):
+        """
+        Validate the parameters to make sure we have a supported case.
+
+        :param call: List of parameter to the Asset object.
+        :type call: dict
+        :returns: True or False
+        """
+        name = call.get('name', None)
+        locations = call.get('locations', None)
+        # probably, parameter name was defined as a class attribute
+        if ((name is None) or
+                # probably, parameter locations was defined as a class attribute
+                (not urllib.parse.urlparse(name).scheme and
+                 locations is None)):
+            return False
+        return True
+
     cache_dirs = data_dir.get_cache_dirs()
     success = []
     fail = []
     handler = FetchAssetHandler(test_file, klass, method)
     for call in handler.calls:
+        # validate the parameters
+        if not validate_parameters(call):
+            continue
         expire = call.pop('expire', None)
         if expire is not None:
             expire = data_structures.time_to_seconds(str(expire))
@@ -217,7 +239,7 @@ def fetch_assets(test_file, klass=None, method=None, logger=None):
                             test_file, klass, method)
             asset_obj.fetch()
             success.append(call['name'])
-        except OSError as failed:
+        except (OSError, ValueError) as failed:
             fail.append(failed)
     return success, fail
 
diff --git a/avocado/utils/asset.py b/avocado/utils/asset.py
--- a/avocado/utils/asset.py
+++ b/avocado/utils/asset.py
@@ -69,9 +69,15 @@ def __init__(self, name, asset_hash, algorithm, locations, cache_dirs,
         self.asset_hash = asset_hash
 
         self.parsed_name = urllib.parse.urlparse(self.name)
-        self.asset_name = os.path.basename(self.parsed_name.path)
-        self.relative_dir = os.path.join(self._get_relative_dir(),
-                                         self.asset_name)
+
+        # we currently support the following options for name and locations:
+        # 1. name is a full URI and locations is empty;
+        # 2. name is a single file name and locations is one or more entries.
+        # raise an exception if we have an unsupported use of those arguments
+        if ((self.parsed_name.scheme and locations is not None) or
+                (not self.parsed_name.scheme and locations is None)):
+            raise ValueError("Incorrect use of parameter name with parameter"
+                             " locations.")
 
         if algorithm is None:
             self.algorithm = DEFAULT_HASH_ALGORITHM
@@ -86,6 +92,12 @@ def __init__(self, name, asset_hash, algorithm, locations, cache_dirs,
         self.expire = expire
         self.metadata = metadata
 
+        # set asset_name according to parsed_name
+        self.asset_name = os.path.basename(self.parsed_name.path)
+        # set relative_dir for the asset
+        self.relative_dir = os.path.join(self._get_relative_dir(),
+                                         self.asset_name)
+
     def _create_hash_file(self, asset_path):
         """
         Compute the hash of the asset file and add it to the CHECKSUM
@@ -196,24 +208,42 @@ def _get_local_file(self, url_obj, asset_path):
 
     def _get_relative_dir(self):
         """
-        When an asset name is not an URL, and it also has a hash,
-        there's a clear intention for it to be unique *by name*,
-        overwriting it if the file is corrupted or expired.  These
-        will be stored in the cache directory indexed by name.
-
-        When an asset name is an URL, whether it has a hash or not, it
-        will be saved according to their locations, so that multiple
-        assets with the same file name, but completely unrelated to
-        each other, will still coexist.
+        When an asset name is not an URI, and:
+          1. it also has a hash;
+          2. or it has multiple locations;
+        there's a clear intention for it to be unique *by name*, overwriting
+        it if the file is corrupted or expired. These will be stored in the
+        cache directory indexed by name.
+
+        When an asset name is an URI, whether it has a hash or not, it will be
+        saved according to their locations, so that multiple assets with the
+        same file name, but completely unrelated to each other, will still
+        coexist.
 
         :returns: target location of asset the file.
         :rtype: str
         """
-        if self.asset_hash and not self.parsed_name.scheme:
+        if (not self.parsed_name.scheme and
+                (self.asset_hash or len(self.locations) > 1)):
             return 'by_name'
-        base_url = os.path.dirname(self.parsed_name.geturl())
+
+        # check if the URI is located on self.locations or self.parsed_name
+        if self.locations is not None:
+            # if it is on self.locations, we need to check if it has the
+            # asset name on it or a trailing '/'
+            if ((self.asset_name in self.locations[0]) or
+                    (self.locations[0][-1] == '/')):
+                base_url = os.path.dirname(self.locations[0])
+            else:
+                # here, self.locations is a pure conformant URI
+                base_url = self.locations[0]
+        else:
+            # the URI is on self.parsed_name
+            base_url = os.path.dirname(self.parsed_name.geturl())
+
         base_url_hash = hashlib.new(DEFAULT_HASH_ALGORITHM,
                                     base_url.encode(astring.ENCODING))
+
         return os.path.join('by_location', base_url_hash.hexdigest())
 
     def _get_writable_cache_dir(self):


Test patch:
diff --git a/selftests/functional/test_plugin_assets.py b/selftests/functional/test_plugin_assets.py
--- a/selftests/functional/test_plugin_assets.py
+++ b/selftests/functional/test_plugin_assets.py
@@ -86,7 +86,7 @@ def setUp(self):
         self.base_dir, self.mapping, self.config_file = (
             get_temporary_config(self))
         asset_dir = os.path.join(self.mapping['cache_dir'], 'by_location',
-                                 'da39a3ee5e6b4b0d3255bfef95601890afd80709')
+                                 'a784600d3e01b346e8813bbd065d00048be8a482')
         os.makedirs(asset_dir)
         open(os.path.join(asset_dir, 'hello-2.9.tar.gz'), "w").close()
 
diff --git a/selftests/functional/test_utils_asset.py b/selftests/functional/test_utils_asset.py
--- a/selftests/functional/test_utils_asset.py
+++ b/selftests/functional/test_utils_asset.py
@@ -36,7 +36,7 @@ def test_fetch_url_cache_by_location(self):
         self.assertTrue(foo_tarball.startswith(expected_location))
         self.assertTrue(foo_tarball.endswith(self.assetname))
 
-    def test_fetch_name_cache_by_name(self):
+    def test_fetch_name_hash_cache_by_name(self):
         foo_tarball = asset.Asset(self.assetname,
                                   asset_hash=self.assethash,
                                   algorithm='sha1',
@@ -47,6 +47,17 @@ def test_fetch_name_cache_by_name(self):
                                          self.assetname)
         self.assertEqual(foo_tarball, expected_location)
 
+    def test_fetch_name_locations_cache_by_name(self):
+        foo_tarball = asset.Asset(self.assetname,
+                                  asset_hash=None,
+                                  algorithm='sha1',
+                                  locations=[self.url, 'file://fake_dir'],
+                                  cache_dirs=[self.cache_dir],
+                                  expire=None).fetch()
+        expected_location = os.path.join(self.cache_dir, 'by_name',
+                                         self.assetname)
+        self.assertEqual(foo_tarball, expected_location)
+
     def test_fetch_expire(self):
         foo_tarball = asset.Asset(self.assetname,
                                   asset_hash=self.assethash,
@@ -88,11 +99,19 @@ def test_fetch_expire(self):
             content2 = f.read()
         self.assertNotEqual(content1, content2)
 
-    def test_exception(self):
-        a = asset.Asset(name='bar.tgz', asset_hash=None, algorithm=None,
+    def test_incorrect_name_locations_parameter_case1(self):
+        # 1. name is a full URI and locations is empty
+        with self.assertRaises(ValueError):
+            asset.Asset(name='file://bar.tgz', asset_hash=None, algorithm=None,
+                        locations='file://foo', cache_dirs=[self.cache_dir],
+                        expire=None)
+
+    def test_incorrect_name_locations_parameter_case2(self):
+        # 2. name is a single file name and locations is one or more entries.
+        with self.assertRaises(ValueError):
+            asset.Asset(name='bar.tgz', asset_hash=None, algorithm=None,
                         locations=None, cache_dirs=[self.cache_dir],
                         expire=None)
-        self.assertRaises(OSError, a.fetch)
 
     def test_fetch_lockerror(self):
         dirname = os.path.join(self.cache_dir, 'by_name')
@@ -101,7 +120,7 @@ def test_fetch_lockerror(self):
             a = asset.Asset(self.assetname,
                             asset_hash=self.assethash,
                             algorithm='sha1',
-                            locations=None,
+                            locations=['file://foo1', 'file://foo2'],
                             cache_dirs=[self.cache_dir],
                             expire=None)
             self.assertRaises(OSError, a.fetch)