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

Adds support for local paths and project bundles #5735

Merged
merged 36 commits into from
Apr 2, 2021

Conversation

IanCaio
Copy link
Contributor

@IanCaio IanCaio commented Oct 26, 2020

This PRs introduces 2 changes:

  • It allows the prefix local: to be used on the project file paths. That prefix translates to a local path in relation to that project file path.
  • It allows saving a project with its resources (Project Bundle) from both the CLI (lmms --makeBundle ~/lmms/projects/input.mmp ~/lmms/projets/bundle/output.mmp) or the Save As dialog (by marking Save as Project Bundle (with resources)).

A project bundle is basically a project file and a resources folder, containing all external files used by the project (i.e.: samples, presets, plugins). The logic for the creation of the bundle goes as following:

  1. There's a std::map on DataFile.cpp called ELEMENTS_WITH_RESOURCES. It maps a QString (the TagName of the element) to a std::vector of QStrings (the attributes of that element that are paths to resources). New elements can be easily added to the map so they are also addressed when creating the bundle.
  2. Create a directory called resources on the same path as the output bundle project
  3. For each tag name on ELEMENTS_WITH_RESOURCES, check if any of the attributes that links to paths are present. If they are, get an absolute path to the resource, copy that resource to the resources directory created earlier and update the attribute to point to that copied file (local:resources/filename).

To avoid name conflicts between files inside the resource folder, all of them are renamed to a number followed by the extension of the file (i.e.: 1.wav, 2.dll).

SaveAs

It's not allowed to save a bundle on the same path where there's also another bundle. I first thought of allowing bundles being overwritten, but I'm aiming towards simplicity on this PR: Overwritting requires that I remove the existing resources folder, which is fine. Except that there's the possibility that the project being saved is that bundle itself, so you remove its resources before copying them to the new resources folder. A temporary folder could be used for the operation, but then there's the issue of guaranteeing an unique name for the temporary folder. So initially I'm submitting this PR with this operation not allowed (and the appropriate feedback to the user explaining it). After it goes through testing and code review and it merged, we can work towards allowing the overwrites if it's a wanted feature (personally I see project bundles as a way to make backups and share projects between different systems, so overwritting doesn't sound like a very important operation on this context).

For testers

Basically test the overall functionality. Try to save project bundles and test them, check if any resources are missing, try to overwrite existing bundles, saving a project bundle from a project that is also a project bundle. Be careful to work with backup files and in a safe folder.

For code reviewers

The logic behind this feature is:

  • PathUtil now resolves the local: base prefix by checking the project file name. If a project isn't open (when running from CLI for example) local: is returned again so the method calling the PathUtil method can resolve the path itself.
  • DataFile::writeFile was rewritten to allow saving with resources depending on the value of a boolean on its parameters. The method that copies the files to the resources folder and updates the paths on the DataFile is DataFile::copyResources.
  • main.cpp checks for the --makeBundle command to create a project bundle from the CLI.
  • MainWindow.cpp and Song.cpp were edited so they account for the possibility of saving with resources. For the Save As dialog, a new saveOption was added to save as a project bundle.

There are warnings that I left on DataFile::writeFile for debugging purposes. Most will be either removed or replaced by an appropriate user warning.

	Adds a new Base for paths called "local:", which will translate to the dir where the currently opened project file is at. In the future this will allow us to make project bundles and make it easier to export and transfer projects to other people without breaking the paths to samples, presets, plugins and others.
	For now, to make a bundle LMMS has to be run through CLI with the makeBundle/--makeBundle command, followed by an input file and an output file ('lmms --makeBundle input.mmp output.mmp'). DataFile::writeBundle() is then called. For now, it only saves the mmp/mmpz file normally and also creates a "resources" folder if it doesn't exists. Later it will also manipulate the DataFile so all paths are local and copy all files to the resources folder.

TODO:
	-Remove warnings.
	-Implement the logic to manipulate the DataFile and copy files.
	Starts implementing the logic that will go through all the resources of the project file and add them to the bundle. We use a std::map of QString to std::vector<QString>: The first string is the DOM element tagname that is going to be searched for. The vector of strings holds all attributes this element can have that accesses resources. For now we just print those to the screen.
	The raw logic for creating the bundle is finished. It now copies the resource files and update the project to use "local:" paths to the new file now.
	Now it's a matter of organizing things and adding safety checks for file operation errors basically.
	Improves comments and debugging warnings to make the writeBundle a bit more organized for review.
@LmmsBot
Copy link

LmmsBot commented Oct 26, 2020

🤖 Hey, I'm @LmmsBot from github.com/lmms/bot and I made downloads for this pull request, click me to make them magically appear! 🎩

Windows

Linux

macOS

🤖
{"platform_name_to_artifacts": {"Windows": [{"artifact": {"title": {"title": "32-bit", "platform_name": "Windows"}, "link": {"link": "https://13307-15778896-gh.circle-artifacts.com/0/lmms-1.3.0-alpha.1.122%2Bg6e8a6366c-mingw-win32.exe"}}, "build_link": "https://circleci.com/gh/LMMS/lmms/13307?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link"}, {"artifact": {"title": {"title": "64-bit", "platform_name": "Windows"}, "link": {"link": "https://13310-15778896-gh.circle-artifacts.com/0/lmms-1.3.0-alpha.1.122%2Bg6e8a6366c-mingw-win64.exe"}}, "build_link": "https://circleci.com/gh/LMMS/lmms/13310?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link"}, {"artifact": {"title": {"title": "32-bit", "platform_name": "Windows"}, "link": {"link": "https://ci.appveyor.com/api/buildjobs/krfxi0nc8dqkt1mt/artifacts/build/lmms-1.3.0-alpha-msvc2017-win32.exe"}}, "build_link": "https://ci.appveyor.com/project/Lukas-W/lmms/builds/38501254"}, {"artifact": {"title": {"title": "64-bit", "platform_name": "Windows"}, "link": {"link": "https://ci.appveyor.com/api/buildjobs/rjg09g99lnehwvxw/artifacts/build/lmms-1.3.0-alpha-msvc2017-win64.exe"}}, "build_link": "https://ci.appveyor.com/project/Lukas-W/lmms/builds/38501254"}], "Linux": [{"artifact": {"title": {"title": "(AppImage)", "platform_name": "Linux"}, "link": {"link": "https://13309-15778896-gh.circle-artifacts.com/0/lmms-1.3.0-alpha.1.122%2Bg6e8a636-linux-x86_64.AppImage"}}, "build_link": "https://circleci.com/gh/LMMS/lmms/13309?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link"}], "macOS": [{"artifact": {"title": {"title": "", "platform_name": "macOS"}, "link": {"link": "https://13308-15778896-gh.circle-artifacts.com/0/lmms-1.3.0-alpha.1.122%2Bg6e8a6366c-mac10.14.dmg"}}, "build_link": "https://circleci.com/gh/LMMS/lmms/13308?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link"}]}, "commit_sha": "91509bda26461f966cb89a2e7be6d62dda7a632d"}

src/core/DataFile.cpp Outdated Show resolved Hide resolved
src/core/DataFile.cpp Outdated Show resolved Hide resolved
src/core/DataFile.cpp Outdated Show resolved Hide resolved
	Adds a project bundle folder, inside which the bundles will be created. Instead of receiving an output project file name, the makeBundle command now receives a bundle name that will be used as the name of the bundle's folder.
	Uses a typedef for the std::map with the tags and attributes with resources.

TODO:
	- Fix the local: prefix so it works when we don't have the project file open (for CLI usage, or find another way to deal with it).
	- Sanitize the bundle name.
	- Allow overwriting bundles?
	The PathUtil base prefix conversion for "local:" uses the loaded song file name. When we are running the makebundle command from the CLI there isn't a loaded project, so those prefixes aren't converted properly. Now, when there isn't a project open PathUtil will return "local:" again when it tries to convert this base prefix. DataFile can then check if the base prefix is still there, and if it is it knows the conversion wasn't possible, so it does the conversion itself. To do that, a member called m_fileName was added to DataFile, which will hold the file being manipulated if that's where the DataFile originated from, and the local path can be retrieved from it.
@IanCaio
Copy link
Contributor Author

IanCaio commented Oct 28, 2020

With the latest commits there were a couple of changes to the way project bundles are built:

  • Now there's a new folder on the working dir of LMMS, called projectbundles
  • The writeBundle method takes a bundle name, not a file name. A folder with that name will be created inside projectbundles, which will contain the bundle itself (project file + resources).
  • Bundles currently can't be overwritten, writeBundle will fail if there's a folder with that name already (I'm thinking of allowing overwriting later, by recursively deleting the folder of the existing bundle and writing again from scratch).
  • The local: base prefix uses the Engine::getSong()->projectFileName() method to retrieve the local folder. For that reason, when running from CLI (the only way to currently make a bundle) we might run into the issue of not having a project open. To tackle that, resolving local: will return local: again if there isn't a project open, so the method calling PathUtil can handle the situation. writeBundle will check if the base prefix is still there after resolving, and if it is it will resolve the local path by itself.
  • For writeBundle to be able to resolve the local path, a new member variable was added to DataFile called m_fileName, which will hold the file path if the DataFile was built from a file (or an empty string if not).

To do list:

  • Sanitize the bundle name so it doesn't contain invalid characters for folders.
  • Allow overwriting an existing bundle.
  • Create the GUI dialog for saving a bundle.

	The bundle name is now sanitized. Since it's going to be used as a folder name, we need to keep the user from giving invalid folder names as a bundle's name. The rules for the name are:
	1) It must start with a word character (either a digit or letter)
	2) It can be followed by any number of letters, digits, whitespaces or hyphens
	3) It must end with a word character (either a digit or letter)

	A Regexp is used to check for the name validity.
	This commit regresses some functionality. Project bundles will be saved just as any other project, except they will also have the resources folder. It will be up to the user to organize the bundles on their own folders. It's currently not allowed to save a bundle on a folder where there's one already though (if there's a resources folder already). Later it might be allowed to overwrite bundles in that case.
	The projectbundles folder was dropped. The user can save project bundles anywhere in the system.
	The DataFile::writeBundle was removed. It's functionality was merged into the DataFile::writeFile method, by adding a boolean on the parameters defining whether it should be saved with resources. The logic of copying the resource files and changing the paths inside the project DataFile was moved to DataFile::copyResources, making the methods a little bit less dense.
	The "Save As" dialog now has an option to save project as a project bundle (with resources), which will save the file as a bundle.

Known bug:
	- Because the "local:" base prefix is translated to the filename from the Engine::getSong(), it breaks when Song::guiSaveProjectAs is called, because that method changes the project name before saving. Urgent fix!
	There was a bug where "local:" prefixes weren't resolved properly during saving because Song::guiSaveProjectAs() changed the project name to the destiny file name before saving. This resulted in the local paths using the destination file as a reference. Both Song::guiSaveProject() and Song::guiSaveProjectAs() were rewritten, and now they only rename the project after it's saved.
@IanCaio
Copy link
Contributor Author

IanCaio commented Oct 29, 2020

I probably have changed the way this PR works too much by now, but I was trying to figure out the best way in terms of simplicity to introduce in the code base. The project bundle folder idea would require lots of extra coding compared to the one used now:

  • To save a project bundle, the user should go on the Save Project As option. On the dialog window, there are save options (Discard MIDI connections and now Save as Project Bundle (with resources)). By selecting the Save as Project Bundle (with resources) option, the project file will be saved as a project bundle.
  • After saving as a bundle, if the user press the Save option the project file will be saved normally (not as a bundle). To save again as a bundle the user have to click Save Project As once again.
  • It's still not possible to overwrite bundles (you can overwrite the project file with a regular save, but you can't overwrite a bundle because the resources folder is already present).

SaveAs

	When the user tries to save a project bundle on a folder that already has a project bundle (contains a resources folder) a message box pops up telling the user it's not permitted and that another path should be chosen.
@IanCaio
Copy link
Contributor Author

IanCaio commented Oct 29, 2020

I think the basic functionality is finished. A little different from the original concept but I had to change things along the way trying to make the feature as simple as possible. Marking as ready for review. Main comment was updated with some more accurate information on the current state of the PR.

@IanCaio IanCaio marked this pull request as ready for review October 29, 2020 14:06
	Forgot to remove <QRegExp> header when I removed the code that used it.
	For safety reasons, remove the possibility to bundle VSTs loaded
through vestige. Also runs a safety check during the project being
loaded (Song::loadProject) to check if either Vestige plugins or effect
plugins are using local paths, and abort the project load if so. That is
to avoid malicious code being run because of bad DLLs being shipped with
a project file.
	Extracts code that checks if a DataFile contains local paths to
plugins to another method inside DataFile.
@IanCaio
Copy link
Contributor Author

IanCaio commented Oct 31, 2020

As per discussed with @DomClark on the discord channel about the safety of allowing plugins to be bundled (or accessed through local paths), I removed the Vestige plugins from the project bundle, which now only bundle samples.

I also changed Song::loadProject so it doesn't load projects that contains local paths to plugins. I'd like others to double check if I covered every place where plugin's path could be on the XML project file. Currently I'm checking the attribute plugin on the <vestige> element and for each <effect> element, I'm checking the child <key> elements and <attribute> elements inside those. If any contain name="file" I check if value has a local path.

@superpaik
Copy link
Contributor

Sorry, I can't test this functionality. Mingw links are broken, and MSVC version crashes when a sample is drag-and-drop to the song-editor.

	Removes warnings previously used for debugging. Improves a
warning message on PathUtil.
	Fixes small bug, where a QMessageBox was being used to prompt an
error without checking if the gui is loaded first. Now we check for the
GUI and if we are in CLI mode we use a QTextStream instead.
@IanCaio
Copy link
Contributor Author

IanCaio commented Feb 21, 2021

PR updated to latest master, debugging messages removed and small bug on the error logging for the CLI mode is fixed.

@IanCaio
Copy link
Contributor Author

IanCaio commented Mar 28, 2021

The doc comment is wrong. Doxygen outputs:

DataFile.cpp:492: warning: argument 'QDomElement' of command @param is not found in the argument list of DataFile::hasLocalPlugins(QDomElement parent=QDomElement(), bool firstCall=true) const
DataFile.cpp:492: warning: argument 'bool' of command @param is not found in the argument list of DataFile::hasLocalPlugins(QDomElement parent=QDomElement(), bool firstCall=true) const
DataFile.h:77: warning: The following parameters of DataFile::hasLocalPlugins(QDomElement parent=QDomElement(), bool firstCall=true) const are not documented:
  parameter 'parent'
  parameter 'firstCall'

The type is only colored to mark it as argument name, which is wrong in this case.

Fixed! But we have a big problem, because there are other places in the codebase where the doxygen comments were written that way, which was where I took reference from. This is probably why make doc shows a gazillion warnings here, I ended up not even spotting those because of that.

@JohannesLorenz
Copy link
Contributor

But we have a big problem

Yeah, I get 473 kB of warnings, this is indeed a "big" problem 😃

The only left comment is writing an optional test. I'm OK with the code. Should we already call out for testers? Or do you think it does not need further testing (there have been tests a while ago)?

@IanCaio
Copy link
Contributor Author

IanCaio commented Mar 28, 2021

@Gumichan01 has tested recently and I tried to manually test after changes like the hasLocalPlugin rewrite. Maybe we could leave those automated unit tests for the future as I'd still have to read on how to write them, but could be a good idea to throw it on Discord in case anyone wants to test.

I think it's working fine though, I tried to continuously check it during the commits.

Btw, CircleCI seems to have failed but not sure why, maybe something went wrong in the middle of the tests?

@JohannesLorenz
Copy link
Contributor

About the CI error:

/root/project/src/core/DataFile.cpp: In member function 'bool DataFile::hasLocalPlugins(QDomElement, bool) const':
/root/project/src/core/DataFile.cpp:523:22: error: 'assert' was not declared in this scope
   assert(childElement);

I get that one all the time (because my system has assert inside the compiler headers somehow, but CI needs <cassert>). However, I then always switch to Q_ASSERT (<QtGlobal>?), to be consistent with other code.

@IanCaio
Copy link
Contributor Author

IanCaio commented Mar 28, 2021

About the CI error:

/root/project/src/core/DataFile.cpp: In member function 'bool DataFile::hasLocalPlugins(QDomElement, bool) const':
/root/project/src/core/DataFile.cpp:523:22: error: 'assert' was not declared in this scope
   assert(childElement);

I get that one all the time (because my system has assert inside the compiler headers somehow, but CI needs <cassert>). However, I then always switch to Q_ASSERT (<QtGlobal>?), to be consistent with other code.

It's good that it triggered an error though, because it uncovered an issue: childElement is of type QDomElement, and assert was converting it to bool. Even if toElement() returned a null node, the assertion would probably be true. Q_ASSERT on the other hand was more strict about the type and complained about the conversion.

I'm thinking about removing that assert though, as it seems that no other part of the codebase asserts for the conversion to element, and if the node is not an element, toElement will return an empty object that can go through the rest of the codeblock with no problems if I understood right. Asserting childElement.isNull() would be a problem as well, for the same reason that caused those infinite iterations (some elements might actually be null, as in empty).

	Some assert statements were being done wrong and are probably
even unnecessary for that piece of code, so they were removed.
	PathUtil::toShortestRelative() was including the local paths on
the candidate paths, which could lead to a unallowed resource (i.e.:
vst plugin) to be assigned a local path even on a regular save.
	The local paths are now skipped when looking for the shortest
relative path, since they should only be used by the bundle save on the
allowed resources.
@IanCaio
Copy link
Contributor Author

IanCaio commented Mar 28, 2021

Last commit fixes a bug where in certain situations a local: path could be assigned to some resource if it turned out to be the shortest relative path to it, even on regular saves. The PathUtil::toShortestRelative method now skips local paths to prevent that from happening, as those should only be used by the bundle save in the allowed resources.

Copy link
Contributor

@JohannesLorenz JohannesLorenz left a comment

Choose a reason for hiding this comment

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

Code is OK until here 👍

@superpaik
Copy link
Contributor

Some comments from a user point of view.

  • you have to go to "Save As..." and then check the option "Save as a Project Bundle". Ok. But it's kind of weird thought. Imagine, you have a project name test.mmpz, that's saved. You go to Save as Bundle. In the filename box, you save it as test.mmpz (original name), but you check the bundle option. Even thought the dialogue says that the file exists and it will be overwritten (project file, test.mmpz), it's not, only the bundle is saved. That can cause user confusion.
  • If I have a project, say test1.mmpz, you go to save as (the normal one) and save it as test2.mmpz, you keep working on test2. But if you do the same but checking the "Save as bundle" you keep working on test1. I understand why, but it's kind of odd.
  • On the other hand, if the bundle exists, it cannot overwritten. Why do not have the same behaviour as a project? A message saying that files will be overwritten (in fact, delete all the folder and write it again).
    I think it all will be more clear and easy to do if we have an specific option for "Save as Bundle" at menu level.

Also (and I think that unfortunately it's not possible to solve, but maybe you guys have a solution) is that if you use a VST that uses samples (like a sampler -vitala or poise, for example-) those samples are not included in the bundle, so you have to keep track of those samples and include them on the bundle if you wanna share it.

@IanCaio
Copy link
Contributor Author

IanCaio commented Mar 29, 2021

Thanks for testing @superpaik !

I agree some details might be a bit confusing from the user perspective, maybe clarifying some behavior with documentation would help for now. This is more of an initial implementation but the idea is to continuously improve it and who knows, maybe even transition the regular "Save" to that approach. It's just that there are lots of changing parts, so it's better done incrementally IMO. Sorry about the big reply, just trying to answer some of your observations with some details from the coding side 😅

  • you have to go to "Save As..." and then check the option "Save as a Project Bundle". Ok. But it's kind of weird thought. Imagine, you have a project name test.mmpz, that's saved. You go to Save as Bundle. In the filename box, you save it as test.mmpz (original name), but you check the bundle option. Even thought the dialogue says that the file exists and it will be overwritten (project file, test.mmpz), it's not, only the bundle is saved. That can cause user confusion.

On the "Save" dialog we have the option of choosing a file or a folder IIRR. If I used the folder selection, the user would probably have to create the folder and then choose it to save the bundle inside. I used the filename to make it more convenient (avoiding that creating folder step), but it ended up causing this weird behavior where the dialog still thinks the save will happen over the file when in reality it will create a folder with the same name (minus the extension). I'll take a look to see if I can omit the "Overwrite" dialog when the "Save with resources" option is selected, but from a quick look on Qt's QFileDialog documentation it seems there's an option for that called QFileDialog::DontConfirmOverwrite. The problem is that the options are usually set before creating the dialog and changing it later with setOption() doesn't seem to guarantee that the option will take effect if the dialog is already visible, so I'm not sure I can alternate the dialog to ask confirmation to overwrite or ignore it depending on whether "Save with resources" is selected or not. I could try to just use it (worst case scenario it still asks confirmation), but then some systems might have that overwrite confirmation behavior and some not.

  • If I have a project, say test1.mmpz, you go to save as (the normal one) and save it as test2.mmpz, you keep working on test2. But if you do the same but checking the "Save as bundle" you keep working on test1. I understand why, but it's kind of odd.

Yeah, that was purposefully. For now the bundle save is just meant for backup/sharing (some things would have to change for the user to be able to continuously work and save over the same bundle). So the project still points to the original file and keeps marked as modified to force the user to do a regular save too and not lose its work.

  • On the other hand, if the bundle exists, it cannot overwritten. Why do not have the same behaviour as a project? A message saying that files will be overwritten (in fact, delete all the folder and write it again).

Since the bundle is (at least now) meant to be a backup, I ended up not bothering about making it able to overwrite automatically, that not being very common when saving backups. It's also a bit safer to obligate the user to manually delete a bundle before writing over it, because if the user picks a project name that would match an existing folder without the extension they could unwillingly overwrite a folder that wasn't a bundle to begin with. If we decide to implement the overwriting despite that, there are some things to decide, as for example: Should it erase everything inside the previous bundle before overwriting or should it only overwrite the project file and samples? If there are samples with the same name should those be overwritten too or assigned a name with a increased number? That change could maybe use a separate PR to decide those things and find the best approach 😄

I think it all will be more clear and easy to do if we have an specific option for "Save as Bundle" at menu level.

That could be done! I'll later check how that change would look like on code, if it turns out to be simple I can include in this same PR!

Also (and I think that unfortunately it's not possible to solve, but maybe you guys have a solution) is that if you use a VST that uses samples (like a sampler -vitala or poise, for example-) those samples are not included in the bundle, so you have to keep track of those samples and include them on the bundle if you wanna share it.

I've to confirm, but I think except for plugins that ship with LMMS the others might store their information encoded in their XML encoded data, making it harder to access and change the paths, at least with the approach used right now. I wonder if those plugins use relative paths themselves though? That would make it easier to workaround the fact that those samples are not included in the bundle, but would still force you to copy by hand to the bundle folder 😕

@Spekular
Copy link
Member

It's taken me quite a while to get around to, but I'd like to review this again tomorrow.

@Spekular
Copy link
Member

I fell behind schedule again so I won't actually do as much of a review as I hoped today, but as I suspected we don't need to hack getBaseDir. Ian wrote on Discord:

When it's called from the GUI, Engine::getSong returns an object so PathUtil can resolve local: paths. If the user runs lmms makebundle project.mmp bundle.mmp though, the bundle is created from the DataFile but it doesn't actually create a Song, so instead DataFile uses the name of the file it originated from to resolve the local path

But DataFile.cpp contains tons of if(gui) checks already. We could simply replace if (PathUtil::baseLookup(resPath) == PathUtil::Base::LocalDir) with if (!gui) in DataFile::copyResources and state that the LocalDir prefix is only safe to use when a gui exists. (Sidenote: this feels like a situation where my original plan of using a new class for enhanced paths would have been useful. Then the paths could be initialized with the location of "local" (default to empty string, perhaps) and they would always be able to return it. A withLocal method would return a copy of the path adapted to a different local dir, allowing it to change.)

Secondly, I don't think we should blindly ignore the local path in PathUtil::toShortestRelative. Either we add a method that takes a list of allowed bases (in order of descending priority) and pass a list w/o local when necessary, or we add a boolean toggle. Normal projects should be allowed to use local as well, this was one of my long term goals with the enhanced paths (that I left as a later enhancement).

@IanCaio
Copy link
Contributor Author

IanCaio commented Mar 30, 2021

But DataFile.cpp contains tons of if(gui) checks already. We could simply replace if (PathUtil::baseLookup(resPath) == PathUtil::Base::LocalDir) with if (!gui) in DataFile::copyResources and state that the LocalDir prefix is only safe to use when a gui exists.

Having PathUtil return an invalid path (not removing the local: prefix) sounds like a better alternative IMO than allowing it to return a valid but wrong path (removing local: but not resolving it at all). The latter could lead to hard to track bugs if someone used an unresolved path while the first would be easier to track since it would probably result in an error instead. Why is it bad to return the unresolved path in the situations where it can't be resolved?

Secondly, I don't think we should blindly ignore the local path in PathUtil::toShortestRelative. Either we add a method that takes a list of allowed bases (in order of descending priority) and pass a list w/o local when necessary, or we add a boolean toggle. Normal projects should be allowed to use local as well, this was one of my long term goals with the enhanced paths (that I left as a later enhancement).

Only a few elements are allowed to have local paths and LMMS will not load any project that has local paths outside that list. If we simply allow local paths to be considered on toShortestRelative, VSTs could try to use it and we would have a project that can't be opened later because LMMS would refuse to, that's why I removed it from there. I agree we could later come up with a way for the method consider local paths for those allowed elements, but right now it's easier to just not allow it at all and leave it just for the bundle save.

The way local paths works right now is not set in stone, so we can slowly change and improve it in the future to transition our projects to use it too (besides the bundles), but there are things to consider while doing so that I believe are out of the scope of this PR, we'd probably be able to cover them better by doing it incrementally.

@Spekular
Copy link
Member

Spekular commented Mar 31, 2021

Having PathUtil return an invalid path (not removing the local: prefix) sounds like a better alternative IMO than allowing it to return a valid but wrong path (removing local: but not resolving it at all). The latter could lead to hard to track bugs if someone used an unresolved path while the first would be easier to track since it would probably result in an error instead. Why is it bad to return the unresolved path in the situations where it can't be resolved?

In either scenario the caller has to be aware of the behavior, which is only specified in the actual method implementation. It makes sense to me that if there's no file saved yet, we assume the local directory is as high up in the hierarchy as possible. I find local: to be more surprising as a return value than an empty string, and want to minimize surprise. After some more thought, the best solution for now is probably to add a status indicator to the method, moving it back towards a more self-documenting state. If the function only takes a base and returns a path, there's no indication that something could go wrong. If it accepts a base and a pointer to a status bool/int, you're alerted to the fact that something could go wrong.

Only a few elements are allowed to have local paths and LMMS will not load any project that has local paths outside that list. If we simply allow local paths to be considered on toShortestRelative, VSTs could try to use it and we would have a project that can't be opened later because LMMS would refuse to, that's why I removed it from there. I agree we could later come up with a way for the method consider local paths for those allowed elements, but right now it's easier to just not allow it at all and leave it just for the bundle save.
The way local paths works right now is not set in stone, so we can slowly change and improve it in the future to transition our projects to use it too (besides the bundles), but there are things to consider while doing so that I believe are out of the scope of this PR, we'd probably be able to cover them better by doing it incrementally.

Incremental improvement is good and all, but I think we need to be careful to avoid "it gets worse before it gets better" changes. Again, toShortestRelative currently does exactly what it says on the tin. Excluding local: is surprising, adding an allowLocal argument would remove the element of surprise (you can once again figure out the behavior via name and type).

@IanCaio
Copy link
Contributor Author

IanCaio commented Mar 31, 2021

I didn't see it much as "it gets worse before it gets better", but "it's just decent, but it will get better" 😆

I like the ideas you suggested though, would you be good with the changes if I make getBaseDir accept an optional status pointer and use it instead, and also add a allowLocal parameter to toShortestRelative and just default it to false so it isn't allowed right now?

@Spekular
Copy link
Member

[...] would you be good with the changes if I make getBaseDir accept an optional status pointer and use it instead, and also add a allowLocal parameter to toShortestRelative and just default it to false so it isn't allowed right now?

Absolutely!

	Changes some of the PathUtil methods to allow a boolean pointer
to be used to return the status of the method, setting it to false if it
failed somewhere.
	Also adds a parameter to toShortestRelative to either allow or
forbid local paths in the search for the shortest relative path.
src/core/PathUtil.cpp Outdated Show resolved Hide resolved
Copy link
Member

@Spekular Spekular left a comment

Choose a reason for hiding this comment

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

Concerns I had are fixed!

@IanCaio
Copy link
Contributor Author

IanCaio commented Mar 31, 2021

Awesome! If nobody's against it I'm merging it tomorrow.

@superpaik Just letting you know, I was working on the UI change to have a separate menu for the bundle save, but it involved changing the PR further and would delay the reviewing a little bit. We'll still take it into consideration and improve it, just in a separate PR so we can get to the reorg sooner!

@superpaik
Copy link
Contributor

Perfect! Thanks!

@Spekular Spekular merged commit e16dca3 into LMMS:master Apr 2, 2021
devnexen pushed a commit to devnexen/lmms that referenced this pull request Apr 10, 2021
* Adds a baseDir for the local path

	Adds a new Base for paths called "local:", which will translate to the dir where the currently opened project file is at. In the future this will allow us to make project bundles and make it easier to export and transfer projects to other people without breaking the paths to samples, presets, plugins and others.

* Starts implementing the makeBundle functionality

	For now, to make a bundle LMMS has to be run through CLI with the makeBundle/--makeBundle command, followed by an input file and an output file ('lmms --makeBundle input.mmp output.mmp'). DataFile::writeBundle() is then called. For now, it only saves the mmp/mmpz file normally and also creates a "resources" folder if it doesn't exists. Later it will also manipulate the DataFile so all paths are local and copy all files to the resources folder.

TODO:
	-Remove warnings.
	-Implement the logic to manipulate the DataFile and copy files.

* Starts implementing logic to go through resources

	Starts implementing the logic that will go through all the resources of the project file and add them to the bundle. We use a std::map of QString to std::vector<QString>: The first string is the DOM element tagname that is going to be searched for. The vector of strings holds all attributes this element can have that accesses resources. For now we just print those to the screen.

* Adds logic to copy files and update the project

	The raw logic for creating the bundle is finished. It now copies the resource files and update the project to use "local:" paths to the new file now.
	Now it's a matter of organizing things and adding safety checks for file operation errors basically.

* Makes the writeBundle method more organized

	Improves comments and debugging warnings to make the writeBundle a bit more organized for review.

* Adds a project bundle folder

	Adds a project bundle folder, inside which the bundles will be created. Instead of receiving an output project file name, the makeBundle command now receives a bundle name that will be used as the name of the bundle's folder.
	Uses a typedef for the std::map with the tags and attributes with resources.

TODO:
	- Fix the local: prefix so it works when we don't have the project file open (for CLI usage, or find another way to deal with it).
	- Sanitize the bundle name.
	- Allow overwriting bundles?

* Handles local paths when a project isn't open

	The PathUtil base prefix conversion for "local:" uses the loaded song file name. When we are running the makebundle command from the CLI there isn't a loaded project, so those prefixes aren't converted properly. Now, when there isn't a project open PathUtil will return "local:" again when it tries to convert this base prefix. DataFile can then check if the base prefix is still there, and if it is it knows the conversion wasn't possible, so it does the conversion itself. To do that, a member called m_fileName was added to DataFile, which will hold the file being manipulated if that's where the DataFile originated from, and the local path can be retrieved from it.

* Sanitizes the bundle name

	The bundle name is now sanitized. Since it's going to be used as a folder name, we need to keep the user from giving invalid folder names as a bundle's name. The rules for the name are:
	1) It must start with a word character (either a digit or letter)
	2) It can be followed by any number of letters, digits, whitespaces or hyphens
	3) It must end with a word character (either a digit or letter)

	A Regexp is used to check for the name validity.

* Moves away from projectbundle folder concept

	This commit regresses some functionality. Project bundles will be saved just as any other project, except they will also have the resources folder. It will be up to the user to organize the bundles on their own folders. It's currently not allowed to save a bundle on a folder where there's one already though (if there's a resources folder already). Later it might be allowed to overwrite bundles in that case.
	The projectbundles folder was dropped. The user can save project bundles anywhere in the system.
	The DataFile::writeBundle was removed. It's functionality was merged into the DataFile::writeFile method, by adding a boolean on the parameters defining whether it should be saved with resources. The logic of copying the resource files and changing the paths inside the project DataFile was moved to DataFile::copyResources, making the methods a little bit less dense.

* Adds an option to save project as bundle

	The "Save As" dialog now has an option to save project as a project bundle (with resources), which will save the file as a bundle.

Known bug:
	- Because the "local:" base prefix is translated to the filename from the Engine::getSong(), it breaks when Song::guiSaveProjectAs is called, because that method changes the project name before saving. Urgent fix!

* Fix local: prefix saving bug

	There was a bug where "local:" prefixes weren't resolved properly during saving because Song::guiSaveProjectAs() changed the project name to the destiny file name before saving. This resulted in the local paths using the destination file as a reference. Both Song::guiSaveProject() and Song::guiSaveProjectAs() were rewritten, and now they only rename the project after it's saved.

* Adds a warning message box

	When the user tries to save a project bundle on a folder that already has a project bundle (contains a resources folder) a message box pops up telling the user it's not permitted and that another path should be chosen.

* Removes unused header

	Forgot to remove <QRegExp> header when I removed the code that used it.

* Removes Vestige plugins bundling

	For safety reasons, remove the possibility to bundle VSTs loaded
through vestige. Also runs a safety check during the project being
loaded (Song::loadProject) to check if either Vestige plugins or effect
plugins are using local paths, and abort the project load if so. That is
to avoid malicious code being run because of bad DLLs being shipped with
a project file.

* Extracts code from loadProject to another method

	Extracts code that checks if a DataFile contains local paths to
plugins to another method inside DataFile.

* Removes debug warnings

	Removes warnings previously used for debugging. Improves a
warning message on PathUtil.

* Fixes small bug with error logging

	Fixes small bug, where a QMessageBox was being used to prompt an
error without checking if the gui is loaded first. Now we check for the
GUI and if we are in CLI mode we use a QTextStream instead.

* Saves the bundle in a newly created folder

	Now a folder with the project name is created inside which the
bundle will be saved. This makes the process more convenient.
	Some save errors that previously only triggered qWarnings now
trigger message boxes to warn the user of what happened (using a lambda
function that either shows message boxes or trigger qWarnings depending
whether a gui is present).
	Makes it so saving a bundle doesn't change the loaded project
path, that way the user won't be able to accidentally "Save" over a
bundle which should not be done for now.

* Enhances the name conflict workaround

	Now, instead of replacing the resource names with meaningless
numbers, the bundle save will just append a counter to the end of
filenames that have been repeated.

* Starts addressing Johannes review

* Adds makebundle action to bash completion file

	Adds the bash completion code for the made bundle action.

* Improves safety check on project files

	Now, instead of checking certain XML tags for local paths,
DataFile::hasLocalPlugin() will return true if ANY tag that isn't on the
RESOURCE_ELEMENTS list contains an attribute that starts with "local:".
	The method is now recursive so it can go through all XML tags
during this check.

* Addresses Spekular change request

	Uses basePrefix(Base::LocalDir) instead of "local:" on the
return of unresolved local paths.

* Makes hasLocalPlugins method const

* Replaces literal uses of "local:"

	Instead of using "local:" we are now retrieving the base prefix
from PathUtil, so if we change the prefix on the future we don't need to
replace every mention to it as well.

* Fix some comments on the header and cpp file

* Changes variable on PathUtil to const

	Changes the retrieved pointer to the song object to a const
pointer.

* Leave doxygen comment on CPP file only

	There was 2 doxygen comments for the same method, on the header
and CPP file. The latter was kept since it goes into more details about
the functionality of the method.

* Fix doxygen comment @param

	Fixes the doxygen comment from hasLocalPlugin().

* Remove assert statements

	Some assert statements were being done wrong and are probably
even unnecessary for that piece of code, so they were removed.

* Skips local paths when looking for shortest path

	PathUtil::toShortestRelative() was including the local paths on
the candidate paths, which could lead to a unallowed resource (i.e.:
vst plugin) to be assigned a local path even on a regular save.
	The local paths are now skipped when looking for the shortest
relative path, since they should only be used by the bundle save on the
allowed resources.

* Address Spekular's review

	Changes some of the PathUtil methods to allow a boolean pointer
to be used to return the status of the method, setting it to false if it
failed somewhere.
	Also adds a parameter to toShortestRelative to either allow or
forbid local paths in the search for the shortest relative path.

* Replaces "ok" with "error"
sdasda7777 pushed a commit to sdasda7777/lmms that referenced this pull request Jun 28, 2022
* Adds a baseDir for the local path

	Adds a new Base for paths called "local:", which will translate to the dir where the currently opened project file is at. In the future this will allow us to make project bundles and make it easier to export and transfer projects to other people without breaking the paths to samples, presets, plugins and others.

* Starts implementing the makeBundle functionality

	For now, to make a bundle LMMS has to be run through CLI with the makeBundle/--makeBundle command, followed by an input file and an output file ('lmms --makeBundle input.mmp output.mmp'). DataFile::writeBundle() is then called. For now, it only saves the mmp/mmpz file normally and also creates a "resources" folder if it doesn't exists. Later it will also manipulate the DataFile so all paths are local and copy all files to the resources folder.

TODO:
	-Remove warnings.
	-Implement the logic to manipulate the DataFile and copy files.

* Starts implementing logic to go through resources

	Starts implementing the logic that will go through all the resources of the project file and add them to the bundle. We use a std::map of QString to std::vector<QString>: The first string is the DOM element tagname that is going to be searched for. The vector of strings holds all attributes this element can have that accesses resources. For now we just print those to the screen.

* Adds logic to copy files and update the project

	The raw logic for creating the bundle is finished. It now copies the resource files and update the project to use "local:" paths to the new file now.
	Now it's a matter of organizing things and adding safety checks for file operation errors basically.

* Makes the writeBundle method more organized

	Improves comments and debugging warnings to make the writeBundle a bit more organized for review.

* Adds a project bundle folder

	Adds a project bundle folder, inside which the bundles will be created. Instead of receiving an output project file name, the makeBundle command now receives a bundle name that will be used as the name of the bundle's folder.
	Uses a typedef for the std::map with the tags and attributes with resources.

TODO:
	- Fix the local: prefix so it works when we don't have the project file open (for CLI usage, or find another way to deal with it).
	- Sanitize the bundle name.
	- Allow overwriting bundles?

* Handles local paths when a project isn't open

	The PathUtil base prefix conversion for "local:" uses the loaded song file name. When we are running the makebundle command from the CLI there isn't a loaded project, so those prefixes aren't converted properly. Now, when there isn't a project open PathUtil will return "local:" again when it tries to convert this base prefix. DataFile can then check if the base prefix is still there, and if it is it knows the conversion wasn't possible, so it does the conversion itself. To do that, a member called m_fileName was added to DataFile, which will hold the file being manipulated if that's where the DataFile originated from, and the local path can be retrieved from it.

* Sanitizes the bundle name

	The bundle name is now sanitized. Since it's going to be used as a folder name, we need to keep the user from giving invalid folder names as a bundle's name. The rules for the name are:
	1) It must start with a word character (either a digit or letter)
	2) It can be followed by any number of letters, digits, whitespaces or hyphens
	3) It must end with a word character (either a digit or letter)

	A Regexp is used to check for the name validity.

* Moves away from projectbundle folder concept

	This commit regresses some functionality. Project bundles will be saved just as any other project, except they will also have the resources folder. It will be up to the user to organize the bundles on their own folders. It's currently not allowed to save a bundle on a folder where there's one already though (if there's a resources folder already). Later it might be allowed to overwrite bundles in that case.
	The projectbundles folder was dropped. The user can save project bundles anywhere in the system.
	The DataFile::writeBundle was removed. It's functionality was merged into the DataFile::writeFile method, by adding a boolean on the parameters defining whether it should be saved with resources. The logic of copying the resource files and changing the paths inside the project DataFile was moved to DataFile::copyResources, making the methods a little bit less dense.

* Adds an option to save project as bundle

	The "Save As" dialog now has an option to save project as a project bundle (with resources), which will save the file as a bundle.

Known bug:
	- Because the "local:" base prefix is translated to the filename from the Engine::getSong(), it breaks when Song::guiSaveProjectAs is called, because that method changes the project name before saving. Urgent fix!

* Fix local: prefix saving bug

	There was a bug where "local:" prefixes weren't resolved properly during saving because Song::guiSaveProjectAs() changed the project name to the destiny file name before saving. This resulted in the local paths using the destination file as a reference. Both Song::guiSaveProject() and Song::guiSaveProjectAs() were rewritten, and now they only rename the project after it's saved.

* Adds a warning message box

	When the user tries to save a project bundle on a folder that already has a project bundle (contains a resources folder) a message box pops up telling the user it's not permitted and that another path should be chosen.

* Removes unused header

	Forgot to remove <QRegExp> header when I removed the code that used it.

* Removes Vestige plugins bundling

	For safety reasons, remove the possibility to bundle VSTs loaded
through vestige. Also runs a safety check during the project being
loaded (Song::loadProject) to check if either Vestige plugins or effect
plugins are using local paths, and abort the project load if so. That is
to avoid malicious code being run because of bad DLLs being shipped with
a project file.

* Extracts code from loadProject to another method

	Extracts code that checks if a DataFile contains local paths to
plugins to another method inside DataFile.

* Removes debug warnings

	Removes warnings previously used for debugging. Improves a
warning message on PathUtil.

* Fixes small bug with error logging

	Fixes small bug, where a QMessageBox was being used to prompt an
error without checking if the gui is loaded first. Now we check for the
GUI and if we are in CLI mode we use a QTextStream instead.

* Saves the bundle in a newly created folder

	Now a folder with the project name is created inside which the
bundle will be saved. This makes the process more convenient.
	Some save errors that previously only triggered qWarnings now
trigger message boxes to warn the user of what happened (using a lambda
function that either shows message boxes or trigger qWarnings depending
whether a gui is present).
	Makes it so saving a bundle doesn't change the loaded project
path, that way the user won't be able to accidentally "Save" over a
bundle which should not be done for now.

* Enhances the name conflict workaround

	Now, instead of replacing the resource names with meaningless
numbers, the bundle save will just append a counter to the end of
filenames that have been repeated.

* Starts addressing Johannes review

* Adds makebundle action to bash completion file

	Adds the bash completion code for the made bundle action.

* Improves safety check on project files

	Now, instead of checking certain XML tags for local paths,
DataFile::hasLocalPlugin() will return true if ANY tag that isn't on the
RESOURCE_ELEMENTS list contains an attribute that starts with "local:".
	The method is now recursive so it can go through all XML tags
during this check.

* Addresses Spekular change request

	Uses basePrefix(Base::LocalDir) instead of "local:" on the
return of unresolved local paths.

* Makes hasLocalPlugins method const

* Replaces literal uses of "local:"

	Instead of using "local:" we are now retrieving the base prefix
from PathUtil, so if we change the prefix on the future we don't need to
replace every mention to it as well.

* Fix some comments on the header and cpp file

* Changes variable on PathUtil to const

	Changes the retrieved pointer to the song object to a const
pointer.

* Leave doxygen comment on CPP file only

	There was 2 doxygen comments for the same method, on the header
and CPP file. The latter was kept since it goes into more details about
the functionality of the method.

* Fix doxygen comment @param

	Fixes the doxygen comment from hasLocalPlugin().

* Remove assert statements

	Some assert statements were being done wrong and are probably
even unnecessary for that piece of code, so they were removed.

* Skips local paths when looking for shortest path

	PathUtil::toShortestRelative() was including the local paths on
the candidate paths, which could lead to a unallowed resource (i.e.:
vst plugin) to be assigned a local path even on a regular save.
	The local paths are now skipped when looking for the shortest
relative path, since they should only be used by the bundle save on the
allowed resources.

* Address Spekular's review

	Changes some of the PathUtil methods to allow a boolean pointer
to be used to return the status of the method, setting it to false if it
failed somewhere.
	Also adds a parameter to toShortestRelative to either allow or
forbid local paths in the search for the shortest relative path.

* Replaces "ok" with "error"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants