Extract and improve code to find missing files (#1940).
authorCarl Hetherington <cth@carlh.net>
Sat, 25 Dec 2021 01:05:54 +0000 (02:05 +0100)
committerCarl Hetherington <cth@carlh.net>
Sun, 1 May 2022 22:31:04 +0000 (00:31 +0200)
src/lib/find_missing.cc [new file with mode: 0644]
src/lib/find_missing.h [new file with mode: 0644]
src/lib/wscript
src/wx/content_menu.cc
src/wx/content_menu.h
test/find_missing_test.cc [new file with mode: 0644]
test/wscript

diff --git a/src/lib/find_missing.cc b/src/lib/find_missing.cc
new file mode 100644 (file)
index 0000000..3d61e74
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+    Copyright (C) 2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "content.h"
+#include "find_missing.h"
+#include "util.h"
+#include <boost/filesystem.hpp>
+
+
+using std::map;
+using std::shared_ptr;
+using std::vector;
+
+
+typedef map<shared_ptr<Content>, vector<boost::filesystem::path>> Replacements;
+
+
+static
+void
+search (Replacements& replacement_paths, boost::filesystem::path directory, int depth = 0)
+{
+       for (auto candidate: boost::filesystem::directory_iterator(directory)) {
+               if (boost::filesystem::is_regular_file(candidate.path())) {
+                       for (auto& replacement: replacement_paths) {
+                               for (auto& path: replacement.second) {
+                                       if (!boost::filesystem::exists(path) && path.filename() == candidate.path().filename()) {
+                                               path = candidate.path();
+                                       }
+                               }
+                       }
+               } else if (boost::filesystem::is_directory(candidate.path()) && depth <= 2) {
+                       search (replacement_paths, candidate, depth + 1);
+               }
+       }
+}
+
+
+void
+dcpomatic::find_missing (vector<shared_ptr<Content>> content_to_fix, boost::filesystem::path clue)
+{
+       using namespace boost::filesystem;
+
+       Replacements replacement_paths;
+       for (auto content: content_to_fix) {
+               replacement_paths[content] = content->paths();
+       }
+
+       search (replacement_paths, is_directory(clue) ? clue : clue.parent_path());
+
+       for (auto content: content_to_fix) {
+               auto const& repl = replacement_paths[content];
+               bool const replacements_exist = std::find_if(repl.begin(), repl.end(), [](path p) { return !exists(p); }) == repl.end();
+               if (replacements_exist && simple_digest(replacement_paths[content]) == content->digest()) {
+                       content->set_paths (repl);
+               }
+       }
+}
diff --git a/src/lib/find_missing.h b/src/lib/find_missing.h
new file mode 100644 (file)
index 0000000..6755b5d
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+    Copyright (C) 2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include <boost/filesystem.hpp>
+#include <memory>
+#include <vector>
+
+
+class Content;
+
+
+namespace dcpomatic {
+
+
+/** Try to resolve some missing content file paths using a clue.  On return
+ *  any content whose files were found will have been updated.
+ *
+ *  @param content Content, some of which may have missing files.
+ *  @param clue Path to a file which gives a clue about where the missing files might be.
+ */
+void find_missing (std::vector<std::shared_ptr<Content>> content, boost::filesystem::path clue);
+
+
+}
+
index 30240cd117efa62fd66cc82cbe52cdde59a6290f..22fb144bc4b8598651468b5d182fe874f55fdb61 100644 (file)
@@ -98,6 +98,7 @@ sources = """
           file_group.cc
           file_log.cc
           filter_graph.cc
+          find_missing.cc
           ffmpeg.cc
           ffmpeg_audio_stream.cc
           ffmpeg_content.cc
index 18ce64475b7c765d8e54aa3d7f79b84b19e94b65..a80a5fc82e1dffda4a3a0019e5ddf02d186d68fc 100644 (file)
@@ -354,35 +354,11 @@ ContentMenu::find_missing ()
                d->Destroy ();
        }
 
-       list<shared_ptr<Content>> content;
-
-       if (r == wxID_OK) {
-               if (dc) {
-                       content.push_back (make_shared<DCPContent>(path));
-               } else {
-                       content = content_factory (path);
-               }
-       }
-
-       if (content.empty ()) {
+       if (r == wxID_CANCEL) {
                return;
        }
 
-       for (auto i: content) {
-               auto j = make_shared<ExamineContentJob>(film, i);
-
-               j->Finished.connect (
-                       bind (
-                               &ContentMenu::maybe_found_missing,
-                               this,
-                               std::weak_ptr<Job> (j),
-                               std::weak_ptr<Content> (_content.front ()),
-                               std::weak_ptr<Content> (i)
-                               )
-                       );
-
-               JobManager::instance()->add (j);
-       }
+       dcpomatic::find_missing (film->content(), path);
 }
 
 void
@@ -398,26 +374,6 @@ ContentMenu::re_examine ()
        }
 }
 
-void
-ContentMenu::maybe_found_missing (weak_ptr<Job> j, weak_ptr<Content> oc, weak_ptr<Content> nc)
-{
-       auto job = j.lock ();
-       if (!job || !job->finished_ok ()) {
-               return;
-       }
-
-       auto old_content = oc.lock ();
-       auto new_content = nc.lock ();
-       DCPOMATIC_ASSERT (old_content);
-       DCPOMATIC_ASSERT (new_content);
-
-       if (new_content->digest() != old_content->digest()) {
-               error_dialog (0, _("The content file(s) you specified are not the same as those that are missing.  Either try again with the correct content file or remove the missing content."));
-               return;
-       }
-
-       old_content->set_paths (new_content->paths());
-}
 
 void
 ContentMenu::kdm ()
index 6e1641e66587529ce2609f2a81d53b7e9da9e211..ebbdf5b60e254021165eaa54b53ad491fb21413b 100644 (file)
@@ -61,7 +61,6 @@ private:
        void ov ();
        void set_dcp_settings ();
        void remove ();
-       void maybe_found_missing (std::weak_ptr<Job>, std::weak_ptr<Content>, std::weak_ptr<Content>);
        void cpl_selected (wxCommandEvent& ev);
 
        wxMenu* _menu;
diff --git a/test/find_missing_test.cc b/test/find_missing_test.cc
new file mode 100644 (file)
index 0000000..1bc4bef
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+    Copyright (C) 2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/content.h"
+#include "lib/content_factory.h"
+#include "lib/dcp_content.h"
+#include "lib/film.h"
+#include "lib/find_missing.h"
+#include "test.h"
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::make_shared;
+using std::string;
+
+
+BOOST_AUTO_TEST_CASE (find_missing_test_with_single_files)
+{
+       using namespace boost::filesystem;
+
+       auto name = string{"find_missing_test_with_single_files"};
+
+       /* Make a directory with some content */
+       auto content_dir = path("build/test") / path(name + "_content");
+       remove_all (content_dir);
+       create_directories (content_dir);
+       copy_file ("test/data/flat_red.png", content_dir / "A.png");
+       copy_file ("test/data/flat_red.png", content_dir / "B.png");
+       copy_file ("test/data/flat_red.png", content_dir / "C.png");
+
+       /* Make a film with that content */
+       auto film = new_test_film2 (name + "_film", {
+               content_factory(content_dir / "A.png").front(),
+               content_factory(content_dir / "B.png").front(),
+               content_factory(content_dir / "C.png").front()
+               });
+       film->write_metadata ();
+
+       /* Move the content somewhere eles */
+       auto moved = path("build/test") / path(name + "_moved");
+       remove_all (moved);
+       rename (content_dir, moved);
+
+       /* That should make the content paths invalid */
+       for (auto content: film->content()) {
+               BOOST_CHECK (!content->paths_valid());
+       }
+
+       /* Fix the missing files and check the result */
+       dcpomatic::find_missing (film->content(), moved / "A.png");
+
+       for (auto content: film->content()) {
+               BOOST_CHECK (content->paths_valid());
+       }
+}
+
+
+BOOST_AUTO_TEST_CASE (find_missing_test_with_multiple_files)
+{
+       using namespace boost::filesystem;
+
+       auto name = string{"find_missing_test_with_multiple_files"};
+
+       /* Copy an arbitrary DCP into a test directory */
+       auto content_dir = path("build/test") / path(name + "_content");
+       remove_all (content_dir);
+       create_directories (content_dir);
+       for (auto ref: directory_iterator("test/data/scaling_test_133_185")) {
+               copy (ref, content_dir / ref.path().filename());
+       }
+
+       /* Make a film containing that DCP */
+       auto film = new_test_film2 (name + "_film", { make_shared<DCPContent>(content_dir) });
+       film->write_metadata ();
+
+       /* Move the DCP's content elsewhere */
+       auto moved = path("build/test") / path(name + "_moved");
+       remove_all (moved);
+       rename (content_dir, moved);
+
+       /* That should make the content paths invalid */
+       for (auto content: film->content()) {
+               BOOST_CHECK (!content->paths_valid());
+       }
+
+       /* Fix the missing files and check the result */
+       dcpomatic::find_missing (film->content(), moved / "foo");
+
+       for (auto content: film->content()) {
+               BOOST_CHECK (content->paths_valid());
+       }
+}
+
+
+BOOST_AUTO_TEST_CASE (find_missing_test_with_multiple_files_one_incorrect)
+{
+       using namespace boost::filesystem;
+
+       auto name = string{"find_missing_test_with_multiple_files_one_incorrect"};
+
+       /* Copy an arbitrary DCP into a test directory */
+       auto content_dir = path("build/test") / path(name + "_content");
+       remove_all (content_dir);
+       create_directories (content_dir);
+       for (auto ref: directory_iterator("test/data/scaling_test_133_185")) {
+               copy (ref, content_dir / ref.path().filename());
+       }
+
+       /* Make a film containing that DCP */
+       auto film = new_test_film2 (name + "_film", { make_shared<DCPContent>(content_dir) });
+       film->write_metadata ();
+
+       /* Move the DCP's content elsewhere */
+       auto moved = path("build/test") / path(name + "_moved");
+       remove_all (moved);
+       rename (content_dir, moved);
+
+       /* Corrupt one of the files in the moved content, so that it should not be found in the find_missing
+        * step
+        */
+       remove (moved / "cpl_80daeb7a-57d8-4a70-abeb-cd92ddac1527.xml");
+       copy ("test/data/scaling_test_133_185/ASSETMAP.xml", moved / "cpl_80daeb7a-57d8-4a70-abeb-cd92ddac1527.xml");
+
+       /* The film's contents should be invalid */
+       for (auto content: film->content()) {
+               BOOST_CHECK (!content->paths_valid());
+       }
+
+       dcpomatic::find_missing (film->content(), moved / "foo");
+
+       /* And even after find_missing there should still be missing content */
+       for (auto content: film->content()) {
+               BOOST_CHECK (!content->paths_valid());
+       }
+}
+
index 4e28f1740c1223640ac37b065501cede1e61faf1..1555f2f59cd019dc3140a388a0ba2256a3cf5965 100644 (file)
@@ -89,6 +89,7 @@ def build(bld):
                  file_log_test.cc
                  file_naming_test.cc
                  film_metadata_test.cc
+                 find_missing_test.cc
                  frame_interval_checker_test.cc
                  frame_rate_test.cc
                  guess_crop_test.cc