Backup URL scheme
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -33,6 +33,7 @@ typedef NS_ENUM(int32_t, TimeUnitType) {
|
||||
};
|
||||
|
||||
@interface NSDate (Ext)
|
||||
+ (NSString*)timeStringISO8601;
|
||||
+ (NSString*)dayStringISO8601;
|
||||
+ (NSString*)dayStringLocalized;
|
||||
@end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user