tracktion-engine 3.0-10-g034fdde4aa5
Tracktion Engine — High level data model for audio applications

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_TemporaryFileManager.cpp
Go to the documentation of this file.
1 /*
2 ,--. ,--. ,--. ,--.
3 ,-' '-.,--.--.,--,--.,---.| |,-.,-' '-.`--' ,---. ,--,--, Copyright 2024
4 '-. .-'| .--' ,-. | .--'| /'-. .-',--.| .-. || \ Tracktion Software
5 | | | | \ '-' \ `--.| \ \ | | | |' '-' '| || | Corporation
6 `---' `--' `--`--'`---'`--'`--' `---' `--' `---' `--''--' www.tracktion.com
7
8 Tracktion Engine uses a GPL/commercial licence - see LICENCE.md for details.
9*/
10
11namespace tracktion { inline namespace engine
12{
13
15{
16 updateDir();
17}
18
19TemporaryFileManager::~TemporaryFileManager()
20{
21}
22
23static juce::File getDefaultTempFolder (Engine& engine)
24{
25 return engine.getPropertyStorage().getAppCacheFolder().getChildFile ("Temporary");
26}
27
28const juce::File& TemporaryFileManager::getTempDirectory() const
29{
30 return tempDir;
31}
32
33bool TemporaryFileManager::setTempDirectory (const juce::File& newFile)
34{
35 auto defaultDir = getDefaultTempFolder (engine);
36
37 if (newFile == defaultDir)
38 {
39 ressetToDefaultLocation();
40 }
41 else
42 {
43 auto path = newFile.getFullPathName();
44 auto parent = defaultDir.getParentDirectory();
45
46 if (newFile.isAChildOf (parent))
47 path = newFile.getRelativePathFrom (parent);
48
49 engine.getPropertyStorage().setProperty (SettingID::tempDirectory, path);
50 updateDir();
51
52 jassert (newFile == tempDir);
53 }
54
55 return wasTempFolderSuccessfullyCreated();
56}
57
58void TemporaryFileManager::ressetToDefaultLocation()
59{
60 tempDir = getDefaultTempFolder (engine);
61 engine.getPropertyStorage().removeProperty (SettingID::tempDirectory);
62}
63
64void TemporaryFileManager::updateDir()
65{
66 auto defaultDir = getDefaultTempFolder (engine);
67 auto userFolder = engine.getPropertyStorage().getProperty (SettingID::tempDirectory).toString().trim();
68
69 if (userFolder.isEmpty())
70 ressetToDefaultLocation();
71 else
72 tempDir = defaultDir.getSiblingFile (userFolder);
73
74 if (! wasTempFolderSuccessfullyCreated())
75 {
76 tempDir = defaultDir;
77 engine.getPropertyStorage().removeProperty (SettingID::tempDirectory);
78 }
79}
80
81//==============================================================================
82bool TemporaryFileManager::wasTempFolderSuccessfullyCreated() const
83{
84 return (tempDir.isDirectory() || tempDir.createDirectory())
85 && tempDir.hasWriteAccess();
86}
87
88bool TemporaryFileManager::isDiskSpaceDangerouslyLow() const
89{
90 auto freeMb = tempDir.getBytesFreeOnVolume() / (1024 * 1024);
91 return freeMb < 80;
92}
93
94int64_t TemporaryFileManager::getMaxSpaceAllowedForTempFiles() const
95{
96 int64_t minAbsoluteSize = 1024 * 1024 * 750;
97 int64_t minProportionOfDisk = tempDir.getBytesFreeOnVolume() / 4;
98
99 return std::min (minProportionOfDisk, minAbsoluteSize);
100}
101
102int TemporaryFileManager::getMaxNumTempFiles() const
103{
104 return 1000;
105}
106
107static bool shouldDeleteTempFile (const juce::File& f, bool spaceIsShort)
108{
109 auto fileName = f.getFileName();
110
111 if (fileName.startsWith ("preview_"))
112 return false;
113
114 if (fileName.startsWith ("temp_"))
115 return true;
116
117 auto daysOld = (juce::Time::getCurrentTime() - f.getLastAccessTime()).inDays();
118
119 return daysOld > 60.0 || (spaceIsShort && daysOld > 1.0);
120}
121
122static void deleteEditPreviewsNotInUse (Engine& engine, juce::Array<juce::File>& files)
123{
124 auto& pm = engine.getProjectManager();
126
127 for (auto& p : pm.getAllProjects (pm.folders))
128 for (auto& itemID : p->getAllProjectItemIDs())
129 ids.add (itemID.toStringSuitableForFilename());
130
131 for (int i = files.size(); --i >= 0;)
132 {
133 auto fileName = files.getUnchecked (i).getFileNameWithoutExtension();
134
135 if (! fileName.startsWith ("preview_"))
136 continue;
137
138 auto itemID = fileName.fromFirstOccurrenceOf ("preview_", false, false);
139
140 if (itemID.isNotEmpty() && ! ids.contains (itemID))
141 files.removeAndReturn (i).deleteFile();
142 }
143}
144
145void TemporaryFileManager::cleanUp()
146{
148 TRACKTION_LOG ("Cleaning up temp files..");
149
150 auto tempFiles = tempDir.findChildFiles (juce::File::findFiles, true);
151
152 deleteEditPreviewsNotInUse (engine, tempFiles);
153
154 int64_t totalBytes = 0;
155
156 for (auto& f : tempFiles)
157 totalBytes += f.getSize();
158
159 std::sort (tempFiles.begin(), tempFiles.end(),
160 [] (const juce::File& first, const juce::File& second) -> bool
161 {
162 return first.getLastAccessTime().toMilliseconds() < second.getLastAccessTime().toMilliseconds();
163 });
164
165 auto numFiles = tempFiles.size();
166 auto maxNumFiles = getMaxNumTempFiles();
167 auto maxSizeToKeep = getMaxSpaceAllowedForTempFiles();
168
169 for (auto& f : tempFiles)
170 {
171 if (shouldDeleteTempFile (f, totalBytes > maxSizeToKeep || numFiles > maxNumFiles))
172 {
173 totalBytes -= f.getSize();
174 f.deleteFile();
175 --numFiles;
176 }
177 }
178
179 for (auto entry : juce::RangedDirectoryIterator (getTempDirectory(), false, "edit_*", juce::File::findDirectories))
180 if (entry.getFile().getNumberOfChildFiles (juce::File::findFilesAndDirectories) == 0)
181 entry.getFile().deleteRecursively();
182}
183
184juce::File TemporaryFileManager::getTempFile (const juce::String& filename) const
185{
186 return tempDir.getChildFile (filename);
187}
188
189juce::File TemporaryFileManager::getUniqueTempFile (const juce::String& prefix, const juce::String& ext) const
190{
191 return tempDir.getChildFile (prefix + juce::String::toHexString (juce::Random::getSystemRandom().nextInt64()))
192 .withFileExtension (ext)
194}
195
196juce::File TemporaryFileManager::getThumbnailsFolder() const
197{
198 return getTempDirectory().getChildFile ("thumbnails");
199}
200
201//==============================================================================
202static juce::String getClipProxyPrefix() { return "clip_"; }
203static juce::String getFileProxyPrefix() { return "proxy_"; }
204static juce::String getDeviceFreezePrefix (Edit& edit) { return "freeze_" + edit.getProjectItemID().toStringSuitableForFilename() + "_"; }
205static juce::String getTrackFreezePrefix() { return "trackFreeze_"; }
206static juce::String getCompPrefix() { return "comp_"; }
207
208static AudioFile getCachedEditFile (Edit& edit, const juce::String& prefix, HashCode hash)
209{
210 return AudioFile (edit.engine, edit.getTempDirectory (true).getChildFile (prefix + juce::String::toHexString (hash) + ".wav"));
211}
212
213static AudioFile getCachedClipFileWithPrefix (const AudioClipBase& clip, const juce::String& prefix, HashCode hash)
214{
215 return getCachedEditFile (clip.edit, prefix + "0_" + clip.itemID.toString() + "_", hash);
216}
217
218AudioFile TemporaryFileManager::getFileForCachedClipRender (const AudioClipBase& clip, HashCode hash)
219{
220 return getCachedClipFileWithPrefix (clip, getClipProxyPrefix(), hash);
221}
222
223AudioFile TemporaryFileManager::getFileForCachedCompRender (const AudioClipBase& clip, HashCode takeHash)
224{
225 return getCachedClipFileWithPrefix (clip, getCompPrefix(), takeHash);
226}
227
228AudioFile TemporaryFileManager::getFileForCachedFileRender (Edit& edit, HashCode hash)
229{
230 return getCachedEditFile (edit, getFileProxyPrefix(), hash);
231}
232
233juce::File TemporaryFileManager::getFreezeFileForDevice (Edit& edit, OutputDevice& device)
234{
235 return edit.getTempDirectory (true)
236 .getChildFile (getDeviceFreezePrefix (edit) + juce::String (device.getDeviceID()) + ".freeze");
237}
238
239juce::String TemporaryFileManager::getDeviceIDFromFreezeFile (Edit& edit, const juce::File& deviceFreezeFile)
240{
241 const auto fileName = deviceFreezeFile.getFileName();
242 jassert (fileName.startsWith (getDeviceFreezePrefix (edit)));
243 jassert (fileName.endsWith (".freeze"));
244
245 return fileName.fromLastOccurrenceOf (getDeviceFreezePrefix (edit), false, false)
246 .upToFirstOccurrenceOf (".", false, false);
247}
248
249juce::File TemporaryFileManager::getFreezeFileForTrack (const AudioTrack& track)
250{
251 return track.edit.getTempDirectory (true)
252 .getChildFile (getTrackFreezePrefix() + "0_" + track.itemID.toString() + ".freeze");
253}
254
255juce::Array<juce::File> TemporaryFileManager::getFrozenTrackFiles (Edit& edit)
256{
257 return edit.getTempDirectory (false)
258 .findChildFiles (juce::File::findFiles, false, getDeviceFreezePrefix (edit) + "*");
259}
260
261static ProjectItemID getProjectItemIDFromFilename (const juce::String& name)
262{
263 auto tokens = juce::StringArray::fromTokens (name.fromFirstOccurrenceOf ("_", false, false), "_", {});
264
265 if (tokens.size() <= 1)
266 return {};
267
268 return ProjectItemID (tokens[0] + "_" + tokens[1]);
269}
270
271void TemporaryFileManager::purgeOrphanEditTempFolders (ProjectManager& pm)
272{
274 juce::Array<juce::File> filesToDelete;
275 juce::StringArray reasonsForDeletion;
276
277 for (auto entry : juce::RangedDirectoryIterator (getTempDirectory(), false, "edit_*", juce::File::findDirectories))
278 {
279 auto itemID = getProjectItemIDFromFilename (entry.getFile().getFileName());
280
281 if (itemID.isValid())
282 {
283 if (pm.getProjectItem (itemID) == nullptr)
284 {
285 filesToDelete.add (entry.getFile());
286
287 auto pp = pm.getProject (itemID.getProjectID());
288
289 if (itemID.getProjectID() == 0)
290 reasonsForDeletion.add ("Invalid project ID");
291 else if (pp == nullptr)
292 reasonsForDeletion.add ("Can't find project");
293 else if (pp->getProjectItemForID (itemID) == nullptr)
294 reasonsForDeletion.add ("Can't find source media");
295 else
296 reasonsForDeletion.add ("Unknown");
297 }
298 }
299 }
300
301 for (int i = filesToDelete.size(); --i >= 0;)
302 {
303 TRACKTION_LOG ("Purging edit folder: " + filesToDelete.getReference (i).getFileName() + " - " + reasonsForDeletion[i]);
304 filesToDelete.getReference (i).deleteRecursively();
305 }
306}
307
308static EditItemID getEditItemIDFromFilename (const juce::String& name)
309{
310 auto tokens = juce::StringArray::fromTokens (name.fromFirstOccurrenceOf ("_", false, false), "_", {});
311
312 if (tokens.isEmpty())
313 return {};
314
315 // TODO: think about backwards-compatibility for these strings - was a two-part
316 // ID separated by an underscore
317 return EditItemID::fromVar (tokens[0] + tokens[1]);
318}
319
320void TemporaryFileManager::purgeOrphanFreezeAndProxyFiles (Edit& edit)
321{
323 juce::Array<juce::File> filesToDelete;
324
325 for (auto entry : juce::RangedDirectoryIterator (edit.getTempDirectory (false), false, "*"))
326 {
327 auto name = entry.getFile().getFileName();
328 auto itemID = getEditItemIDFromFilename (name);
329
330 if (itemID.isValid())
331 {
332 if (name.startsWith (getClipProxyPrefix())
333 || name.startsWith (getCompPrefix()))
334 {
335 auto clip = findClipForID (edit, itemID);
336
337 if (auto acb = dynamic_cast<AudioClipBase*> (clip))
338 {
339 if (! acb->isUsingFile (AudioFile (edit.engine, entry.getFile())))
340 filesToDelete.add (entry.getFile());
341 }
342 else if (clip == nullptr)
343 {
344 filesToDelete.add (entry.getFile());
345 }
346 }
347 else if (name.startsWith (getFileProxyPrefix()))
348 {
349 // XXX
350 }
351 else if (name.startsWith (getTrackFreezePrefix()))
352 {
353 if (auto at = findAudioTrackForID (edit, itemID))
354 if (! at->isFrozen (Track::individualFreeze))
355 filesToDelete.add (entry.getFile());
356 }
357 }
358 else if (name.startsWith (RenderManager::getFileRenderPrefix()))
359 {
360 if (! edit.areAnyClipsUsingFile (AudioFile (edit.engine, entry.getFile())))
361 filesToDelete.add (entry.getFile());
362 }
363 }
364
365 for (auto& f : filesToDelete)
366 {
367 DBG ("Purging temp file: " << f.getFileName());
368 AudioFile (edit.engine, f).deleteFile();
369 }
370}
371
372}} // namespace tracktion { inline namespace engine
ElementType getUnchecked(int index) const
int size() const noexcept
ElementType removeAndReturn(int indexToRemove)
bool isDirectory() const
Array< File > findChildFiles(int whatToLookFor, bool searchRecursively, const String &wildCardPattern="*", FollowSymlinks followSymlinks=FollowSymlinks::yes) const
int64 getBytesFreeOnVolume() const
bool hasWriteAccess() const
int64 getSize() const
const String & getFullPathName() const noexcept
String getFileName() const
File getChildFile(StringRef relativeOrAbsolutePath) const
File getSiblingFile(StringRef siblingFileName) const
File getNonexistentSibling(bool putNumbersInBrackets=true) const
String getRelativePathFrom(const File &directoryToBeRelativeTo) const
File withFileExtension(StringRef newExtension) const
bool deleteFile() const
bool isAChildOf(const File &potentialParentDirectory) const
Time getLastAccessTime() const
Result createDirectory() const
const String & toString() const noexcept
static Random & getSystemRandom() noexcept
bool contains(StringRef stringToLookFor, bool ignoreCase=false) const
static StringArray fromTokens(StringRef stringToTokenise, bool preserveQuotedStrings)
void add(String stringToAdd)
static String toHexString(IntegerType number)
static Time JUCE_CALLTYPE getCurrentTime() noexcept
The Engine is the central class for all tracktion sessions.
PropertyStorage & getPropertyStorage() const
Returns the PropertyStorage user settings customisable XML file.
static juce::StringRef getFileRenderPrefix()
Returns the prefix used for render files.
TemporaryFileManager(Engine &)
You shouldn't have to ever create your own instance of this class - the Engine itself has a Temporary...
@ individualFreeze
Freezes a track in to a single audio file.
#define jassert(expression)
#define DBG(textToWrite)
T min(T... args)
Clip * findClipForID(ClipOwner &co, EditItemID id)
Returns a clip with the given ID if the ClipOwner contains it.
AudioTrack * findAudioTrackForID(const Edit &edit, EditItemID id)
Returns the AudioTrack with a given ID if contained in the Edit.
T sort(T... args)
typedef int64_t
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.