diff --git a/src/AdvGeneral.cpp b/src/AdvGeneral.cpp index 78c122c1..c51107d1 100644 --- a/src/AdvGeneral.cpp +++ b/src/AdvGeneral.cpp @@ -163,6 +163,7 @@ END_MESSAGE_MAP() #define SETTING_HIDE_TASKBAR_ICON_ON_CLOSE 109 #define SETTING_USE_MODERN_SCROLLBAR 110 #define SETTING_ENFORCE_CLIPBOARD_IGNORE_FORMATS 111 +#define SETTING_EXPIRED_IMAGE_DAYS 112 BOOL CAdvGeneral::OnInitDialog() { @@ -217,6 +218,8 @@ BOOL CAdvGeneral::OnInitDialog() pGroupTest->AddSubItem(new CMFCPropertyGridProperty(_T("Default paste string"), CGetSetOptions::GetDefaultPasteString(), _T(""), SETTING_DEFAULT_PASTE_STRING)); pGroupTest->AddSubItem(new CMFCPropertyGridProperty(_T("Default copy string"), CGetSetOptions::GetDefaultCopyString(), _T(""), SETTING_DEFAULT_COPY_STRING)); pGroupTest->AddSubItem(new CMFCPropertyGridProperty(_T("Default cut string"), CGetSetOptions::GetDefaultCutString(), _T(""), SETTING_DEFAULT_CUT_STRING)); + + pGroupTest->AddSubItem(new CMFCPropertyGridProperty(_T("Delete images (CF_DIB, PNG) from clips not used in x days (0 = never)"), CGetSetOptions::GetExpiredImageDays(), _T("Removes CF_DIB, CF_DIBV5 and PNG data from clips that have not been used in this many days, other formats of the clip are kept. Clips that only contain image data are deleted. Clips with shortcuts, in groups, sticky or set to never auto delete are not affected."), SETTING_EXPIRED_IMAGE_DAYS)); static TCHAR BASED_CODE szDiffFilter[] = _T("Diff Applications(*.exe)|*.exe||"); CMFCPropertyGridFileProperty* pDiffProp = new CMFCPropertyGridFileProperty(_T("Diff application path"), TRUE, CGetSetOptions::GetDiffApp(), _T("exe"), 0, szDiffFilter, (LPCTSTR)0, SETTING_DIFF_APP); @@ -999,6 +1002,12 @@ void CAdvGeneral::OnBnClickedOk() CGetSetOptions::SetEnforceClipboardIgnoreFormats(val); } break; + case SETTING_EXPIRED_IMAGE_DAYS: + if (pNewValue->lVal != pOrigValue->lVal) + { + CGetSetOptions::SetExpiredImageDays(max(pNewValue->lVal, 0)); + } + break; } } } diff --git a/src/DatabaseUtilities.cpp b/src/DatabaseUtilities.cpp index 3c69d475..05deb734 100644 --- a/src/DatabaseUtilities.cpp +++ b/src/DatabaseUtilities.cpp @@ -841,6 +841,74 @@ BOOL RepairDatabase() return TRUE; } +//Images are by far the largest items in the database, remove the image formats from clips that have not been used +//in the set number of days. Other formats of the clip are kept, clips that only had image data are deleted. +static void RemoveExpiredImages(CppSQLite3DB &db, bool checkIdleTime) +{ + long expireDays = CGetSetOptions::GetExpiredImageDays(); + if (expireDays <= 0) + return; + + //deleting large blobs can lock the db for a while, only do it when idle and in batches + if (checkIdleTime && IdleSeconds() <= CGetSetOptions::GetIdleSecondsBeforeDelete()) + { + Log(_T("Computer has not been idle long enough to remove expired images")); + return; + } + + CTime expireTime = CTime::GetCurrentTime(); + expireTime -= CTimeSpan(expireDays, 0, 0, 0); + + const TCHAR *imageFormats = _T("'CF_DIB', 'CF_DIBV5', 'PNG'"); + + std::vector clipIds; + { + CppSQLite3Query q = db.execQueryEx(_T("SELECT DISTINCT Main.lID FROM Main ") + _T("INNER JOIN Data ON Data.lParentID = Main.lID ") + _T("WHERE Main.lastPasteDate < %d AND Data.strClipBoardFormat IN (%s) AND ") + _T("Main.bIsGroup = 0 AND Main.lShortCut = 0 AND Main.lParentID <= 0 AND Main.lDontAutoDelete = 0 AND Main.stickyClipOrder = -(2147483647) AND Main.stickyClipGroupOrder = -(2147483647) ") + _T("LIMIT %d"), (int)expireTime.GetTime(), imageFormats, CGetSetOptions::GetMainDeletesDeleteCount()); + + while (q.eof() == false) + { + clipIds.push_back(q.getIntField(_T("lID"))); + q.nextRow(); + } + } + + CClipIDs emptyClipIds; + std::vector changedClipIds; + + for (int clipId : clipIds) + { + int deleteCount = db.execDMLEx(_T("DELETE FROM Data WHERE lParentID = %d AND strClipBoardFormat IN (%s)"), clipId, imageFormats); + + Log(StrF(_T("From Images Expire - Deleted %d image formats from Id: %d"), deleteCount, clipId)); + + if (db.execScalarEx(_T("SELECT COUNT(lID) FROM Data WHERE lParentID = %d"), clipId) == 0) + { + emptyClipIds.Add(clipId); + } + else + { + //the clip no longer holds everything that was copied, clear the CRC so copying the + //same thing again adds a new clip with the image rather than moving this one to the top + db.execDMLEx(_T("UPDATE Main SET CRC = 0 WHERE lID = %d"), clipId); + changedClipIds.push_back(clipId); + } + } + + if (emptyClipIds.GetCount() > 0) + { + emptyClipIds.DeleteIDs(false, db); + } + + for (int clipId : changedClipIds) + { + theApp.RefreshClipInUI(clipId, UPDATE_CLIP_DESCRIPTION); + } +} + BOOL RemoveOldEntries(bool checkIdleTime) { Log(StrF(_T("Beginning of RemoveOldEntries MaxEntries: %d - Keep days: %d"), CGetSetOptions::GetMaxEntries(), CGetSetOptions::GetExpiredEntries())); @@ -921,6 +989,8 @@ BOOL RemoveOldEntries(bool checkIdleTime) } } + RemoveExpiredImages(db, checkIdleTime); + int toDeleteCount = db.execScalar(_T("SELECT COUNT(clipID) FROM MainDeletes")); Log(StrF(_T("Before Deleting emptied out data, count: %d, Idle Seconds: %f"), toDeleteCount, IdleSeconds())); diff --git a/src/Options.cpp b/src/Options.cpp index 3bb1a932..3a9b1366 100644 --- a/src/Options.cpp +++ b/src/Options.cpp @@ -1089,6 +1089,16 @@ long CGetSetOptions::GetExpiredEntries() return GetProfileLong("ExpiredEntries", 5); } +void CGetSetOptions::SetExpiredImageDays(long lVal) +{ + SetProfileLong("ExpiredImageDays", lVal); +} + +long CGetSetOptions::GetExpiredImageDays() +{ + return GetProfileLong("ExpiredImageDays", 0); +} + void CGetSetOptions::SetTripCopyCount(long lVal) { // negative means a relative offset diff --git a/src/Options.h b/src/Options.h index 9f1196f7..eaba4e96 100644 --- a/src/Options.h +++ b/src/Options.h @@ -149,6 +149,9 @@ class CGetSetOptions static void SetExpiredEntries(long lVal); static long GetExpiredEntries(); + static void SetExpiredImageDays(long lVal); + static long GetExpiredImageDays(); + static void SetTripCopyCount(long lVal); static long GetTripCopyCount();