Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Package Manager] Re-Download missing packages #14018

Open
wants to merge 31 commits into
base: 3.x
Choose a base branch
from

Conversation

exside
Copy link
Contributor

@exside exside commented Jul 28, 2018

Was surprised that the package manager is not able to re-download packages in case the zip files / extracted folders are missing for an installed package (for whatever reason, in my particular case did I want to save space for backups and did not include the packages folder...). When trying to re-install a package with missing files it would proceed to the installation (of course without metadata displaying), then when hitting "Continue" show the modal and say could not download the package etc., but it never actually tried to re-download...

What does it do?

This PR changes that so if the zip file is not present, it would actually try to re-download it. There is an edge case where the package is not available anymore on the provider (for example I had an old version of the "console" extra that was not available anymore) and the provider would return an error code (e.g. 404 in that case), but the old code would actually "package" the response HTML or JSON code into a .zip file and then of course the installation would fail with "could not unpack package"...This PR also solves that issue by handling error codes from the provider.

Although not used very often, I did rewrite large parts of the _getByFsockopen() method as it needed some love.

Why is it needed?

See above.

opengeek and others added 5 commits July 11, 2018 16:29
Was surprised that the package manager is not able to re-download packages in case the zip files / extracted folders are missing for an installed package (for whatever reason, in my particular case did I want to save space for backups and did not include the packages folder...). When trying to re-install a package with missing files it would proceed to the installation (of course without metadata displaying), then when hitting "Continue" show the modal and say could not download the package etc., but it never actually tried to re-download...

This PR changes that so if the zip file is not present, it would actually try to re-download it. There is an edge case where the package is not available anymore on the provider (for example I had an old version of the "console" extra that was not available anymore) and the provider would return an error code (e.g. 404 in that case), but the old code would actually "package" the response HTML or JSON code into a .zip file and then of course the installation would fail with "could not unpack package"...This PR also solves that issue by handling error codes from the provider.

Although not used very often, I did rewrite large parts of the _getByFsockopen() method as it needed some love.
@exside
Copy link
Contributor Author

exside commented Jul 28, 2018

If anybody knows a better way to get a packages download link than (without making an unnecessary remote request)

$location = $this->get('metadata')[array_keys(array_filter($this->get('metadata'), function($item) { if ($item['name'] === 'location') { return $item; } }))[0]];

please let me know, happy to change that monster...

And of course it would be nice if the frontend would inform the user about whats happening before he hits "Continue/Install" and then gets the error, but not sure how to add that...

@OptimusCrime
Copy link
Contributor

I do not know how the lookup is structured in $this->get('metadata'), but at the very least have the method on a seperate line:

$location = $this->get('metadata')[array_keys(array_filter($this->get('metadata'), function($item) { 
    if ($item['name'] === 'location') { 
        return $item; 
    }
}))[0]];

I would split this expression into a method instead and do each step seperate. Here the code is a bit too confusing to just read out. You also seem to use array_filter and then just use the first element in the resulting array. This could be split into a method that finds the correct $item or returns null, which, in turn, should execute the remote fetch instead.

@exside
Copy link
Contributor Author

exside commented Jul 29, 2018

@OptimusCrime added your multiline suggestion, I'd like to avoid creating new methods for something "that trivial", as far as I can see if a package is installed it will always have the location in the metadata, so there should be no need for a remote request. The problem is, that the metadata is horribly structured to say the least, its an array with numeric keys, so the location was at index 28 in my case, but I don't want to rely on that index to be always the same, therefore the array_filter() which of course will always only return one item. Maybe we can take the multiliner a bit apart without creating a new method:

// loop trough metadata arrays until location is found
$locationarr = array_filter($this->get('metadata'), function($item) { 
    if ($item['name'] === 'location') { 
        return $item; 
    }
});
// determine the index at which the location metadata was found
$locationkey = array_keys($locationarr)[0];
// get the location metadata
$location = $this->get('metadata')[$locationkey];

will update like that for now.

Hopefully slightly more readable, also added comments.
Switch comment style to the one used in the rest of the file. Also make sure that if there is a package directory it is checked that it has files in it, I occasionally have the scenario when that directory is there, but its empty, then the package re-unpack fails and so does the installation of the package.
/* not very pretty way of getting the download URL, but otherwise we have to make a
* remote request to $this->Provider->info($this->signature) to get the URL.
* loop trough metadata arrays until location is found */
$locationarr = array_filter($this->get('metadata'), function($item) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To maintain readability I suggest using the camelCase naming convention for variable names.

core/model/modx/transport/modtransportpackage.class.php Outdated Show resolved Hide resolved
@JoshuaLuckers
Copy link
Contributor

Nice work! Just to be sure, did you sign the Contributor License Agreement?

Following the advice from @OptimusCrime, I added a more generic getMetadata() method to the class, like this any other metadata can also be accessed more easily in case its ever needed.
@exside
Copy link
Contributor Author

exside commented Jul 29, 2018

@JoshuaLuckers @OptimusCrime

Separated the metadata code into a separate, more generic method getMetadata(), like this a package's metadata can be accessed more easily in general, any other field as well, in case its needed.

    /**
     * Get metadata for a package in a more usable format
     *
     * @return array an array of metadata accessible by metadata name field
     */
    public function getMetadata() {
        $metadata = array_reduce($this->get('metadata'), function ($result, $item) {
            $key = $item['name'];
            unset($item['name']);
            
            $result[$key] = $item;
            
            return $result;
        }, array());
        
        return $metadata;
    }

That method will return an array in the form of:

Array
(
    [id] => Array
        (
            [attributes] => Array
                (
                )

            [text] => 4d556cf0b2b083396d000eeb
            [children] => Array
                (
                )

        ),
...
    [location] => Array
        (
            [attributes] => Array
                (
                )

            [text] => http://modx.com/extras/download/?id=4d556cf1b2b083396d000eec
            [children] => Array
                (
                )

        )
...
)

that reduces the code within transportPackage() to:

            /* make sure the package is downloaded, if not attempt re-download */
            if (strpos($sourceFile, '//') === false && !file_exists($targetDir . $sourceFile)) {
                /* get the package metadata */
                $metadata = $this->getMetadata();

                if (!empty($metadata) && $metadata['location']) {
                    /* assign remote download URL */
                    $source = $metadata['location']['text'];
                }
            }

Need some help with the sprintf() thingy, not sure how you would do that optimization.

@JoshuaLuckers signed the contributor agreement years ago =)

When updating a package it would also trigger the re-download logic (as package file not present yet) and re-assign the `$sourceFile` variable passed to `transportPackage()` which would cause it to fail. Therefore it is now checked if the `$sourceFile` variable contains a URL already, if that's the case, the re-download logic is skipped. When triggering a re-install action from the manager the `$sourceFile` variable is set to the package string and not a URL like when an update is triggered.
@OptimusCrime
Copy link
Contributor

Not sure if we use sprintf that much in the core. I usually just see string concatenation like you have already done.

@exside
Copy link
Contributor Author

exside commented Jul 30, 2018

Another update, did the direct return as suggested but also found some strange edge cases with an installation I have:

  1. it failed because there was no location attribute on the first level, only in the nested array of the file attribute --> therefore I had to implement recursion into getMetadata(), so it also parses the nested arrays and falls back to $metadata['file']['children']['location']['text'] (yeah, my fingers hurt after typing that =D), the method got a bit uglier but also more versatile/complete. It's worth mentioning that I used use, but as the min. PHP requirements for MODX 2.6+ is PHP 5.3+, that should be fine right?

  2. For some strange reason there are packages where a nicely formatted, one-dimensional attributes array is returned from $this->get('metadata) (newest MIGX and pThumb for example), had to deal with that as well...

There is an edge case I couldn't solve: Missing dependencies (like resizer for pThumb) are not properly re-downloaded, I had to delete the package via manager first, then re-install pThumb to make it work...don't know why^^, not familiar enough with the dependency feature of the package manager.

@Jako
Copy link
Collaborator

Jako commented Jul 31, 2018

Just one thing. Could this PR target 2.x?

core/model/modx/transport/modtransportpackage.class.php Outdated Show resolved Hide resolved
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7",
"Keep-Alive: 300",
"Connection: keep-alive",
"Referer: http://$host",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it has any influence on how the code works but just to be consistent it would be nice if the referer would use the same protocol (http or https) as the "host".

also changed referrer URL scheme
@exside exside changed the base branch from 2.6.x to 2.x August 3, 2018 15:21
@exside
Copy link
Contributor Author

exside commented Aug 3, 2018

@Jako just changed the base to 2.x, is that the proper way of targeting PRs? It seems it kind of messes up the PR a bit, there are now 10 changed files (probably because the PR was based on 2.6.5) instead of just one...do I need to resolve that by re-doing the PR based on the 2.x branch?

@JoshuaLuckers changed the way of returning metadata according to your suggestion and added the proper URL scheme to the referrer, guess that's the right way:

"Referer: " . MODX_URL_SCHEME . $host,

?

@JoshuaLuckers
Copy link
Contributor

Looks fine to me! After you changes the target you have some conflicts that need to be solved. Best way to fix it is to create a new branch with your changes. Revert the current branch to the latest commit from 2.x and cherry pick your changes on to the current branch and force push them.

@Jako
Copy link
Collaborator

Jako commented Aug 6, 2018

I am not sure, if it is the case. It should be possible to redownload every package, not only missing ones.

@opengeek
Copy link
Member

opengeek commented Aug 6, 2018

Looks fine to me! After you changes the target you have some conflicts that need to be solved. Best way to fix it is to create a new branch with your changes. Revert the current branch to the latest commit from 2.x and cherry pick your changes on to the current branch and force push them.

@JoshuaLuckers — Don't fear the rebase! ;)

@exside
Copy link
Contributor Author

exside commented Aug 7, 2018

@Jako A general Re-download feature would be another story, this is simply about fixing a broken "Re-install" functionality in case there is no zip-file present in the packages directory (or the unpacked folder is empty)...

regarding the branch mess, was that really necessary (to change the target branch to 2.x)? Switched back to the original 2.6.x for now...

@opengeek opengeek modified the milestones: v3.0.0-alpha2, v3.0.0-alpha3 Jan 29, 2020
@Ibochkarev
Copy link
Collaborator

@exside please accept @JoshuaLuckers suggestion and merge conflict.

@cla-bot
Copy link

cla-bot bot commented Sep 19, 2020

Thank you for your contribution! Before your pull request can be reviewed, please sign the MODX Contributor License Agreement (CLA). This is to make sure MODX has your written permission to distribute projects containing your contributions under the appropriate license.

Please make sure the CLA has been signed for GitHub user(s): @exside

Once you've signed, please reply with @cla-bot check to verify and update the status of this pull request.

@Jako
Copy link
Collaborator

Jako commented Sep 19, 2020

I have commited the changes of @JoshuaLuckers here.

@opengeek
Copy link
Member

@cla-bot check

@cla-bot cla-bot bot added the cla-signed CLA confirmed for contributors to this PR. label Sep 19, 2020
@cla-bot
Copy link

cla-bot bot commented Sep 19, 2020

The cla-bot has been summoned, and re-checked this pull request!

@opengeek opengeek modified the milestones: v3.0.0-alpha3, v3.0.0-beta1 Oct 27, 2021
@opengeek opengeek modified the milestones: v3.0.0-beta1, v3.0.0-beta2 Nov 9, 2021
@opengeek opengeek modified the milestones: v3.0.0-beta2, v3.0.0-rc1 Nov 23, 2021
@opengeek opengeek modified the milestones: v3.0.0-rc1, v3.0.0-rc2 Jan 20, 2022
@JoshuaLuckers
Copy link
Contributor

@Mark-H @opengeek any idea what's the best way to resolve the conflict(s) in this PR? Do we need to manually apply the changes to the refactored file?

@Mark-H
Copy link
Collaborator

Mark-H commented Jan 23, 2022

Typically you'd need to do a rebase + force push. If that's not feasible or complex, manually applying changes In a new branch is sometimes easier.

@Mark-H Mark-H modified the milestones: v3.0.0-rc2, v3.1.0 Jan 23, 2022
@opengeek
Copy link
Member

There are 31 commits in this PR. It needs to be reapplied manually on top of latest. It seems someone merged other work into this PR branch.

@OptimusCrime

This comment was marked as off-topic.

@JoshuaLuckers

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-core blocked Active participation around the pull request or issue required. Consensus is not reached. cla-signed CLA confirmed for contributors to this PR. proposal Proposal about improvement aka RFC. Need to be discussed before start implementation.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants