Backup URL scheme

This commit is contained in:
relikd
2019-09-23 17:38:31 +02:00
parent 6da852f2c9
commit 2c028e79e0
16 changed files with 113 additions and 48 deletions

View File

@@ -25,7 +25,7 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
- *UI:* Accessibility hints for most UI elements
- *UI*: Show welcome message upon first usage (empty db)
- Welcome message also adds Github releases feed
- Config URL scheme `barss:` with `open/preferences` and `config/fixcache`
- Config URL scheme `barss:` with `open/preferences`, `config/fixcache`, and `backup/show`
### Fixed
- *Adding feed:* Show proper HTTP status code error message (4xx and 5xx)

View File

@@ -28,11 +28,13 @@
#import "BarStatusItem.h"
#import "UpdateScheduler.h"
#import "StoreCoordinator.h"
#import "OpmlFile.h"
#import "SettingsFeeds+DragDrop.h"
#import "NSURL+Ext.h"
#import "NSDate+Ext.h"
#import "NSError+Ext.h"
@interface AppHook()
@interface AppHook() <OpmlFileExportDelegate>
@property (strong) NSWindowController *prefWindow;
@end
@@ -203,6 +205,7 @@
@textblock
barss:open/preferences[/0-4]
barss:config/fixcache[/silent]
barss:backup[/show]
@/textblock
*/
- (void)handleConfigURLScheme:(const NSString*)action parameters:(NSArray<NSString*>*)params {
@@ -215,9 +218,24 @@
if ([params.firstObject isEqualToString:kURLParamFixCache]) {
[StoreCoordinator cleanupAndShowAlert:![params.lastObject isEqualToString:kURLParamSilent]];
}
} else if ([action isEqualToString:kURLActionBackup]) {
NSURL *baseURL = [NSURL backupPathURL];
[baseURL mkdir]; // non destructive make dir
NSURL *dest = [baseURL file:[@"feeds_" stringByAppendingString:[NSDate dayStringISO8601]] ext:@"opml"];
NSURL *sym = [baseURL file:@"feeds_latest" ext:@"opml"];
[sym remove]; // remove old sym link, otherwise won't be updated
[[NSFileManager defaultManager] createSymbolicLinkAtURL:sym withDestinationURL:[NSURL URLWithString:dest.lastPathComponent] error:nil];
[[OpmlFileExport withDelegate:self] writeOPMLFile:dest withOptions:OpmlFileExportOptionFullBackup];
if ([params.firstObject isEqualToString:kURLParamShow])
[[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[dest]];
}
}
/// Callback method for OPML backup export
- (NSArray<FeedGroup*>*)opmlFileExportListOfFeedGroups:(OpmlFileExportOptions)options {
return [StoreCoordinator sortedFeedGroupsWithParent:nil inContext:nil];
}
#pragma mark - Event Handling, Forward Send Key Down Events

View File

@@ -120,12 +120,16 @@ static NSString* const kURLSchemeBarss = @"barss";
static NSString* const kURLActionOpen = @"open";
/// Use @c barss:config to perform configuration steps
static NSString* const kURLActionConfig = @"config";
/// Use @c barss:backup to backup opml file into container
static NSString* const kURLActionBackup = @"backup";
/// Open preferences window with optional tab index. E.g., @c barss:open/preferences/1
static NSString* const kURLParamPreferences = @"preferences";
/// Run core data cleanup with optional silent parameter. E.g., @c barss:config/fixcache/silent
static NSString* const kURLParamFixCache = @"fixcache";
/// Disables error alerts and other interactive UI
static NSString* const kURLParamSilent = @"silent";
/// Show backup directory in Finder. E.g., @c barss:backup/show
static NSString* const kURLParamShow = @"show";
#pragma mark - Internal

View File

@@ -209,8 +209,7 @@
/// Image file path at e.g., "Application Support/baRSS/favicons/p42". @warning File may not exist!
- (NSURL*)iconPath {
NSString *pk = self.objectID.URIRepresentation.lastPathComponent;
return [[NSURL faviconsCacheURL] URLByAppendingPathComponent:pk isDirectory:NO];
return [[NSURL faviconsCacheURL] file:self.objectID.URIRepresentation.lastPathComponent ext:nil];
}
/// Move favicon from @c $TMPDIR to permanent destination in Application Support.

View File

@@ -27,6 +27,7 @@ static int const dbFileVersion = 1; // update in case database structure changes
@interface StoreCoordinator : NSObject
// Managing contexts
+ (NSManagedObjectContext*)getMainContext;
+ (NSManagedObjectContext*)createChildContext;
+ (void)saveContext:(NSManagedObjectContext*)context andParent:(BOOL)flag;
@@ -36,7 +37,7 @@ static int const dbFileVersion = 1; // update in case database structure changes
// Feed update
+ (NSDate*)nextScheduledUpdate;
+ (NSArray<Feed*>*)listOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(NSManagedObjectContext*)moc;
+ (NSArray<Feed*>*)listOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(nullable NSManagedObjectContext*)moc;
// Count elements
+ (BOOL)isEmpty;
@@ -45,10 +46,8 @@ static int const dbFileVersion = 1; // update in case database structure changes
+ (NSArray<NSDictionary*>*)countAggregatedUnread;
// Get List Of Elements
+ (NSArray<FeedGroup*>*)sortedFeedGroupsWithParent:(id)parent inContext:(NSManagedObjectContext*)moc;
+ (NSArray<FeedArticle*>*)sortedArticlesWithParent:(id)parent inContext:(NSManagedObjectContext*)moc;
+ (NSArray<Feed*>*)listOfFeedsMissingArticlesInContext:(NSManagedObjectContext*)moc;
+ (Feed*)feedWithIndexPath:(nonnull NSString*)path inContext:(NSManagedObjectContext*)moc;
+ (NSArray<FeedGroup*>*)sortedFeedGroupsWithParent:(id)parent inContext:(nullable NSManagedObjectContext*)moc;
+ (Feed*)feedWithIndexPath:(nonnull NSString*)path inContext:(nullable NSManagedObjectContext*)moc;
+ (NSString*)urlForFeedWithIndexPath:(nonnull NSString*)path;
// Unread articles list & mark articled read

View File

@@ -98,14 +98,15 @@
List of @c Feed items that need to be updated. Scheduled time is now (or in past).
@param forceAll If @c YES get a list of all @c Feed regardless of schedules time.
@param moc If @c nil perform requests on main context (ok for reading).
*/
+ (NSArray<Feed*>*)listOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(NSManagedObjectContext*)moc {
+ (NSArray<Feed*>*)listOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(nullable NSManagedObjectContext*)moc {
NSFetchRequest *fr = [Feed fetchRequest];
if (!forceAll) {
// when fetching also get those feeds that would need update soon (now + 2s)
[fr where:@"meta.scheduled <= %@", [NSDate dateWithTimeIntervalSinceNow:+2]];
}
return [fr fetchAllRows:moc];
return [fr fetchAllRows:moc ? moc : [self getMainContext]];
}
@@ -139,24 +140,30 @@
#pragma mark - Get List Of Elements
/// @return Sorted list of @c FeedGroup items where @c FeedGroup.parent @c = @c parent.
+ (NSArray<FeedGroup*>*)sortedFeedGroupsWithParent:(id)parent inContext:(NSManagedObjectContext*)moc {
return [[[[FeedGroup fetchRequest] where:@"parent = %@", parent] sortASC:@"sortIndex"] fetchAllRows:moc];
/**
@param moc If @c nil perform requests on main context (ok for reading).
@return Sorted list of @c FeedGroup items where @c FeedGroup.parent @c = @c parent.
*/
+ (NSArray<FeedGroup*>*)sortedFeedGroupsWithParent:(id)parent inContext:(nullable NSManagedObjectContext*)moc {
return [[[[FeedGroup fetchRequest] where:@"parent = %@", parent] sortASC:@"sortIndex"] fetchAllRows:moc ? moc : [self getMainContext]];
}
/// @return Sorted list of @c FeedArticle items where @c FeedArticle.feed @c = @c parent.
+ (NSArray<FeedArticle*>*)sortedArticlesWithParent:(id)parent inContext:(NSManagedObjectContext*)moc {
return [[[[FeedArticle fetchRequest] where:@"feed = %@", parent] sortDESC:@"sortIndex"] fetchAllRows:moc];
}
//+ (NSArray<FeedArticle*>*)sortedArticlesWithParent:(id)parent inContext:(NSManagedObjectContext*)moc {
// return [[[[FeedArticle fetchRequest] where:@"feed = %@", parent] sortDESC:@"sortIndex"] fetchAllRows:moc];
//}
/// @return Unsorted list of @c Feed items where @c articles.count @c == @c 0.
+ (NSArray<Feed*>*)listOfFeedsMissingArticlesInContext:(NSManagedObjectContext*)moc {
return [[[Feed fetchRequest] where:@"articles.@count == 0"] fetchAllRows:moc];
}
//+ (NSArray<Feed*>*)listOfFeedsMissingArticlesInContext:(NSManagedObjectContext*)moc {
// return [[[Feed fetchRequest] where:@"articles.@count == 0"] fetchAllRows:moc];
//}
/// @return Single @c Feed item where @c Feed.indexPath @c = @c path.
+ (Feed*)feedWithIndexPath:(nonnull NSString*)path inContext:(NSManagedObjectContext*)moc {
return [[[Feed fetchRequest] where:@"indexPath = %@", path] fetchFirst:moc];
/**
@param moc If @c nil perform requests on main context (ok for reading).
@return Single @c Feed item where @c Feed.indexPath @c = @c path.
*/
+ (Feed*)feedWithIndexPath:(nonnull NSString*)path inContext:(nullable NSManagedObjectContext*)moc {
return [[[Feed fetchRequest] where:@"indexPath = %@", path] fetchFirst:moc ? moc : [self getMainContext]];
}
/// @return URL of @c Feed item where @c Feed.indexPath @c = @c path.

View File

@@ -24,6 +24,7 @@
#import "FaviconDownload.h"
#import "Feed+Ext.h"
#import "FeedMeta+Ext.h"
#import "NSURL+Ext.h"
#import "NSURLRequest+Ext.h"
@interface FaviconDownload()
@@ -148,12 +149,10 @@
if (error) path = nil; // will also nullify img
NSImage *img = path ? [[NSImage alloc] initByReferencingURL:path] : nil;
if (img.valid) {
NSString *tmp = NSProcessInfo.processInfo.globallyUniqueString;
NSURL *dest = [path URLByDeletingLastPathComponent];
dest = [dest URLByAppendingPathComponent:tmp isDirectory:NO];
// move image to temporary destination, otherwise dataTask: will delete it.
[[NSFileManager defaultManager] moveItemAtURL:path toURL:dest error:nil];
self.fileURL = dest;
NSString *tmpFile = NSProcessInfo.processInfo.globallyUniqueString;
self.fileURL = [[path URLByDeletingLastPathComponent] file:tmpFile ext:nil];
[path moveTo:self.fileURL];
} else if (self.hostURL) {
[self loadImageFromDefaultLocation]; // starts a new request
return;

View File

@@ -262,7 +262,7 @@ static NSInteger RadioGroupSelection(NSView *view) {
NSXMLElement *head = [NSXMLElement elementWithName:@"head"];
head.children = @[[NSXMLElement elementWithName:@"title" stringValue:@"baRSS feeds"],
[NSXMLElement elementWithName:@"ownerName" stringValue:@"baRSS"],
[NSXMLElement elementWithName:@"dateCreated" stringValue:[NSDate dayStringISO8601]] ];
[NSXMLElement elementWithName:@"dateCreated" stringValue:[NSDate timeStringISO8601]] ];
NSXMLElement *body = [NSXMLElement elementWithName:@"body"];
for (FeedGroup *item in list) {

View File

@@ -164,10 +164,8 @@ static _Atomic(NSUInteger) _queueSize = 0;
/// Perform @c FaviconDownload on all core data @c Feed entries.
+ (void)updateAllFavicons {
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
for (Feed *f in [StoreCoordinator listOfFeedsThatNeedUpdate:YES inContext:moc])
for (Feed *f in [StoreCoordinator listOfFeedsThatNeedUpdate:YES inContext:nil])
[FaviconDownload updateFeed:f finally:nil];
[moc reset];
}
/// Download list of feeds. Either silently in background or with alerts in foreground.

View File

@@ -70,7 +70,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>13460</string>
<string>13607</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.news</string>
<key>LSMinimumSystemVersion</key>

View File

@@ -33,6 +33,7 @@ typedef NS_ENUM(int32_t, TimeUnitType) {
};
@interface NSDate (Ext)
+ (NSString*)timeStringISO8601;
+ (NSString*)dayStringISO8601;
+ (NSString*)dayStringLocalized;
@end

View File

@@ -35,11 +35,15 @@ static TimeUnitType const _values[] = {
@implementation NSDate (Ext)
/// @return Day as string in iso format: @c YYYY-MM-DD'T'hh:mm:ss'Z'
+ (NSString*)dayStringISO8601 {
/// @return Time as string in iso format: @c YYYY-MM-DD'T'hh:mm:ss'Z'
+ (NSString*)timeStringISO8601 {
return [[[NSISO8601DateFormatter alloc] init] stringFromDate:[NSDate date]];
// NSDateComponents *now = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear fromDate:[NSDate date]];
// return [NSString stringWithFormat:@"%04ld-%02ld-%02ld", now.year, now.month, now.day];
}
/// @return Day as string in iso format: @c YYYY-MM-DD
+ (NSString*)dayStringISO8601 {
NSDateComponents *now = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear fromDate:[NSDate date]];
return [NSString stringWithFormat:@"%04ld-%02ld-%02ld", now.year, now.month, now.day];
}
/// @return Day as string in localized short format, e.g., @c DD.MM.YY

View File

@@ -23,8 +23,15 @@
@import Cocoa;
@interface NSURL (Ext)
// Generators
+ (NSURL*)applicationSupportURL;
+ (NSURL*)faviconsCacheURL;
+ (NSURL*)backupPathURL;
// File Traversal
- (BOOL)existsAndIsDir:(BOOL)dir;
- (NSURL*)subdir:(NSString*)dirname;
- (NSURL*)file:(NSString*)filename ext:(nullable NSString*)ext;
// File Manipulation
- (BOOL)mkdir;
- (void)remove;
- (void)moveTo:(NSURL*)destination;

View File

@@ -26,24 +26,56 @@
@implementation NSURL (Ext)
/// @return Directory URL pointing to "Application Support/baRSS/favicons". Does @b not create directory!
+ (NSURL*)faviconsCacheURL {
// ---------------------------------------------------------------
// | MARK: - Generators
// ---------------------------------------------------------------
/// @return Directory URL pointing to "Application Support/baRSS". Does @b not create directory!
+ (NSURL*)applicationSupportURL {
static NSURL *path = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
path = [[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil];
path = [path URLByAppendingPathComponent:[UserPrefs appName] isDirectory:YES];
path = [path URLByAppendingPathComponent:@"favicons" isDirectory:YES];
});
return path;
}
/// @return Directory URL pointing to "Application Support/baRSS/favicons". Does @b not create directory!
+ (NSURL*)faviconsCacheURL {
return [[self applicationSupportURL] URLByAppendingPathComponent:@"favicons" isDirectory:YES];
}
/// @return Directory URL pointing to "Application Support/baRSS/backup". Does @b not create directory!
+ (NSURL*)backupPathURL {
return [[self applicationSupportURL] URLByAppendingPathComponent:@"backup" isDirectory:YES];
}
// ---------------------------------------------------------------
// | MARK: - File Traversal
// ---------------------------------------------------------------
/// @return @c YES if and only if item exists at URL and item matches @c dir flag
- (BOOL)existsAndIsDir:(BOOL)dir {
BOOL d;
return self.path && [[NSFileManager defaultManager] fileExistsAtPath:self.path isDirectory:&d] && d == dir;
}
/// @return @c NSURL copy with appended directory path
- (NSURL*)subdir:(NSString*)dirname {
return [self URLByAppendingPathComponent:dirname isDirectory:YES];
}
/// @return @c NSURL copy with appended file path and extension
- (NSURL*)file:(NSString*)filename ext:(nullable NSString*)ext {
NSURL *u = [self URLByAppendingPathComponent:filename isDirectory:NO];
return ext.length > 0 ? [u URLByAppendingPathExtension:ext] : u;
}
// ---------------------------------------------------------------
// | MARK: - File Manipulation
// ---------------------------------------------------------------
/**
Create directory at URL. If directory exists, this method does nothing.
@return @c YES if dir created successfully. @c NO if dir already exists or an error occured.

View File

@@ -69,7 +69,8 @@
[self str:mas add:@"RSXML2" link:@"https://github.com/relikd/RSXML2"];
[self str:mas add:@" (MIT License)" bold:NO];
[self str:mas add:@"\n\n\n\nOptions\n" bold:YES];
[self str:mas add:@"Fix Cache" link:@"barss:config/fixcache"];
[self str:mas add:@"Fix Cache\n" link:@"barss:config/fixcache"];
[self str:mas add:@"Backup now\n" link:@"barss:backup/show"];
[mas endEditing];
return mas;
}

View File

@@ -69,19 +69,17 @@
/// Populate menu with items.
- (void)menuNeedsUpdate:(NSMenu*)menu {
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
if (menu.isFeedMenu) {
Feed *feed = [StoreCoordinator feedWithIndexPath:menu.titleIndexPath inContext:moc];
Feed *feed = [StoreCoordinator feedWithIndexPath:menu.titleIndexPath inContext:nil];
[self setArticles:[feed sortedArticles] forMenu:menu];
} else {
NSArray<FeedGroup*> *groups = [StoreCoordinator sortedFeedGroupsWithParent:menu.parentItem.representedObject inContext:moc];
NSArray<FeedGroup*> *groups = [StoreCoordinator sortedFeedGroupsWithParent:menu.parentItem.representedObject inContext:nil];
if (groups.count == 0) {
[menu addItemWithTitle:NSLocalizedString(@"~~~ no entries ~~~", nil) action:nil keyEquivalent:@""].enabled = NO;
} else {
[self setFeedGroups:groups forMenu:menu];
}
}
[moc reset];
}
/// Get rid of everything that is not needed.
@@ -128,13 +126,11 @@
@warning @c item and @c feed will often mismatch.
*/
- (void)updateFeedMenuItem:(NSManagedObjectID*)oid withBlock:(void(^)(Feed *feed, NSMenuItem *item))block {
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
Feed *feed = [moc objectWithID:oid];
Feed *feed = [[StoreCoordinator getMainContext] objectWithID:oid];
if ([feed isKindOfClass:[Feed class]]) {
NSMenuItem *item = [self.statusItem.mainMenu deepestItemWithPath:feed.indexPath];
if (item) block(feed, item);
}
[moc reset];
}
/// Callback method fired when feed has been updated in the background.