Repository: ethanal/Nimbus Branch: master Commit: 2259a68f7e4c Files: 70 Total size: 222.7 KB Directory structure: gitextract_4bhtxnk0/ ├── .gitignore ├── LICENSE ├── NimbusMenuBarApp/ │ └── Nimbus/ │ ├── Nimbus/ │ │ ├── APIClient.swift │ │ ├── AppDelegate.swift │ │ ├── Base.lproj/ │ │ │ └── MainMenu.xib │ │ ├── Images.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── KeychainManager.swift │ │ ├── Nimbus-Bridging-Header.h │ │ ├── PreferencesManager.swift │ │ ├── PreferencesWindowController.swift │ │ ├── PreferencesWindowController.xib │ │ ├── ScreenshotWatcher.h │ │ ├── ScreenshotWatcher.m │ │ ├── StatusItemView.swift │ │ ├── SwiftyJSON.swift │ │ └── main.swift │ ├── Nimbus.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Nimbus.xcscheme │ └── NimbusTests/ │ ├── Info.plist │ └── NimbusTests.swift ├── README.md ├── api_docs.md ├── graphics_assets/ │ └── assets.sketch/ │ ├── Data │ ├── metadata │ └── version ├── manage.py ├── nimbus/ │ ├── __init__.py │ ├── apps/ │ │ ├── __init__.py │ │ ├── accounts/ │ │ │ ├── __init__.py │ │ │ ├── forms.py │ │ │ ├── urls.py │ │ │ └── views.py │ │ ├── api/ │ │ │ ├── __init__.py │ │ │ ├── urls.py │ │ │ └── views.py │ │ └── media/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── urls.py │ │ └── views.py │ ├── settings/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── local.py │ │ ├── production.py │ │ └── secret.sample.py │ ├── static/ │ │ ├── css/ │ │ │ ├── base.css │ │ │ ├── dashboard.css │ │ │ ├── login.css │ │ │ └── share_preview.css │ │ └── js/ │ │ ├── dashboard.js │ │ └── jquery-photo-resize.js │ ├── templates/ │ │ ├── admin/ │ │ │ └── base_site.html │ │ ├── nimbus/ │ │ │ ├── accounts/ │ │ │ │ ├── dashboard.html │ │ │ │ ├── login.html │ │ │ │ └── media_table_row.html │ │ │ ├── media/ │ │ │ │ ├── share_base.html │ │ │ │ ├── share_download.html │ │ │ │ ├── share_img_preview.html │ │ │ │ └── share_txt_preview.html │ │ │ └── page_base.html │ │ └── rest_framework/ │ │ ├── api.html │ │ └── login.html │ ├── utils.py │ └── wsgi.py └── requirements/ ├── base.txt ├── local.txt └── production.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ bin/ build/ develop-eggs/ dist/ eggs/ #lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Rope .ropeproject # Django stuff: *.log *.pot # Sphinx documentation docs/_build/ # Xcode # build/ *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata *.xccheckout *.moved-aside DerivedData *.hmap *.ipa *.xcuserstate # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control # # Pods/ testing_database.db secret.py collected_static newrelic.ini ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 Ethan Lowman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/APIClient.swift ================================================ // // APIClient.swift // Nimbus // // Created by Ethan Lowman on 7/25/14. // Copyright (c) 2014 Ethanal. All rights reserved. // import Cocoa class APIClient: NSObject { var prefs = PreferencesManager() var apiRoot: String { get { return "https://api." + prefs.hostname } } var authToken: String override init() { if let token = KeychainService.loadToken() { authToken = token as String } else { authToken = "" } super.init() } func request(uri: NSString, withAuth: Bool) -> NSMutableURLRequest { let r = NSMutableURLRequest(URL: NSURL(string: apiRoot + (uri as String))!) if withAuth { r.setValue("Token \(authToken)", forHTTPHeaderField: "Authorization") } r.timeoutInterval = 10.0 return r } func getTokenForUsername(username: NSString, password: NSString, successCallback: ((NSString!) -> Void)?, errorCallback: (() -> Void)?) { let req = request("/api-token-auth", withAuth: false) req.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") req.HTTPMethod = "POST" let requestJSON = JSON([ "username": username, "password": password ]) req.HTTPBody = requestJSON.rawString()!.dataUsingEncoding(NSUTF8StringEncoding) NSURLConnection.sendAsynchronousRequest(req, queue: NSOperationQueue.mainQueue()) {(response, data, error) in guard let data = data where error == nil else { if (errorCallback != nil) { errorCallback!() } return } let responseString = NSString(data: data, encoding: NSUTF8StringEncoding) var responseJSON = JSON(data: data) if let token = responseJSON["token"].string { if (successCallback != nil) { self.authToken = token successCallback!(token) } } else { print(responseString) if (errorCallback != nil) { errorCallback!() } return } } } func addFile(fileData: NSData, filename: NSString, successCallback: ((NSURL!) -> Void)?, errorCallback: (() -> Void)?) { let req = request("/media/add_file", withAuth: true) req.HTTPMethod = "POST" let boundary = "gc0p4Jq0M2Yt08jU534c0pgc0p4Jq0M2Yt08jU534c0p" let contentType = "multipart/form-data; boundary=\(boundary)" req.setValue(contentType, forHTTPHeaderField: "Content-Type") let postData = NSMutableData() postData.appendData("\r\n--\(boundary)\r\n".dataUsingEncoding(NSUTF8StringEncoding)!) postData.appendData("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n".dataUsingEncoding(NSUTF8StringEncoding)!) postData.appendData("Content-Type: application/octet-stream\r\n\r\n".dataUsingEncoding(NSUTF8StringEncoding)!) postData.appendData(NSData(data: fileData)) postData.appendData("\r\n--\(boundary)--\r\n".dataUsingEncoding(NSUTF8StringEncoding)!) req.HTTPBody = postData NSURLConnection.sendAsynchronousRequest(req, queue: NSOperationQueue.mainQueue()) {(response, data, error) in guard let data = data where error == nil else { if (errorCallback != nil) { errorCallback!() } return } let responseString = NSString(data: data, encoding: NSUTF8StringEncoding) var responseJSON = JSON(data: data) if let shareURL = responseJSON["share_url"].URL { if (successCallback != nil) { successCallback!(shareURL) } } else { print(responseString) if (errorCallback != nil) { errorCallback!() } return } } } func addLink(link: NSURL, successCallback: ((NSURL!) -> Void)?, errorCallback: (() -> Void)?) { let req = request("/media/add_link", withAuth: true) req.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") req.HTTPMethod = "POST" let requestJSON = JSON([ "target_url": link.absoluteString ]) req.HTTPBody = requestJSON.rawString()!.dataUsingEncoding(NSUTF8StringEncoding) NSURLConnection.sendAsynchronousRequest(req, queue: NSOperationQueue.mainQueue()) {(response, data, error) in guard let data = data where error == nil else { if (errorCallback != nil) { errorCallback!() } return } let responseString = NSString(data: data, encoding: NSUTF8StringEncoding) var responseJSON = JSON(data: data) if let shareURL = responseJSON["share_url"].URL { if (successCallback != nil) { successCallback!(shareURL) } } else { print(responseString) if (errorCallback != nil) { errorCallback!() } return } } } } ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/AppDelegate.swift ================================================ // // AppDelegate.swift // Nimbus // // Created by Ethan Lowman on 7/25/14. // Copyright (c) 2014 Ethanal. All rights reserved. // import Cocoa class AppDelegate: NSObject, NSApplicationDelegate, NSMetadataQueryDelegate { var statusView = StatusItemView() var prefs = PreferencesManager() var api = APIClient() var sw: ScreenshotWatcher? override init() { super.init() } func applicationDidFinishLaunching(aNotification: NSNotification) { sw = ScreenshotWatcher(uploadFileCallback: uploadFile); sw!.startWatchingPath(screenCaptureLocation()) } func screenCaptureLocation() -> String { let screenCapturePrefs: NSDictionary? = NSUserDefaults.standardUserDefaults().persistentDomainForName("com.apple.screencapture") let location: String? = screenCapturePrefs?.valueForKey("location")?.stringByExpandingTildeInPath as String? if let loc = location { return loc.hasSuffix("/") ? loc : (loc + "/") } return ("~/Desktop" as NSString).stringByExpandingTildeInPath + "/" } func uploadFile(fileData: NSData!, filename: String!) { print(filename); // NSMetadataItem *metadata = NSMetadataItem(URL: filename) // BOOL isScreenshot = [[metadata valueForAttribute:@"kMDItemIsScreenCapture"] integerValue] == 1; statusView.status = .Working api.addFile(fileData, filename: filename, successCallback: {(shareURL: NSURL!) -> Void in let pb = NSPasteboard.generalPasteboard() pb.clearContents() pb.writeObjects([shareURL.absoluteString]) self.statusView.status = .Success }, errorCallback: {() -> Void in print("Error uploading file") self.statusView.status = .Error }) } func uploadLink(link: NSURL) { statusView.status = .Working api.addLink(link, successCallback: {(shareURL: NSURL!) -> Void in let pb = NSPasteboard.generalPasteboard() pb.clearContents() pb.writeObjects([shareURL.absoluteString]) self.statusView.status = .Success }, errorCallback: {() -> Void in print("Error uploading link") self.statusView.status = .Error }) } } ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/Base.lproj/MainMenu.xib ================================================ Default Left to Right Right to Left Default Left to Right Right to Left ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "icon_16x16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "icon_16x16@2x.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "icon_32x32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "icon_32x32@2x.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "icon_128x128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "icon_128x128@2x.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "icon_256x256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "icon_256x256@2x.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "icon_512x512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "icon_512x512@2x.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSMinimumSystemVersion ${MACOSX_DEPLOYMENT_TARGET} LSUIElement NSHumanReadableCopyright Copyright © 2014 Ethanal. All rights reserved. NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/KeychainManager.swift ================================================ import Cocoa import Security // Identifiers let serviceIdentifier = "Nimbus" let userAccount = "authenticatedUser" let accessGroup = "Nimbus" // Arguments for the keychain queries let kSecClassValue = NSString(format: kSecClass) let kSecAttrAccountValue = NSString(format: kSecAttrAccount) let kSecValueDataValue = NSString(format: kSecValueData) let kSecClassGenericPasswordValue = NSString(format: kSecClassGenericPassword) let kSecAttrServiceValue = NSString(format: kSecAttrService) let kSecMatchLimitValue = NSString(format: kSecMatchLimit) let kSecReturnDataValue = NSString(format: kSecReturnData) let kSecMatchLimitOneValue = NSString(format: kSecMatchLimitOne) class KeychainService: NSObject { /** * Exposed methods to perform queries. * Note: feel free to play around with the arguments * for these if you want to be able to customise the * service identifier, user accounts, access groups, etc. */ internal class func saveToken(token: NSString) { self.save(serviceIdentifier, data: token) } internal class func loadToken() -> NSString? { let token = self.load(serviceIdentifier) return token } /** * Internal methods for querying the keychain. */ private class func save(service: NSString, data: NSString) { let dataFromString: NSData = data.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! // Instantiate a new default keychain query let keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPasswordValue, service, userAccount, dataFromString], forKeys: [kSecClassValue, kSecAttrServiceValue, kSecAttrAccountValue, kSecValueDataValue]) // Delete any existing items SecItemDelete(keychainQuery as CFDictionaryRef) // Add the new keychain item let status: OSStatus = SecItemAdd(keychainQuery as CFDictionaryRef, nil) if status != errSecSuccess { print("Error saving to keychain") } } private class func load(service: NSString) -> NSString? { // Instantiate a new default keychain query // Tell the query to return a result // Limit our results to one item let keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPasswordValue, service, userAccount, kCFBooleanTrue, kSecMatchLimitOneValue], forKeys: [kSecClassValue, kSecAttrServiceValue, kSecAttrAccountValue, kSecReturnDataValue, kSecMatchLimitValue]) var extractedData : AnyObject? // Search for the keychain items let status: OSStatus = SecItemCopyMatching(keychainQuery, &extractedData) var contentsOfKeychain: NSString? if let retrievedData = extractedData as? NSData where status == errSecSuccess { // Convert the data retrieved from the keychain into a string contentsOfKeychain = NSString(data: retrievedData, encoding: NSUTF8StringEncoding) } else { print("Nothing was retrieved from the keychain. Status code \(status)") } return contentsOfKeychain } } ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/Nimbus-Bridging-Header.h ================================================ // // Use this file to import your target's public headers that you would like to expose to Swift. // #import "ScreenshotWatcher.h" ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/PreferencesManager.swift ================================================ // // PreferencesManager.swift // Nimbus // // Created by Ethan Lowman on 7/25/14. // Copyright (c) 2014 Ethanal. All rights reserved. // import Cocoa class PreferencesManager: NSObject { var prefs = NSUserDefaults.standardUserDefaults() var hostname: String { get { if let val = prefs.stringForKey("hostname") as String! { return val } return "example.com" } set { prefs.setObject(newValue, forKey: "hostname") } } var loggedIn: Bool { get { if let val = prefs.boolForKey("loggedIn") as Bool! { return val } return false } set { prefs.setBool(newValue, forKey: "loggedIn") } } var username: String { get { if let val = prefs.stringForKey("username") as String! { return val } return "" } set { prefs.setValue(newValue, forKey: "username") } } var uploadScreenshots: Int { get { if let val = prefs.integerForKey("uploadScreenshots") as Int! { return val } return 1 } set { prefs.setInteger(newValue, forKey: "uploadScreenshots") } } } ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/PreferencesWindowController.swift ================================================ // // PreferencesWindowController.swift // Nimbus // // Created by Ethan Lowman on 7/25/14. // Copyright (c) 2014 Ethanal. All rights reserved. // import Cocoa class PreferencesWindowController: NSWindowController, NSWindowDelegate { var prefs = PreferencesManager() var api = APIClient() @IBOutlet weak var hostnameField: NSTextField? @IBOutlet weak var usernameField: NSTextField? @IBOutlet weak var passwordField: NSSecureTextField? @IBOutlet weak var uploadScreenshotsCheckbox: NSButton? @IBOutlet weak var accountActionButton: NSButton? @IBOutlet weak var hostnameLabel: NSTextField? @IBOutlet weak var usernameLabel: NSTextField? @IBOutlet weak var passwordLabel: NSTextField? @IBOutlet var appMenu: NSMenu! override func windowDidLoad() { super.windowDidLoad() updateAccountUI() } @IBAction func uploadScreenshotsCheckboxPressed(sender: AnyObject) { prefs.uploadScreenshots = uploadScreenshotsCheckbox!.state } @IBAction func accountActionButtonPressed(sender: AnyObject) { if !prefs.loggedIn { prefs.hostname = hostnameField!.stringValue prefs.username = usernameField!.stringValue api.getTokenForUsername(usernameField!.stringValue, password: passwordField!.stringValue, successCallback: {(token: NSString!) -> Void in self.prefs.loggedIn = true KeychainService.saveToken(token) self.updateAccountUI() }, errorCallback: {() -> Void in let alert = NSAlert() alert.messageText = "Unable to login with provided credentials" alert.runModal() }) } else { prefs.loggedIn = false updateAccountUI() } } func updateAccountUI() { uploadScreenshotsCheckbox!.state = prefs.uploadScreenshots if prefs.loggedIn { accountActionButton!.title = "Logout" hostnameField!.hidden = true hostnameLabel!.hidden = false hostnameLabel!.stringValue = prefs.hostname usernameField!.hidden = true usernameLabel!.hidden = false usernameLabel!.stringValue = prefs.username passwordField!.hidden = true passwordLabel!.hidden = true } else { accountActionButton!.title = "Login" hostnameField!.hidden = false hostnameField!.stringValue = "" hostnameLabel!.hidden = true usernameField!.hidden = false usernameField!.stringValue = "" usernameLabel!.hidden = true passwordField!.hidden = false passwordField!.stringValue = "" passwordLabel!.hidden = false } } } ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/PreferencesWindowController.xib ================================================ NSAllRomanInputSourcesLocaleIdentifier ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/ScreenshotWatcher.h ================================================ // // ScreenshotWatch.h // Nimbus // // Created by Ethan Lowman on 7/27/14. // Copyright (c) 2014 Ethanal. All rights reserved. // #import typedef void(^FileCallbackBlock)(NSData*, NSString*); @interface ScreenshotWatcher : NSObject @property (nonatomic, copy) FileCallbackBlock uploadCallback; - (instancetype)initWithUploadFileCallback:(FileCallbackBlock)callback; - (void)startWatchingPath:(NSString*) path; @end ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/ScreenshotWatcher.m ================================================ // // ScreenshotWatch.m // Nimbus // // Created by Ethan Lowman on 7/27/14. // Copyright (c) 2014 Ethanal. All rights reserved. // #import "ScreenshotWatcher.h" @interface ScreenshotWatcher() @property (nonatomic, assign) FSEventStreamRef eventStream; @end static NSMutableSet *uploadedFilenames; @implementation ScreenshotWatcher - (instancetype)initWithUploadFileCallback:(FileCallbackBlock) callback { self = [super init]; if (self) { if (uploadedFilenames == nil) { uploadedFilenames = [NSMutableSet new]; } self.uploadCallback = callback; } return self; } void describeFSEvent(FSEventStreamEventFlags eventFlags) { // if (eventFlags & kFSEventStreamEventFlagNone) printf("None\t"); if (eventFlags & kFSEventStreamEventFlagMustScanSubDirs) printf("MustScanSubDirs\t"); if (eventFlags & kFSEventStreamEventFlagUserDropped) printf("UserDropped\t"); if (eventFlags & kFSEventStreamEventFlagKernelDropped) printf("KernelDropped\t"); if (eventFlags & kFSEventStreamEventFlagEventIdsWrapped) printf("EventIdsWrapped\t"); if (eventFlags & kFSEventStreamEventFlagHistoryDone) printf("HistoryDone\t"); if (eventFlags & kFSEventStreamEventFlagRootChanged) printf("RootChanged\t"); if (eventFlags & kFSEventStreamEventFlagMount) printf("Mount\t"); if (eventFlags & kFSEventStreamEventFlagUnmount) printf("Unmount\t"); if (eventFlags & kFSEventStreamEventFlagItemCreated) printf("ItemCreated\t"); if (eventFlags & kFSEventStreamEventFlagItemRemoved) printf("ItemRemoved\t"); if (eventFlags & kFSEventStreamEventFlagItemInodeMetaMod) printf("ItemInodeMetaMod\t"); if (eventFlags & kFSEventStreamEventFlagItemRenamed) printf("ItemRenamed\t"); if (eventFlags & kFSEventStreamEventFlagItemModified) printf("ItemModified\t"); if (eventFlags & kFSEventStreamEventFlagItemFinderInfoMod) printf("ItemFinderInfoMod\t"); if (eventFlags & kFSEventStreamEventFlagItemChangeOwner) printf("ItemChangeOwner\t"); if (eventFlags & kFSEventStreamEventFlagItemXattrMod) printf("ItemXattrMod\t"); if (eventFlags & kFSEventStreamEventFlagItemIsFile) printf("ItemIsFile\t"); if (eventFlags & kFSEventStreamEventFlagItemIsDir) printf("ItemIsDir\t"); if (eventFlags & kFSEventStreamEventFlagItemIsSymlink) printf("ItemIsSymlink\t"); printf("\n"); } void fsEventsCallback(ConstFSEventStreamRef streamRef, void *info, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) { char** paths = (char**)eventPaths; for (int i = 0; i < numEvents; i++) { NSString *path = [NSString stringWithUTF8String:paths[i]]; NSURL *fileURL = [NSURL fileURLWithPath:path]; NSFileManager *fileManager = [NSFileManager defaultManager]; // if (![fileURL.lastPathComponent hasPrefix:@"."]) { // printf("%s\t", [fileURL.lastPathComponent cStringUsingEncoding:NSUTF8StringEncoding]); // describeFSEvent(eventFlags[i]); // } if ((eventFlags[i] & kFSEventStreamEventFlagItemRenamed) && (eventFlags[i] & kFSEventStreamEventFlagItemXattrMod) && !([uploadedFilenames containsObject:path])) { if (![fileURL.lastPathComponent hasPrefix:@"."]) { NSMetadataItem *metadata = [[NSMetadataItem alloc] initWithURL:fileURL]; if ([fileManager fileExistsAtPath:path]) { BOOL isScreenshot = [[metadata valueForAttribute:@"kMDItemIsScreenCapture"] integerValue] == 1; if (isScreenshot) { // printf("Uploading %s\n", [fileURL.lastPathComponent cStringUsingEncoding:NSUTF8StringEncoding]); NSData *fileData = [fileManager contentsAtPath:path]; ScreenshotWatcher *watcher = (__bridge ScreenshotWatcher*)info; watcher.uploadCallback(fileData, path); [uploadedFilenames addObject:path]; } } } } } } - (void)startWatchingPath:(NSString*) path { FSEventStreamContext context; context.info = (__bridge void *)(self); context.version = 0; context.retain = NULL; context.release = NULL; context.copyDescription = NULL; NSArray *pathsToWatch = @[path]; self.eventStream = FSEventStreamCreate(NULL, &fsEventsCallback, &context, (__bridge CFArrayRef)(pathsToWatch), kFSEventStreamEventIdSinceNow, 0.1, kFSEventStreamCreateFlagFileEvents ); FSEventStreamScheduleWithRunLoop(self.eventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); FSEventStreamStart(self.eventStream); } @end ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/StatusItemView.swift ================================================ // // StatusItemView.swift // Nimbus // // Created by Ethan Lowman on 7/25/14. // Copyright (c) 2014 Ethanal. All rights reserved. // import Cocoa enum StatusItemViewStatus: String { case Normal = "Normal" case Error = "Error" case Success = "Success" case Working = "Working" } func delay(delay:Double, closure:()->()) { dispatch_after( dispatch_time( DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC)) ), dispatch_get_main_queue(), closure) } class StatusItemView: NSView, NSMenuDelegate, NSWindowDelegate { let statusItemWidth = 30.0 let statusBarHeight = NSStatusBar.systemStatusBar().thickness let statusItemRect: NSRect var status: StatusItemViewStatus = .Normal { didSet { switch status { case .Normal: progressFrame = 0 case .Working: if oldValue != .Working { progressFrame = 1 } default: progressFrame = 0 let statusAtDispatch = status delay(2.0) { if statusAtDispatch == self.status { self.status = .Normal self.updateUI() } } } self.updateUI() } } var active: Bool = false { didSet { self.updateUI() } } var progressFrame: Int = 0 { didSet { if self.progressFrame != 0 { delay(0.25) { if self.progressFrame != 0 { self.progressFrame = 1 + (self.progressFrame % 3) } } } updateUI() } } var statusItem: NSStatusItem var popover: NSPopover? var preferencesWindowController: NSWindowController? var prefs = PreferencesManager() lazy var imageView: NSImageView = { let view:NSImageView = NSImageView(frame: self.statusItemRect) view.unregisterDraggedTypes() return view }() lazy var statusItemMenu: NSMenu = { let menu: NSMenu = NSMenu() menu.delegate = self menu.autoenablesItems = true menu.addItem(self.websiteItem) menu.addItem(self.preferencesItem) menu.addItem(self.quitItem) return menu }() lazy var websiteItem: NSMenuItem = { let item = NSMenuItem(title: "Launch Nimbus Website", action: "openWebsite:", keyEquivalent: "") item.target = self return item }() lazy var preferencesItem: NSMenuItem = { let item = NSMenuItem(title: "Preferences", action: "openPreferences:", keyEquivalent: "") item.target = self return item }() lazy var quitItem: NSMenuItem = { let item = NSMenuItem(title: "Quit", action: "quitApp:", keyEquivalent: "") item.target = self return item }() init() { statusItemRect = NSMakeRect(0, 0, CGFloat(statusItemWidth), CGFloat(statusBarHeight)) statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(CGFloat(statusItemWidth)) super.init(frame: statusItemRect) statusItem.view = self statusItem.menu = statusItemMenu self.addSubview(imageView) self.registerForDraggedTypes([NSFilenamesPboardType, NSURLPboardType, NSStringPboardType]) updateUI() } required convenience init?(coder: NSCoder) { self.init() } override func drawRect(dirtyRect: NSRect) { if active { NSColor.selectedMenuItemColor().setFill() NSRectFill(dirtyRect) } else { NSColor.clearColor().setFill() NSRectFill(dirtyRect) } } func updateUI() { if (active) { self.imageView.image = NSImage(named: "menubar-highlighted") } else { if status != .Working { var imageNames: Dictionary = [ .Normal: "menubar", .Error: "menubar-error", .Success: "menubar-success", ] self.imageView.image = NSImage(named: imageNames[status]!) } else { self.imageView.image = NSImage(named: "menubar-progress-\(progressFrame)") } } self.needsDisplay = true } override func mouseDown(theEvent: NSEvent) { active = true statusItem.popUpStatusItemMenu(statusItemMenu) } override func mouseUp(theEvent: NSEvent) { active = false } func openWebsite(sender: NSStatusItem!) { NSWorkspace.sharedWorkspace().openURL(NSURL(string: "http://account." + prefs.hostname)!) } func openPreferences(sender: NSStatusItem!) { if !(preferencesWindowController != nil) { preferencesWindowController = PreferencesWindowController(windowNibName: "PreferencesWindowController") } preferencesWindowController!.showWindow(self) NSApp.activateIgnoringOtherApps(true) } func quitApp(sender: NSStatusItem!) { NSApplication.sharedApplication().terminate(nil) } // NSMenuDelegate func menuDidClose(menu: NSMenu) { active = false } override func draggingEntered(sender: NSDraggingInfo) -> NSDragOperation { return .Copy } override func draggingEnded(sender: NSDraggingInfo!) { if NSPointInRect(sender.draggingLocation(), self.frame) { handleDrop(sender) } } // Manually called instead of performDragOperation // http://openradar.appspot.com/radar?id=1745403 func handleDrop(sender: NSDraggingInfo!) -> Bool { let pboard = sender.draggingPasteboard(); let types: NSArray = pboard.types! let appDelegate = NSApplication.sharedApplication().delegate as! AppDelegate let fileManager = NSFileManager.defaultManager() if types.containsObject(NSFilenamesPboardType) { let fileURL = NSURL(fromPasteboard: pboard)! let fileData = fileManager.contentsAtPath(fileURL.path!) let fileName = fileURL.lastPathComponent if (fileData != nil) && (fileName != nil) { appDelegate.uploadFile(fileData!, filename: fileName!) } else { status = .Error } } else if types.containsObject(NSURLPboardType) { let url = NSURL(fromPasteboard: pboard)! appDelegate.uploadLink(url) } else if types.containsObject(NSStringPboardType) { let text = pboard.stringForType(NSStringPboardType) as NSString! let legalChars = NSMutableCharacterSet.alphanumericCharacterSet() legalChars.formUnionWithCharacterSet(NSCharacterSet.whitespaceCharacterSet()) legalChars.invert() var filename = (text.componentsSeparatedByCharactersInSet(legalChars) as NSArray).componentsJoinedByString("") as NSString filename = filename.substringToIndex(30 > text.length ? text.length : 30) + ".txt" let fileData = text.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true) if let d = fileData { appDelegate.uploadFile(d, filename: filename as String) } else { status = .Error } } return true; } } ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/SwiftyJSON.swift ================================================ // SwiftyJSON.swift // // Copyright (c) 2014 Ruoyu Fu, Pinglin Tang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation // MARK: - Error ///Error domain public let ErrorDomain: String = "SwiftyJSONErrorDomain" ///Error code public let ErrorUnsupportedType: Int = 999 public let ErrorIndexOutOfBounds: Int = 900 public let ErrorWrongType: Int = 901 public let ErrorNotExist: Int = 500 public let ErrorInvalidJSON: Int = 490 // MARK: - JSON Type /** JSON's type definitions. See http://www.json.org */ public enum Type :Int{ case Number case String case Bool case Array case Dictionary case Null case Unknown } // MARK: - JSON Base public struct JSON { /** Creates a JSON using the data. - parameter data: The NSData used to convert to json.Top level object in data is an NSArray or NSDictionary - parameter opt: The JSON serialization reading options. `.AllowFragments` by default. - parameter error: error The NSErrorPointer used to return the error. `nil` by default. - returns: The created JSON */ public init(data:NSData, options opt: NSJSONReadingOptions = .AllowFragments, error: NSErrorPointer = nil) { do { let object: AnyObject = try NSJSONSerialization.JSONObjectWithData(data, options: opt) self.init(object) } catch let aError as NSError { if error != nil { error.memory = aError } self.init(NSNull()) } } /** Create a JSON from JSON string - parameter string: Normal json string like '{"a":"b"}' - returns: The created JSON */ public static func parse(string:String) -> JSON { return string.dataUsingEncoding(NSUTF8StringEncoding) .flatMap({JSON(data: $0)}) ?? JSON(NSNull()) } /** Creates a JSON using the object. - parameter object: The object must have the following properties: All objects are NSString/String, NSNumber/Int/Float/Double/Bool, NSArray/Array, NSDictionary/Dictionary, or NSNull; All dictionary keys are NSStrings/String; NSNumbers are not NaN or infinity. - returns: The created JSON */ public init(_ object: AnyObject) { self.object = object } /** Creates a JSON from a [JSON] - parameter jsonArray: A Swift array of JSON objects - returns: The created JSON */ public init(_ jsonArray:[JSON]) { self.init(jsonArray.map { $0.object }) } /** Creates a JSON from a [String: JSON] - parameter jsonDictionary: A Swift dictionary of JSON objects - returns: The created JSON */ public init(_ jsonDictionary:[String: JSON]) { var dictionary = [String: AnyObject]() for (key, json) in jsonDictionary { dictionary[key] = json.object } self.init(dictionary) } /// Private object private var rawArray: [AnyObject] = [] private var rawDictionary: [String : AnyObject] = [:] private var rawString: String = "" private var rawNumber: NSNumber = 0 private var rawNull: NSNull = NSNull() /// Private type private var _type: Type = .Null /// prviate error private var _error: NSError? = nil /// Object in JSON public var object: AnyObject { get { switch self.type { case .Array: return self.rawArray case .Dictionary: return self.rawDictionary case .String: return self.rawString case .Number: return self.rawNumber case .Bool: return self.rawNumber default: return self.rawNull } } set { _error = nil switch newValue { case let number as NSNumber: if number.isBool { _type = .Bool } else { _type = .Number } self.rawNumber = number case let string as String: _type = .String self.rawString = string case _ as NSNull: _type = .Null case let array as [AnyObject]: _type = .Array self.rawArray = array case let dictionary as [String : AnyObject]: _type = .Dictionary self.rawDictionary = dictionary default: _type = .Unknown _error = NSError(domain: ErrorDomain, code: ErrorUnsupportedType, userInfo: [NSLocalizedDescriptionKey: "It is a unsupported type"]) } } } /// json type public var type: Type { get { return _type } } /// Error in JSON public var error: NSError? { get { return self._error } } /// The static null json @available(*, unavailable, renamed="null") public static var nullJSON: JSON { get { return null } } public static var null: JSON { get { return JSON(NSNull()) } } } // MARK: - CollectionType, SequenceType, Indexable extension JSON : Swift.CollectionType, Swift.SequenceType, Swift.Indexable { public typealias Generator = JSONGenerator public typealias Index = JSONIndex public var startIndex: JSON.Index { switch self.type { case .Array: return JSONIndex(arrayIndex: self.rawArray.startIndex) case .Dictionary: return JSONIndex(dictionaryIndex: self.rawDictionary.startIndex) default: return JSONIndex() } } public var endIndex: JSON.Index { switch self.type { case .Array: return JSONIndex(arrayIndex: self.rawArray.endIndex) case .Dictionary: return JSONIndex(dictionaryIndex: self.rawDictionary.endIndex) default: return JSONIndex() } } public subscript (position: JSON.Index) -> JSON.Generator.Element { switch self.type { case .Array: return (String(position.arrayIndex), JSON(self.rawArray[position.arrayIndex!])) case .Dictionary: let (key, value) = self.rawDictionary[position.dictionaryIndex!] return (key, JSON(value)) default: return ("", JSON.null) } } /// If `type` is `.Array` or `.Dictionary`, return `array.empty` or `dictonary.empty` otherwise return `true`. public var isEmpty: Bool { get { switch self.type { case .Array: return self.rawArray.isEmpty case .Dictionary: return self.rawDictionary.isEmpty default: return true } } } /// If `type` is `.Array` or `.Dictionary`, return `array.count` or `dictonary.count` otherwise return `0`. public var count: Int { switch self.type { case .Array: return self.rawArray.count case .Dictionary: return self.rawDictionary.count default: return 0 } } public func underestimateCount() -> Int { switch self.type { case .Array: return self.rawArray.underestimateCount() case .Dictionary: return self.rawDictionary.underestimateCount() default: return 0 } } /** If `type` is `.Array` or `.Dictionary`, return a generator over the elements like `Array` or `Dictionary`, otherwise return a generator over empty. - returns: Return a *generator* over the elements of JSON. */ public func generate() -> JSON.Generator { return JSON.Generator(self) } } public struct JSONIndex: ForwardIndexType, _Incrementable, Equatable, Comparable { let arrayIndex: Int? let dictionaryIndex: DictionaryIndex? let type: Type init(){ self.arrayIndex = nil self.dictionaryIndex = nil self.type = .Unknown } init(arrayIndex: Int) { self.arrayIndex = arrayIndex self.dictionaryIndex = nil self.type = .Array } init(dictionaryIndex: DictionaryIndex) { self.arrayIndex = nil self.dictionaryIndex = dictionaryIndex self.type = .Dictionary } public func successor() -> JSONIndex { switch self.type { case .Array: return JSONIndex(arrayIndex: self.arrayIndex!.successor()) case .Dictionary: return JSONIndex(dictionaryIndex: self.dictionaryIndex!.successor()) default: return JSONIndex() } } } public func ==(lhs: JSONIndex, rhs: JSONIndex) -> Bool { switch (lhs.type, rhs.type) { case (.Array, .Array): return lhs.arrayIndex == rhs.arrayIndex case (.Dictionary, .Dictionary): return lhs.dictionaryIndex == rhs.dictionaryIndex default: return false } } public func <(lhs: JSONIndex, rhs: JSONIndex) -> Bool { switch (lhs.type, rhs.type) { case (.Array, .Array): return lhs.arrayIndex < rhs.arrayIndex case (.Dictionary, .Dictionary): return lhs.dictionaryIndex < rhs.dictionaryIndex default: return false } } public func <=(lhs: JSONIndex, rhs: JSONIndex) -> Bool { switch (lhs.type, rhs.type) { case (.Array, .Array): return lhs.arrayIndex <= rhs.arrayIndex case (.Dictionary, .Dictionary): return lhs.dictionaryIndex <= rhs.dictionaryIndex default: return false } } public func >=(lhs: JSONIndex, rhs: JSONIndex) -> Bool { switch (lhs.type, rhs.type) { case (.Array, .Array): return lhs.arrayIndex >= rhs.arrayIndex case (.Dictionary, .Dictionary): return lhs.dictionaryIndex >= rhs.dictionaryIndex default: return false } } public func >(lhs: JSONIndex, rhs: JSONIndex) -> Bool { switch (lhs.type, rhs.type) { case (.Array, .Array): return lhs.arrayIndex > rhs.arrayIndex case (.Dictionary, .Dictionary): return lhs.dictionaryIndex > rhs.dictionaryIndex default: return false } } public struct JSONGenerator : GeneratorType { public typealias Element = (String, JSON) private let type: Type private var dictionayGenerate: DictionaryGenerator? private var arrayGenerate: IndexingGenerator<[AnyObject]>? private var arrayIndex: Int = 0 init(_ json: JSON) { self.type = json.type if type == .Array { self.arrayGenerate = json.rawArray.generate() }else { self.dictionayGenerate = json.rawDictionary.generate() } } public mutating func next() -> JSONGenerator.Element? { switch self.type { case .Array: if let o = self.arrayGenerate?.next() { return (String(self.arrayIndex++), JSON(o)) } else { return nil } case .Dictionary: if let (k, v): (String, AnyObject) = self.dictionayGenerate?.next() { return (k, JSON(v)) } else { return nil } default: return nil } } } // MARK: - Subscript /** * To mark both String and Int can be used in subscript. */ public enum JSONKey { case Index(Int) case Key(String) } public protocol JSONSubscriptType { var jsonKey:JSONKey { get } } extension Int: JSONSubscriptType { public var jsonKey:JSONKey { return JSONKey.Index(self) } } extension String: JSONSubscriptType { public var jsonKey:JSONKey { return JSONKey.Key(self) } } extension JSON { /// If `type` is `.Array`, return json which's object is `array[index]`, otherwise return null json with error. private subscript(index index: Int) -> JSON { get { if self.type != .Array { var r = JSON.null r._error = self._error ?? NSError(domain: ErrorDomain, code: ErrorWrongType, userInfo: [NSLocalizedDescriptionKey: "Array[\(index)] failure, It is not an array"]) return r } else if index >= 0 && index < self.rawArray.count { return JSON(self.rawArray[index]) } else { var r = JSON.null r._error = NSError(domain: ErrorDomain, code:ErrorIndexOutOfBounds , userInfo: [NSLocalizedDescriptionKey: "Array[\(index)] is out of bounds"]) return r } } set { if self.type == .Array { if self.rawArray.count > index && newValue.error == nil { self.rawArray[index] = newValue.object } } } } /// If `type` is `.Dictionary`, return json which's object is `dictionary[key]` , otherwise return null json with error. private subscript(key key: String) -> JSON { get { var r = JSON.null if self.type == .Dictionary { if let o = self.rawDictionary[key] { r = JSON(o) } else { r._error = NSError(domain: ErrorDomain, code: ErrorNotExist, userInfo: [NSLocalizedDescriptionKey: "Dictionary[\"\(key)\"] does not exist"]) } } else { r._error = self._error ?? NSError(domain: ErrorDomain, code: ErrorWrongType, userInfo: [NSLocalizedDescriptionKey: "Dictionary[\"\(key)\"] failure, It is not an dictionary"]) } return r } set { if self.type == .Dictionary && newValue.error == nil { self.rawDictionary[key] = newValue.object } } } /// If `sub` is `Int`, return `subscript(index:)`; If `sub` is `String`, return `subscript(key:)`. private subscript(sub sub: JSONSubscriptType) -> JSON { get { switch sub.jsonKey { case .Index(let index): return self[index: index] case .Key(let key): return self[key: key] } } set { switch sub.jsonKey { case .Index(let index): self[index: index] = newValue case .Key(let key): self[key: key] = newValue } } } /** Find a json in the complex data structuresby using the Int/String's array. - parameter path: The target json's path. Example: let json = JSON[data] let path = [9,"list","person","name"] let name = json[path] The same as: let name = json[9]["list"]["person"]["name"] - returns: Return a json found by the path or a null json with error */ public subscript(path: [JSONSubscriptType]) -> JSON { get { return path.reduce(self) { $0[sub: $1] } } set { switch path.count { case 0: return case 1: self[sub:path[0]].object = newValue.object default: var aPath = path; aPath.removeAtIndex(0) var nextJSON = self[sub: path[0]] nextJSON[aPath] = newValue self[sub: path[0]] = nextJSON } } } /** Find a json in the complex data structuresby using the Int/String's array. - parameter path: The target json's path. Example: let name = json[9,"list","person","name"] The same as: let name = json[9]["list"]["person"]["name"] - returns: Return a json found by the path or a null json with error */ public subscript(path: JSONSubscriptType...) -> JSON { get { return self[path] } set { self[path] = newValue } } } // MARK: - LiteralConvertible extension JSON: Swift.StringLiteralConvertible { public init(stringLiteral value: StringLiteralType) { self.init(value) } public init(extendedGraphemeClusterLiteral value: StringLiteralType) { self.init(value) } public init(unicodeScalarLiteral value: StringLiteralType) { self.init(value) } } extension JSON: Swift.IntegerLiteralConvertible { public init(integerLiteral value: IntegerLiteralType) { self.init(value) } } extension JSON: Swift.BooleanLiteralConvertible { public init(booleanLiteral value: BooleanLiteralType) { self.init(value) } } extension JSON: Swift.FloatLiteralConvertible { public init(floatLiteral value: FloatLiteralType) { self.init(value) } } extension JSON: Swift.DictionaryLiteralConvertible { public init(dictionaryLiteral elements: (String, AnyObject)...) { self.init(elements.reduce([String : AnyObject]()){(dictionary: [String : AnyObject], element:(String, AnyObject)) -> [String : AnyObject] in var d = dictionary d[element.0] = element.1 return d }) } } extension JSON: Swift.ArrayLiteralConvertible { public init(arrayLiteral elements: AnyObject...) { self.init(elements) } } extension JSON: Swift.NilLiteralConvertible { public init(nilLiteral: ()) { self.init(NSNull()) } } // MARK: - Raw extension JSON: Swift.RawRepresentable { public init?(rawValue: AnyObject) { if JSON(rawValue).type == .Unknown { return nil } else { self.init(rawValue) } } public var rawValue: AnyObject { return self.object } public func rawData(options opt: NSJSONWritingOptions = NSJSONWritingOptions(rawValue: 0)) throws -> NSData { guard NSJSONSerialization.isValidJSONObject(self.object) else { throw NSError(domain: ErrorDomain, code: ErrorInvalidJSON, userInfo: [NSLocalizedDescriptionKey: "JSON is invalid"]) } return try NSJSONSerialization.dataWithJSONObject(self.object, options: opt) } public func rawString(encoding: UInt = NSUTF8StringEncoding, options opt: NSJSONWritingOptions = .PrettyPrinted) -> String? { switch self.type { case .Array, .Dictionary: do { let data = try self.rawData(options: opt) return NSString(data: data, encoding: encoding) as? String } catch _ { return nil } case .String: return self.rawString case .Number: return self.rawNumber.stringValue case .Bool: return self.rawNumber.boolValue.description case .Null: return "null" default: return nil } } } // MARK: - Printable, DebugPrintable extension JSON: Swift.Printable, Swift.DebugPrintable { public var description: String { if let string = self.rawString(options:.PrettyPrinted) { return string } else { return "unknown" } } public var debugDescription: String { return description } } // MARK: - Array extension JSON { //Optional [JSON] public var array: [JSON]? { get { if self.type == .Array { return self.rawArray.map{ JSON($0) } } else { return nil } } } //Non-optional [JSON] public var arrayValue: [JSON] { get { return self.array ?? [] } } //Optional [AnyObject] public var arrayObject: [AnyObject]? { get { switch self.type { case .Array: return self.rawArray default: return nil } } set { if let array = newValue { self.object = array } else { self.object = NSNull() } } } } // MARK: - Dictionary extension JSON { //Optional [String : JSON] public var dictionary: [String : JSON]? { if self.type == .Dictionary { return self.rawDictionary.reduce([String : JSON]()) { (dictionary: [String : JSON], element: (String, AnyObject)) -> [String : JSON] in var d = dictionary d[element.0] = JSON(element.1) return d } } else { return nil } } //Non-optional [String : JSON] public var dictionaryValue: [String : JSON] { return self.dictionary ?? [:] } //Optional [String : AnyObject] public var dictionaryObject: [String : AnyObject]? { get { switch self.type { case .Dictionary: return self.rawDictionary default: return nil } } set { if let v = newValue { self.object = v } else { self.object = NSNull() } } } } // MARK: - Bool extension JSON: Swift.BooleanType { //Optional bool public var bool: Bool? { get { switch self.type { case .Bool: return self.rawNumber.boolValue default: return nil } } set { if let newValue = newValue { self.object = NSNumber(bool: newValue) } else { self.object = NSNull() } } } //Non-optional bool public var boolValue: Bool { get { switch self.type { case .Bool, .Number, .String: return self.object.boolValue default: return false } } set { self.object = NSNumber(bool: newValue) } } } // MARK: - String extension JSON { //Optional string public var string: String? { get { switch self.type { case .String: return self.object as? String default: return nil } } set { if let newValue = newValue { self.object = NSString(string:newValue) } else { self.object = NSNull() } } } //Non-optional string public var stringValue: String { get { switch self.type { case .String: return self.object as? String ?? "" case .Number: return self.object.stringValue case .Bool: return (self.object as? Bool).map { String($0) } ?? "" default: return "" } } set { self.object = NSString(string:newValue) } } } // MARK: - Number extension JSON { //Optional number public var number: NSNumber? { get { switch self.type { case .Number, .Bool: return self.rawNumber default: return nil } } set { self.object = newValue ?? NSNull() } } //Non-optional number public var numberValue: NSNumber { get { switch self.type { case .String: let decimal = NSDecimalNumber(string: self.object as? String) if decimal == NSDecimalNumber.notANumber() { // indicates parse error return NSDecimalNumber.zero() } return decimal case .Number, .Bool: return self.object as? NSNumber ?? NSNumber(int: 0) default: return NSNumber(double: 0.0) } } set { self.object = newValue } } } //MARK: - Null extension JSON { public var null: NSNull? { get { switch self.type { case .Null: return self.rawNull default: return nil } } set { self.object = NSNull() } } public func isExists() -> Bool{ if let errorValue = error where errorValue.code == ErrorNotExist{ return false } return true } } //MARK: - URL extension JSON { //Optional URL public var URL: NSURL? { get { switch self.type { case .String: if let encodedString_ = self.rawString.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) { return NSURL(string: encodedString_) } else { return nil } default: return nil } } set { self.object = newValue?.absoluteString ?? NSNull() } } } // MARK: - Int, Double, Float, Int8, Int16, Int32, Int64 extension JSON { public var double: Double? { get { return self.number?.doubleValue } set { if let newValue = newValue { self.object = NSNumber(double: newValue) } else { self.object = NSNull() } } } public var doubleValue: Double { get { return self.numberValue.doubleValue } set { self.object = NSNumber(double: newValue) } } public var float: Float? { get { return self.number?.floatValue } set { if let newValue = newValue { self.object = NSNumber(float: newValue) } else { self.object = NSNull() } } } public var floatValue: Float { get { return self.numberValue.floatValue } set { self.object = NSNumber(float: newValue) } } public var int: Int? { get { return self.number?.longValue } set { if let newValue = newValue { self.object = NSNumber(integer: newValue) } else { self.object = NSNull() } } } public var intValue: Int { get { return self.numberValue.integerValue } set { self.object = NSNumber(integer: newValue) } } public var uInt: UInt? { get { return self.number?.unsignedLongValue } set { if let newValue = newValue { self.object = NSNumber(unsignedLong: newValue) } else { self.object = NSNull() } } } public var uIntValue: UInt { get { return self.numberValue.unsignedLongValue } set { self.object = NSNumber(unsignedLong: newValue) } } public var int8: Int8? { get { return self.number?.charValue } set { if let newValue = newValue { self.object = NSNumber(char: newValue) } else { self.object = NSNull() } } } public var int8Value: Int8 { get { return self.numberValue.charValue } set { self.object = NSNumber(char: newValue) } } public var uInt8: UInt8? { get { return self.number?.unsignedCharValue } set { if let newValue = newValue { self.object = NSNumber(unsignedChar: newValue) } else { self.object = NSNull() } } } public var uInt8Value: UInt8 { get { return self.numberValue.unsignedCharValue } set { self.object = NSNumber(unsignedChar: newValue) } } public var int16: Int16? { get { return self.number?.shortValue } set { if let newValue = newValue { self.object = NSNumber(short: newValue) } else { self.object = NSNull() } } } public var int16Value: Int16 { get { return self.numberValue.shortValue } set { self.object = NSNumber(short: newValue) } } public var uInt16: UInt16? { get { return self.number?.unsignedShortValue } set { if let newValue = newValue { self.object = NSNumber(unsignedShort: newValue) } else { self.object = NSNull() } } } public var uInt16Value: UInt16 { get { return self.numberValue.unsignedShortValue } set { self.object = NSNumber(unsignedShort: newValue) } } public var int32: Int32? { get { return self.number?.intValue } set { if let newValue = newValue { self.object = NSNumber(int: newValue) } else { self.object = NSNull() } } } public var int32Value: Int32 { get { return self.numberValue.intValue } set { self.object = NSNumber(int: newValue) } } public var uInt32: UInt32? { get { return self.number?.unsignedIntValue } set { if let newValue = newValue { self.object = NSNumber(unsignedInt: newValue) } else { self.object = NSNull() } } } public var uInt32Value: UInt32 { get { return self.numberValue.unsignedIntValue } set { self.object = NSNumber(unsignedInt: newValue) } } public var int64: Int64? { get { return self.number?.longLongValue } set { if let newValue = newValue { self.object = NSNumber(longLong: newValue) } else { self.object = NSNull() } } } public var int64Value: Int64 { get { return self.numberValue.longLongValue } set { self.object = NSNumber(longLong: newValue) } } public var uInt64: UInt64? { get { return self.number?.unsignedLongLongValue } set { if let newValue = newValue { self.object = NSNumber(unsignedLongLong: newValue) } else { self.object = NSNull() } } } public var uInt64Value: UInt64 { get { return self.numberValue.unsignedLongLongValue } set { self.object = NSNumber(unsignedLongLong: newValue) } } } //MARK: - Comparable extension JSON : Swift.Comparable {} public func ==(lhs: JSON, rhs: JSON) -> Bool { switch (lhs.type, rhs.type) { case (.Number, .Number): return lhs.rawNumber == rhs.rawNumber case (.String, .String): return lhs.rawString == rhs.rawString case (.Bool, .Bool): return lhs.rawNumber.boolValue == rhs.rawNumber.boolValue case (.Array, .Array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray case (.Dictionary, .Dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary case (.Null, .Null): return true default: return false } } public func <=(lhs: JSON, rhs: JSON) -> Bool { switch (lhs.type, rhs.type) { case (.Number, .Number): return lhs.rawNumber <= rhs.rawNumber case (.String, .String): return lhs.rawString <= rhs.rawString case (.Bool, .Bool): return lhs.rawNumber.boolValue == rhs.rawNumber.boolValue case (.Array, .Array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray case (.Dictionary, .Dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary case (.Null, .Null): return true default: return false } } public func >=(lhs: JSON, rhs: JSON) -> Bool { switch (lhs.type, rhs.type) { case (.Number, .Number): return lhs.rawNumber >= rhs.rawNumber case (.String, .String): return lhs.rawString >= rhs.rawString case (.Bool, .Bool): return lhs.rawNumber.boolValue == rhs.rawNumber.boolValue case (.Array, .Array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray case (.Dictionary, .Dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary case (.Null, .Null): return true default: return false } } public func >(lhs: JSON, rhs: JSON) -> Bool { switch (lhs.type, rhs.type) { case (.Number, .Number): return lhs.rawNumber > rhs.rawNumber case (.String, .String): return lhs.rawString > rhs.rawString default: return false } } public func <(lhs: JSON, rhs: JSON) -> Bool { switch (lhs.type, rhs.type) { case (.Number, .Number): return lhs.rawNumber < rhs.rawNumber case (.String, .String): return lhs.rawString < rhs.rawString default: return false } } private let trueNumber = NSNumber(bool: true) private let falseNumber = NSNumber(bool: false) private let trueObjCType = String.fromCString(trueNumber.objCType) private let falseObjCType = String.fromCString(falseNumber.objCType) // MARK: - NSNumber: Comparable extension NSNumber { var isBool:Bool { get { let objCType = String.fromCString(self.objCType) if (self.compare(trueNumber) == NSComparisonResult.OrderedSame && objCType == trueObjCType) || (self.compare(falseNumber) == NSComparisonResult.OrderedSame && objCType == falseObjCType){ return true } else { return false } } } } func ==(lhs: NSNumber, rhs: NSNumber) -> Bool { switch (lhs.isBool, rhs.isBool) { case (false, true): return false case (true, false): return false default: return lhs.compare(rhs) == NSComparisonResult.OrderedSame } } func !=(lhs: NSNumber, rhs: NSNumber) -> Bool { return !(lhs == rhs) } func <(lhs: NSNumber, rhs: NSNumber) -> Bool { switch (lhs.isBool, rhs.isBool) { case (false, true): return false case (true, false): return false default: return lhs.compare(rhs) == NSComparisonResult.OrderedAscending } } func >(lhs: NSNumber, rhs: NSNumber) -> Bool { switch (lhs.isBool, rhs.isBool) { case (false, true): return false case (true, false): return false default: return lhs.compare(rhs) == NSComparisonResult.OrderedDescending } } func <=(lhs: NSNumber, rhs: NSNumber) -> Bool { switch (lhs.isBool, rhs.isBool) { case (false, true): return false case (true, false): return false default: return lhs.compare(rhs) != NSComparisonResult.OrderedDescending } } func >=(lhs: NSNumber, rhs: NSNumber) -> Bool { switch (lhs.isBool, rhs.isBool) { case (false, true): return false case (true, false): return false default: return lhs.compare(rhs) != NSComparisonResult.OrderedAscending } } ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus/main.swift ================================================ // // main.swift // Nimbus // // Created by Ethan Lowman on 7/25/14. // Copyright (c) 2014 Ethanal. All rights reserved. // import Cocoa NSApplicationMain(Process.argc, Process.unsafeArgv) ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ AA0C67691983375C004BBF89 /* menubar-error.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C675B1983375C004BBF89 /* menubar-error.png */; }; AA0C676A1983375C004BBF89 /* menubar-error@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C675C1983375C004BBF89 /* menubar-error@2x.png */; }; AA0C676B1983375C004BBF89 /* menubar-highlighted.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C675D1983375C004BBF89 /* menubar-highlighted.png */; }; AA0C676C1983375C004BBF89 /* menubar-highlighted@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C675E1983375C004BBF89 /* menubar-highlighted@2x.png */; }; AA0C676D1983375C004BBF89 /* menubar-progress-1.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C675F1983375C004BBF89 /* menubar-progress-1.png */; }; AA0C676E1983375C004BBF89 /* menubar-progress-1@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C67601983375C004BBF89 /* menubar-progress-1@2x.png */; }; AA0C676F1983375C004BBF89 /* menubar-progress-2.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C67611983375C004BBF89 /* menubar-progress-2.png */; }; AA0C67701983375C004BBF89 /* menubar-progress-2@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C67621983375C004BBF89 /* menubar-progress-2@2x.png */; }; AA0C67711983375C004BBF89 /* menubar-progress-3.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C67631983375C004BBF89 /* menubar-progress-3.png */; }; AA0C67721983375C004BBF89 /* menubar-progress-3@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C67641983375C004BBF89 /* menubar-progress-3@2x.png */; }; AA0C67731983375C004BBF89 /* menubar-success.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C67651983375C004BBF89 /* menubar-success.png */; }; AA0C67741983375C004BBF89 /* menubar-success@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C67661983375C004BBF89 /* menubar-success@2x.png */; }; AA0C67751983375C004BBF89 /* menubar.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C67671983375C004BBF89 /* menubar.png */; }; AA0C67761983375C004BBF89 /* menubar@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AA0C67681983375C004BBF89 /* menubar@2x.png */; }; AA0C678619835367004BBF89 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0C678519835367004BBF89 /* APIClient.swift */; }; AA9325B319831B3E0027847E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9325B219831B3E0027847E /* main.swift */; }; AA9325B519831B3E0027847E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9325B419831B3E0027847E /* AppDelegate.swift */; }; AA9325B719831B3E0027847E /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA9325B619831B3E0027847E /* Images.xcassets */; }; AA9325BA19831B3E0027847E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA9325B819831B3E0027847E /* MainMenu.xib */; }; AA9325C619831B3E0027847E /* NimbusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9325C519831B3E0027847E /* NimbusTests.swift */; }; AA9325F119831C860027847E /* StatusItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9325F019831C860027847E /* StatusItemView.swift */; }; AAD4434A19835E9900AA5459 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD4434819835E9900AA5459 /* PreferencesWindowController.swift */; }; AAD4434B19835E9900AA5459 /* PreferencesWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = AAD4434919835E9900AA5459 /* PreferencesWindowController.xib */; }; AAD4434D19835ED800AA5459 /* PreferencesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD4434C19835ED800AA5459 /* PreferencesManager.swift */; }; AAD4434F1983952100AA5459 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD4434E1983952100AA5459 /* KeychainManager.swift */; }; AAD443511984085B00AA5459 /* SwiftyJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD443501984085B00AA5459 /* SwiftyJSON.swift */; }; AADCA9EE1984DFEB00304703 /* ScreenshotWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = AADCA9ED1984DFEB00304703 /* ScreenshotWatcher.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ AA9325C019831B3E0027847E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AA9325A519831B3E0027847E /* Project object */; proxyType = 1; remoteGlobalIDString = AA9325AC19831B3E0027847E; remoteInfo = Nimbus; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ AA0C675B1983375C004BBF89 /* menubar-error.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-error.png"; sourceTree = ""; }; AA0C675C1983375C004BBF89 /* menubar-error@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-error@2x.png"; sourceTree = ""; }; AA0C675D1983375C004BBF89 /* menubar-highlighted.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-highlighted.png"; sourceTree = ""; }; AA0C675E1983375C004BBF89 /* menubar-highlighted@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-highlighted@2x.png"; sourceTree = ""; }; AA0C675F1983375C004BBF89 /* menubar-progress-1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-progress-1.png"; sourceTree = ""; }; AA0C67601983375C004BBF89 /* menubar-progress-1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-progress-1@2x.png"; sourceTree = ""; }; AA0C67611983375C004BBF89 /* menubar-progress-2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-progress-2.png"; sourceTree = ""; }; AA0C67621983375C004BBF89 /* menubar-progress-2@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-progress-2@2x.png"; sourceTree = ""; }; AA0C67631983375C004BBF89 /* menubar-progress-3.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-progress-3.png"; sourceTree = ""; }; AA0C67641983375C004BBF89 /* menubar-progress-3@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-progress-3@2x.png"; sourceTree = ""; }; AA0C67651983375C004BBF89 /* menubar-success.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-success.png"; sourceTree = ""; }; AA0C67661983375C004BBF89 /* menubar-success@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-success@2x.png"; sourceTree = ""; }; AA0C67671983375C004BBF89 /* menubar.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menubar.png; sourceTree = ""; }; AA0C67681983375C004BBF89 /* menubar@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar@2x.png"; sourceTree = ""; }; AA0C678519835367004BBF89 /* APIClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; }; AA9325AD19831B3E0027847E /* Nimbus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nimbus.app; sourceTree = BUILT_PRODUCTS_DIR; }; AA9325B119831B3E0027847E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AA9325B219831B3E0027847E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; AA9325B419831B3E0027847E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AA9325B619831B3E0027847E /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; AA9325B919831B3E0027847E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; AA9325BF19831B3E0027847E /* NimbusTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NimbusTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AA9325C419831B3E0027847E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AA9325C519831B3E0027847E /* NimbusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbusTests.swift; sourceTree = ""; }; AA9325F019831C860027847E /* StatusItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusItemView.swift; sourceTree = ""; }; AAD4434819835E9900AA5459 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; AAD4434919835E9900AA5459 /* PreferencesWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PreferencesWindowController.xib; sourceTree = ""; }; AAD4434C19835ED800AA5459 /* PreferencesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesManager.swift; sourceTree = ""; }; AAD4434E1983952100AA5459 /* KeychainManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = ""; }; AAD443501984085B00AA5459 /* SwiftyJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyJSON.swift; sourceTree = ""; }; AADCA9E81984DCC400304703 /* Nimbus-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Nimbus-Bridging-Header.h"; sourceTree = ""; }; AADCA9EC1984DFEB00304703 /* ScreenshotWatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScreenshotWatcher.h; sourceTree = ""; }; AADCA9ED1984DFEB00304703 /* ScreenshotWatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ScreenshotWatcher.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ AA9325AA19831B3E0027847E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; AA9325BC19831B3E0027847E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ AA9325A419831B3E0027847E = { isa = PBXGroup; children = ( AA9325AF19831B3E0027847E /* Nimbus */, AA9325C219831B3E0027847E /* NimbusTests */, AA9325AE19831B3E0027847E /* Products */, ); sourceTree = ""; }; AA9325AE19831B3E0027847E /* Products */ = { isa = PBXGroup; children = ( AA9325AD19831B3E0027847E /* Nimbus.app */, AA9325BF19831B3E0027847E /* NimbusTests.xctest */, ); name = Products; sourceTree = ""; }; AA9325AF19831B3E0027847E /* Nimbus */ = { isa = PBXGroup; children = ( AA9325B419831B3E0027847E /* AppDelegate.swift */, AA9325F019831C860027847E /* StatusItemView.swift */, AAD4434819835E9900AA5459 /* PreferencesWindowController.swift */, AAD4434919835E9900AA5459 /* PreferencesWindowController.xib */, AA0C678519835367004BBF89 /* APIClient.swift */, AAD4434C19835ED800AA5459 /* PreferencesManager.swift */, AAD4434E1983952100AA5459 /* KeychainManager.swift */, AAD443501984085B00AA5459 /* SwiftyJSON.swift */, AADCA9EC1984DFEB00304703 /* ScreenshotWatcher.h */, AADCA9ED1984DFEB00304703 /* ScreenshotWatcher.m */, AADCA9E81984DCC400304703 /* Nimbus-Bridging-Header.h */, AA9325B619831B3E0027847E /* Images.xcassets */, AA9325CF19831B6A0027847E /* Assets */, AA9325B019831B3E0027847E /* Supporting Files */, ); path = Nimbus; sourceTree = ""; }; AA9325B019831B3E0027847E /* Supporting Files */ = { isa = PBXGroup; children = ( AA9325B119831B3E0027847E /* Info.plist */, AA9325B819831B3E0027847E /* MainMenu.xib */, AA9325B219831B3E0027847E /* main.swift */, ); name = "Supporting Files"; sourceTree = ""; }; AA9325C219831B3E0027847E /* NimbusTests */ = { isa = PBXGroup; children = ( AA9325C519831B3E0027847E /* NimbusTests.swift */, AA9325C319831B3E0027847E /* Supporting Files */, ); path = NimbusTests; sourceTree = ""; }; AA9325C319831B3E0027847E /* Supporting Files */ = { isa = PBXGroup; children = ( AA9325C419831B3E0027847E /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; AA9325CF19831B6A0027847E /* Assets */ = { isa = PBXGroup; children = ( AA0C675B1983375C004BBF89 /* menubar-error.png */, AA0C675C1983375C004BBF89 /* menubar-error@2x.png */, AA0C675D1983375C004BBF89 /* menubar-highlighted.png */, AA0C675E1983375C004BBF89 /* menubar-highlighted@2x.png */, AA0C675F1983375C004BBF89 /* menubar-progress-1.png */, AA0C67601983375C004BBF89 /* menubar-progress-1@2x.png */, AA0C67611983375C004BBF89 /* menubar-progress-2.png */, AA0C67621983375C004BBF89 /* menubar-progress-2@2x.png */, AA0C67631983375C004BBF89 /* menubar-progress-3.png */, AA0C67641983375C004BBF89 /* menubar-progress-3@2x.png */, AA0C67651983375C004BBF89 /* menubar-success.png */, AA0C67661983375C004BBF89 /* menubar-success@2x.png */, AA0C67671983375C004BBF89 /* menubar.png */, AA0C67681983375C004BBF89 /* menubar@2x.png */, ); name = Assets; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ AA9325AC19831B3E0027847E /* Nimbus */ = { isa = PBXNativeTarget; buildConfigurationList = AA9325C919831B3E0027847E /* Build configuration list for PBXNativeTarget "Nimbus" */; buildPhases = ( AA9325A919831B3E0027847E /* Sources */, AA9325AA19831B3E0027847E /* Frameworks */, AA9325AB19831B3E0027847E /* Resources */, ); buildRules = ( ); dependencies = ( ); name = Nimbus; productName = Nimbus; productReference = AA9325AD19831B3E0027847E /* Nimbus.app */; productType = "com.apple.product-type.application"; }; AA9325BE19831B3E0027847E /* NimbusTests */ = { isa = PBXNativeTarget; buildConfigurationList = AA9325CC19831B3E0027847E /* Build configuration list for PBXNativeTarget "NimbusTests" */; buildPhases = ( AA9325BB19831B3E0027847E /* Sources */, AA9325BC19831B3E0027847E /* Frameworks */, AA9325BD19831B3E0027847E /* Resources */, ); buildRules = ( ); dependencies = ( AA9325C119831B3E0027847E /* PBXTargetDependency */, ); name = NimbusTests; productName = NimbusTests; productReference = AA9325BF19831B3E0027847E /* NimbusTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ AA9325A519831B3E0027847E /* Project object */ = { isa = PBXProject; attributes = { LastSwiftMigration = 0720; LastSwiftUpdateCheck = 0720; LastUpgradeCheck = 0720; ORGANIZATIONNAME = Ethanal; TargetAttributes = { AA9325AC19831B3E0027847E = { CreatedOnToolsVersion = 6.0; }; AA9325BE19831B3E0027847E = { CreatedOnToolsVersion = 6.0; TestTargetID = AA9325AC19831B3E0027847E; }; }; }; buildConfigurationList = AA9325A819831B3E0027847E /* Build configuration list for PBXProject "Nimbus" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = AA9325A419831B3E0027847E; productRefGroup = AA9325AE19831B3E0027847E /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( AA9325AC19831B3E0027847E /* Nimbus */, AA9325BE19831B3E0027847E /* NimbusTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ AA9325AB19831B3E0027847E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( AAD4434B19835E9900AA5459 /* PreferencesWindowController.xib in Resources */, AA0C676F1983375C004BBF89 /* menubar-progress-2.png in Resources */, AA0C67711983375C004BBF89 /* menubar-progress-3.png in Resources */, AA0C67701983375C004BBF89 /* menubar-progress-2@2x.png in Resources */, AA9325B719831B3E0027847E /* Images.xcassets in Resources */, AA0C67691983375C004BBF89 /* menubar-error.png in Resources */, AA0C676E1983375C004BBF89 /* menubar-progress-1@2x.png in Resources */, AA0C67761983375C004BBF89 /* menubar@2x.png in Resources */, AA0C67721983375C004BBF89 /* menubar-progress-3@2x.png in Resources */, AA0C676A1983375C004BBF89 /* menubar-error@2x.png in Resources */, AA0C67751983375C004BBF89 /* menubar.png in Resources */, AA0C676D1983375C004BBF89 /* menubar-progress-1.png in Resources */, AA0C676C1983375C004BBF89 /* menubar-highlighted@2x.png in Resources */, AA0C67731983375C004BBF89 /* menubar-success.png in Resources */, AA0C67741983375C004BBF89 /* menubar-success@2x.png in Resources */, AA9325BA19831B3E0027847E /* MainMenu.xib in Resources */, AA0C676B1983375C004BBF89 /* menubar-highlighted.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; AA9325BD19831B3E0027847E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ AA9325A919831B3E0027847E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( AAD4434D19835ED800AA5459 /* PreferencesManager.swift in Sources */, AAD4434A19835E9900AA5459 /* PreferencesWindowController.swift in Sources */, AAD4434F1983952100AA5459 /* KeychainManager.swift in Sources */, AAD443511984085B00AA5459 /* SwiftyJSON.swift in Sources */, AA9325F119831C860027847E /* StatusItemView.swift in Sources */, AADCA9EE1984DFEB00304703 /* ScreenshotWatcher.m in Sources */, AA9325B519831B3E0027847E /* AppDelegate.swift in Sources */, AA0C678619835367004BBF89 /* APIClient.swift in Sources */, AA9325B319831B3E0027847E /* main.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; AA9325BB19831B3E0027847E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( AA9325C619831B3E0027847E /* NimbusTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ AA9325C119831B3E0027847E /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = AA9325AC19831B3E0027847E /* Nimbus */; targetProxy = AA9325C019831B3E0027847E /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ AA9325B819831B3E0027847E /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( AA9325B919831B3E0027847E /* Base */, ); name = MainMenu.xib; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ AA9325C719831B3E0027847E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.9; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; AA9325C819831B3E0027847E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.9; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; }; name = Release; }; AA9325CA19831B3E0027847E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; GCC_OPTIMIZATION_LEVEL = 0; INFOPLIST_FILE = Nimbus/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.ethanal.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Nimbus/Nimbus-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; AA9325CB19831B3E0027847E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; GCC_OPTIMIZATION_LEVEL = fast; INFOPLIST_FILE = Nimbus/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.ethanal.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Nimbus/Nimbus-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Release; }; AA9325CD19831B3E0027847E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Nimbus.app/Contents/MacOS/Nimbus"; COMBINE_HIDPI_IMAGES = YES; FRAMEWORK_SEARCH_PATHS = ( "$(DEVELOPER_FRAMEWORKS_DIR)", "$(inherited)", ); GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = NimbusTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.ethanal.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUNDLE_LOADER)"; }; name = Debug; }; AA9325CE19831B3E0027847E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Nimbus.app/Contents/MacOS/Nimbus"; COMBINE_HIDPI_IMAGES = YES; FRAMEWORK_SEARCH_PATHS = ( "$(DEVELOPER_FRAMEWORKS_DIR)", "$(inherited)", ); INFOPLIST_FILE = NimbusTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.ethanal.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUNDLE_LOADER)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ AA9325A819831B3E0027847E /* Build configuration list for PBXProject "Nimbus" */ = { isa = XCConfigurationList; buildConfigurations = ( AA9325C719831B3E0027847E /* Debug */, AA9325C819831B3E0027847E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; AA9325C919831B3E0027847E /* Build configuration list for PBXNativeTarget "Nimbus" */ = { isa = XCConfigurationList; buildConfigurations = ( AA9325CA19831B3E0027847E /* Debug */, AA9325CB19831B3E0027847E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; AA9325CC19831B3E0027847E /* Build configuration list for PBXNativeTarget "NimbusTests" */ = { isa = XCConfigurationList; buildConfigurations = ( AA9325CD19831B3E0027847E /* Debug */, AA9325CE19831B3E0027847E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = AA9325A519831B3E0027847E /* Project object */; } ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: NimbusMenuBarApp/Nimbus/Nimbus.xcodeproj/xcshareddata/xcschemes/Nimbus.xcscheme ================================================ ================================================ FILE: NimbusMenuBarApp/Nimbus/NimbusTests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: NimbusMenuBarApp/Nimbus/NimbusTests/NimbusTests.swift ================================================ // // NimbusTests.swift // NimbusTests // // Created by Ethan Lowman on 7/25/14. // Copyright (c) 2014 Ethanal. All rights reserved. // import Cocoa import XCTest class NimbusTests: XCTestCase { override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testExample() { // This is an example of a functional test case. XCTAssert(true, "Pass") } func testPerformanceExample() { // This is an example of a performance test case. self.measureBlock() { // Put the code you want to measure the time of here. } } } ================================================ FILE: README.md ================================================

Nimbus Logo
Nimbus

Nimbus is a private file sharer and URL shortener. Heavily inspired by [Cloudapp](http://www.getcloudapp.com/), Nimbus is a free and open source solution to file sharing and URL shortening that you can host yourself and fully control. Nimbus consists of several components: * A website that displays sharing pages for generated links and redirects shortened URLs to their targets * A website to manage shared items * An API to manipulate shared items * A Mac OS X menubar app to upload files and shorten links The menubar app is only compatible with OS X 10.9 and up since it is written in [Swift](https://developer.apple.com/swift/). The files are stored in Amazon S3, so you must have an AWS account. ## Screenshots
Screenshot Screenshot
Screenshot Screenshot
## Features - Share pages that show file previews or redirect to the shortened link - Image file previews - Text file previews with automatic syntax highlighting if applicable - Screenshots are automatically uploaded and the share link is copied to the clipboard - Drag a file or text to the menubar icon to upload it and copy the share link to the clipboard - Drag a URL to the menubar icon to create a shortened link and copy it to the clipboard - Keep track of view counts for files and shortened URLS ## Setup To set up the Django app, perform the following steps on your server (assumes [pip](http://pip.readthedocs.org/en/latest/), [virtualenv](http://virtualenv.readthedocs.org/en/latest/), and [MySQL](http://www.mysql.com/) are already installed) 1. Create a virtualenv and activate it 2. Clone the repository (from here on, it is assumed that the respository's location is `/usr/local/www/Nimbus`) 3. While in the repository root, install the Python requirements by running ```bash pip install -r requirements/production.txt ``` 4. Create a database and grant a user full access to it. 5. Follow the instructions in `nimbus/settings/secret.sample.py` to create a secrets file with your MySQL and Amazon S3 credentials 6. Set up the environment for the Django app by running ```bash export PRODUCTION=TRUE ``` 7. Set up the database and create your user by running `./manage.py syncdb` 8. Start a Django shell (`./manage.py shell`) and run the following commands, replacing `example.com` with your domain name ```python from django.contrib.sites.models import Site Site.objects.update(name="example.com", domain="example.com") ``` 9. Collect static files by running ```bash yes yes | ./manage.py collectstatic ``` ### Serving Nimbus Make sure you have a domain name configured with the following records: ``` @ IN A api CNAME @ account CNAME @ files CNAME files..s3.amazonaws.com. ``` Also make sure you have an Amazon S3 bucket called `files.` The recommended setup for serving Nimbus is [Gunicorn](http://gunicorn.org/) managed by [Supervisor](http://supervisord.org/) with [nginx](http://nginx.org/) as a reverse proxy. Configuration requirements are as follows. * Nginx must be listening on the subdomains `account` and `api` of your domain as well as the root domain. Forward all of this traffic to Gunicorn - the Django app handles the subdomain routing. * The attribute `client_max_body_size` must be set in the nginx config to a sufficiently large value to allow uploads of big files. * Static file requests (`/static/`) should be aliased to `nimbus/collected_static` in the repository root * Supervisor must call the version of gunicorn in your virtualenv #### Example Supervisor Configuration ```ini [program:nimbus] directory = /usr/local/www/Nimbus user = nobody command = /usr/local/virtualenvs/Nimbus/bin/gunicorn nimbus.wsgi:application --user=nobody --workers=1 --bind=127.0.0.1:8080 environment = PRODUCTION=TRUE stdout_logfile = /var/log/sites/nimbus.gunicorn.log stderr_logfile = /var/log/sites/nimbus.gunicorn.log autostart = true autorestart = true ``` #### Example Nginx Configuration ```nginx server { listen 80; server_name example.com account.example.com api.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name example.com account.example.com api.example.com; ssl on; ssl_certificate /usr/local/certs/example.com.crt; ssl_certificate_key /usr/local/certs/example.com.key; client_max_body_size 1024M; access_log /var/log/sites/nimbus.access.log; error_log /var/log/sites/nimbus.error.log; location /favicon.ico { alias /usr/local/www/Nimbus/nimbus/static/img/favicon.ico; } location /static/ { alias /usr/local/www/Nimbus/nimbus/collected_static/; } location / { rewrite ^/((?!(api-auth|admin))(.*))/$ /$1 permanent; proxy_pass http://127.0.0.1:8080; proxy_set_header X-Forwarded-Host $host; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } } ``` ## API Reference API documentation can be found [here](api_docs.md). ## Contact Ethan Lowman - https://github.com/ethanal - ethan@ethanlowman.com ================================================ FILE: api_docs.md ================================================ # API Reference The Nimbus API uses token authentication, so it is recommended that you secure the `account` and `api` subdomains with HTTPS. The `Authorization` header for requests that require it should have the following form: ``` Token 81d1445d307741e63f5c6f26d4e840175c21a34d ``` The term "media item" refers to a file or a shortened link. *** ## Obtain Authorization Token Obtain the API authorization token corresponding to a username/password pair. ### Request - Requires: Authentication - HTTP Request Method: `POST` - URL: `/api-token-auth` - Parameters - `username`: The username for the user whose token should be returned - `password`: The password for the user whose token should be returned ### Response - Status: 200 OK - Body: ```js { "token": "81d1445d307741e63f5c6f26d4e840175c21a34d" } ``` ### Errors If the provided credentials are not valid, the following response is returned: - Status: 400 Bad Request - Body: ```js { "non_field_errors": [ "Unable to login with provided credentials." ] } ``` *** ## List Media Items List all media items created by the authorized user. All media items are serialized the same way. Links and files can be differentiated using the `media_type` attribute. Valid media type codes are as follows: - `URL`: Shortened URLS - `IMG`: Image files - `TXT`: Text files - `ARC`: Archive files - `AUD`: Audio files - `VID`: Video files - `ETC`: Other files ### Request - Requires: Authentication - HTTP Request Method: `GET` - URL: `/media/list` - Optional URL Parameters: - `media_type`: The media type code used to filter the list (e.g. `GET /media/list?media_type=IMG` will list all images) ### Response - Status: 200 OK - Body: ```js [ { "url_hash": "clJwWj", "share_url": "http://example.com/clJwWj", "name": "example.png", "target_url": "", "target_file": "de807a6626ed47c1adf3696bfb2cb9ef/example.png", "target_file_url": "http://files.example.com/de807a6626ed47c1adf3696bfb2cb9ef/example.png", "view_count": 4, "upload_date": "2014-01-02T03:04:05.060Z", "media_type": "IMG" }, { "url_hash": "i7UrcU", "share_url": "http://example.com/i7UrcU", "name": "http://en.wikipedia.org/wiki/Example", "target_url": "http://en.wikipedia.org/wiki/Example", "target_file": "", "target_file_url": "", "view_count": 2, "upload_date": "upload_date": "2014-01-02T03:45:06.070Z", "media_type": "URL" } ] ``` *** ## Show Media Item Details Show details for a media item. ### Request - Requires: Authentication - HTTP Request Method: `GET` - URL: `/media/show` - URL Parameters: - `url_hash`: The media type code used to filter the list (e.g. `GET /media/list?media_type=IMG` will list all images) ### Response - Status: 200 OK - Body: ```js { "url_hash": "clJwWj", "share_url": "http://example.com/clJwWj", "name": "example.png", "target_url": "", "target_file": "de807a6626ed47c1adf3696bfb2cb9ef/example.png", "target_file_url": "http://files.example.com/de807a6626ed47c1adf3696bfb2cb9ef/example.png", "view_count": 4, "upload_date": "2014-01-02T03:04:05.060Z", "media_type": "IMG" } ``` *** ## Upload File Create a media item for a file. ### Request - Requires: Authentication - HTTP Request Method: `POST` - URL: `/media/add_file` - Parameters - `file`: The file to upload. ### Response - Status: 201 Created - Body: ```js { "url_hash": "clJwWj", "share_url": "http://example.com/clJwWj", "name": "example.png", "target_file": "de807a6626ed47c1adf3696bfb2cb9ef/example.png", "target_file_url": "http://files.example.com/de807a6626ed47c1adf3696bfb2cb9ef/example.png", "upload_date": "2014-01-02T03:04:05.060Z", "media_type": "IMG" } ``` *** ## Add Link Create a media item for a URL. ### Request - Requires: Authentication - HTTP Request Method: `POST` - URL: `/media/add_link` - Parameters - `target_url`: The URL to shorten. ### Response - Status: 201 Created - Body: ```js { "url_hash": "i7UrcU", "share_url": "http://example.com/i7UrcU", "target_url": "http://en.wikipedia.org/wiki/Example", "upload_date": "2014-01-02T03:45:06.070Z" } ``` *** ## Delete Media Items Delete one or more media items. ### Request - Requires: Authentication - HTTP Request Method: `DELETE` - URL: `/media/delete` - URL Parameters - `url_hash`: The URL hash of the media item that should be deleted. This parameter can be repeated to delete multiple items. ### Response - Status: 204 No Content *** ================================================ FILE: graphics_assets/assets.sketch/metadata ================================================ app com.bohemiancoding.sketch3 build 8054 commit b2079fe10151ad0eef6cc553b7369ec78d67b9b5 fonts length 193569 version 37 ================================================ FILE: graphics_assets/assets.sketch/version ================================================ 37 ================================================ FILE: manage.py ================================================ #!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "nimbus.settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) ================================================ FILE: nimbus/__init__.py ================================================ ================================================ FILE: nimbus/apps/__init__.py ================================================ from django.conf.urls import patterns, include, url from nimbus import settings def debug_urls(): if settings.SHOW_DEBUG_TOOLBAR: import debug_toolbar return patterns("", url(r"^__debug__/", include(debug_toolbar.urls)), ) return [] ================================================ FILE: nimbus/apps/accounts/__init__.py ================================================ ================================================ FILE: nimbus/apps/accounts/forms.py ================================================ import logging from django.contrib.auth.forms import AuthenticationForm from django import forms from django.utils.html import strip_tags logger = logging.getLogger(__name__) class AuthenticateForm(AuthenticationForm): username = forms.CharField(required=True, widget=forms.widgets.TextInput(attrs={"placeholder": "Username"}), error_messages={"required": "Invalid username"}) password = forms.CharField(required=True, widget=forms.widgets.PasswordInput(attrs={"placeholder": "Password"}), error_messages={"required": "Invalid password"}) def is_valid(self): form = super(AuthenticateForm, self).is_valid() for f, error in self.errors.iteritems(): if f == "__all__": self.fields["password"].widget.attrs.update({"class": "error", "placeholder": "Invalid password"}) else: self.fields[f].widget.attrs.update({"class": "error", "placeholder": strip_tags(str(error))}) logger.debug(self.errors) return form class UploadFileForm(forms.Form): file = forms.FileField() ================================================ FILE: nimbus/apps/accounts/urls.py ================================================ from django.conf.urls import include, url from django.contrib import admin from . import views from nimbus.apps import debug_urls urlpatterns = debug_urls() admin.autodiscover() urlpatterns += [ url(r'^admin/', include(admin.site.urls)), url(r"^$", views.index, name="index"), url(r"^login$", views.login_view.as_view(), name="login"), url(r"^logout$", views.logout_view, name="logout"), url(r"^(?P(images)|(links)|(text)|(archives)|(audio)|(video)|(other))$", views.dashboard_view, name="filter_media"), ] ================================================ FILE: nimbus/apps/accounts/views.py ================================================ import logging from .forms import AuthenticateForm from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.shortcuts import render, redirect from django.contrib.auth import login, logout from django.views.generic.base import View from nimbus.apps.media.models import Media logger = logging.getLogger(__name__) def index(request, auth_form=None): if request.user.is_authenticated(): return dashboard_view(request) else: auth_form = auth_form or AuthenticateForm() request.session.set_test_cookie() return render(request, "nimbus/accounts/login.html", { "auth_form": auth_form }) class login_view(View): def post(self, request): form = AuthenticateForm(data=request.POST) if request.session.test_cookie_worked(): request.session.delete_test_cookie() else: logger.error("No cookie support detected! This could cause problems.") if form.is_valid(): login(request, form.get_user()) logger.info("Login succeeded as {}".format(request.POST.get("username", "unknown"))) next = request.GET.get("next", "/") return redirect(next) else: logger.info("Login failed as {}".format(request.POST.get("username", "unknown"))) return index(request, auth_form=form) # Modified to show errors def get(self, request): return index(request) def logout_view(request): logout(request) return redirect("/") @login_required def dashboard_view(request, media_type="all"): media_type_codes = {j.lower(): i for i, j in Media.MEDIA_TYPES_PLURAL} if media_type == "all": media_list = Media.objects.filter(user=request.user).order_by("-upload_date", "name") else: media_list = Media.objects.filter(user=request.user, media_type=media_type_codes[media_type]).order_by("-upload_date", "name") paginator = Paginator(media_list, 50) page = request.GET.get("p") try: media = paginator.page(page) except PageNotAnInteger: media = paginator.page(1) except EmptyPage: media = paginator.page(paginator.num_pages) return render(request, "nimbus/accounts/dashboard.html", { "media_type": media_type, "media_type_code": media_type_codes.get(media_type, "ALL"), "media": media }) ================================================ FILE: nimbus/apps/api/__init__.py ================================================ ================================================ FILE: nimbus/apps/api/urls.py ================================================ from django.conf.urls import url, include from nimbus.apps import debug_urls from . import views urlpatterns = debug_urls() urlpatterns += [ url(r"^$", views.api_root, name="api_root"), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), url(r'^api-token-auth$', 'rest_framework.authtoken.views.obtain_auth_token'), url(r"^media/list$", views.MediaList.as_view(), name="media_list"), url(r"^media/show$", views.MediaDetail.as_view(), name="media_detail"), url(r"^media/add_file$", views.AddFile.as_view(), name="add_file"), url(r"^media/add_link$", views.AddLink.as_view(), name="add_link"), url(r"^media/delete$", views.delete_media, name="delete_media"), ] ================================================ FILE: nimbus/apps/api/views.py ================================================ from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string from rest_framework import generics, views, status from rest_framework.decorators import api_view from rest_framework.exceptions import APIException from rest_framework.response import Response from rest_framework.parsers import MultiPartParser from nimbus.apps.media.models import Media from nimbus.apps.media.serializers import MediaSerializer, CreateLinkSerializer, ViewCreatedFileSerializer, ViewCreatedLinkSerializer class URLHashParameterRequiredException(APIException): status_code = 400 default_detail = "'url_hash' parameter required" @api_view(("GET",)) def api_root(request): """Welcome to the Nimbus API! Documentation can be found at [github.com/ethanal/nimbus](http://github.com/ethanal/nimbus) """ return Response("Documentation can be found at http://github.com/ethanal/nimbus") class MediaList(generics.ListAPIView): serializer_class = MediaSerializer def get_queryset(self): user = self.request.user if "media_type" in self.request.QUERY_PARAMS: return Media.objects.filter(user=user, media_type=self.requeset.QUERY_PARAMS["media_type"]) return Media.objects.filter(user=user) class MediaDetail(generics.RetrieveAPIView): serializer_class = MediaSerializer def get_object(self): if "url_hash" not in self.request.QUERY_PARAMS: raise URLHashParameterRequiredException() queryset = Media.objects.filter(user=self.request.user) return get_object_or_404(queryset, url_hash=self.request.QUERY_PARAMS["url_hash"]) class AddFile(views.APIView): parser_classes = (MultiPartParser,) def post(self, request): f = request.FILES.get("file", None) if not f: return Response(status=400) media_item = Media(name=f.name, user=request.user, target_file=f) media_item.save() if media_item.media_type == "TXT": text = f.file.getvalue() media_item.fill_syntax_highlighted(text) data = ViewCreatedFileSerializer(media_item).data if "include-html" in request.QUERY_PARAMS: context = { "media_item": media_item } html = render_to_string("nimbus/accounts/media_table_row.html", context) data["html"] = html return Response(data, status=201) class AddLink(generics.CreateAPIView): serializer_class = CreateLinkSerializer def pre_save(self, obj): obj.user = self.request.user obj.name = obj.target_url def create(self, request, *args, **kwargs): response = super(AddLink, self).create(request, *args, **kwargs) if response.status_code == status.HTTP_201_CREATED: response.data = ViewCreatedLinkSerializer(self.object).data return response @api_view(("DELETE",)) def delete_media(request): user = request.user hashes = request.QUERY_PARAMS.getlist("url_hash") Media.objects.filter(user=user, url_hash__in=hashes).delete() return Response(status=204) ================================================ FILE: nimbus/apps/media/__init__.py ================================================ ================================================ FILE: nimbus/apps/media/admin.py ================================================ from django.contrib import admin from .models import Media class MediaAdmin(admin.ModelAdmin): fields = ("url_hash", "name", "target_url", "target_file", "user", "syntax_highlighted") list_display = ("id", "url_hash", "name", "user", "target_url", "target_file", "upload_date", "view_count", "media_type") list_filter = ("media_type",) search_fields = ("id", "url_hash", "name", "user__username", "target_url", "target_file", "upload_date", "view_count", "media_type") admin.site.register(Media, MediaAdmin) ================================================ FILE: nimbus/apps/media/models.py ================================================ import mimetypes import uuid import string import random from pygments import highlight from pygments.lexers import guess_lexer_for_filename from pygments.formatters import HtmlFormatter from django.db import models from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver from django.contrib.auth import get_user_model from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.core.validators import URLValidator from django.utils.encoding import filepath_to_uri from rest_framework.authtoken.models import Token class Media(models.Model): MEDIA_TYPES = ( ("IMG", "Image"), ("URL", "Link"), ("TXT", "Text"), ("ARC", "Archive"), ("AUD", "Audio"), ("VID", "Video"), ("ETC", "Other") ) MEDIA_TYPES_PLURAL = ( ("IMG", "Images"), ("URL", "Links"), ("TXT", "Text"), ("ARC", "Archives"), ("AUD", "Audio"), ("VID", "Video"), ("ETC", "Other") ) _random_filename = lambda i, f: str(uuid.uuid4()).replace("-", "") + "/" + f url_hash = models.CharField(max_length=100, blank=True) name = models.CharField(max_length=500) target_url = models.URLField(max_length=2048, blank=True) target_file = models.FileField(upload_to=_random_filename, blank=True) view_count = models.IntegerField(default=0) upload_date = models.DateTimeField(auto_now_add=True) user = models.ForeignKey(User) media_type = models.CharField(max_length=3, choices=MEDIA_TYPES, blank=True) syntax_highlighted = models.TextField(blank=True) # all prefixed by "application/" ARCHIVE_MIME_TYPES = ["x-cpio", "x-shar", "x-tar", "x-bzip2", "x-gzip", "x-lzip", "x-lzma", "x-lzop", "x-xz", "x-compress", "x-compress", "x-7z-compressed", "x-ace-compressed", "x-astrotite-afa", "x-alz-compressed", "vnd.android.package-archive", "x-arj", "x-b1", "vnd.ms-cab-compressed", "x-cfs-compressed", "x-dar", "x-dgc-compressed", "x-apple-diskimage", "x-gca-compressed", "x-lzh", "x-lzx", "x-rar-compressed", "x-stuffit", "x-stuffitx", "x-gtar", "zip", "x-zoo", "x-par2"] @property def raw_url(self): return self.target_file.storage.url(self.target_file.name) @property def raw_ssl_url(self): storage = self.target_file.storage name = storage._normalize_name(storage._clean_name(self.target_file.name)) return "https://s3.amazonaws.com/{}/{}".format(storage.bucket_name, filepath_to_uri(name)) @staticmethod def guess_media_type(resource_name): validator = URLValidator() try: validator(resource_name) except ValidationError: pass else: return "URL" top, sub = (mimetypes.guess_type(resource_name, strict=False)[0] or "/").split("/") if top == "image": return "IMG" if top == "text": return "TXT" if top == "audio": return "AUD" if top == "video": return "VID" if (top == "application") and (sub in Media.ARCHIVE_MIME_TYPES): return "ARC" return "ETC" def fill_syntax_highlighted(self, text): lexer = guess_lexer_for_filename(self.name, text) html = highlight(text, lexer, HtmlFormatter()) self.syntax_highlighted = html self.save() def __unicode__(self): return self.name class Meta: verbose_name_plural = "Media" @receiver(post_save, sender=Media) def fill_auto_fields(sender, **kwargs): instance = kwargs.get("instance") fields = {} if not instance.media_type: fields["media_type"] = Media.guess_media_type(instance.name) if not instance.url_hash: url_hash = None alphabet = string.uppercase + string.lowercase + string.digits length = 5 tries = 0 while url_hash is None or Media.objects.filter(url_hash=url_hash).exists(): url_hash = "".join(random.SystemRandom().choice(alphabet) for _ in range(length)) tries += 1 if tries % 3 == 0: length += 1 fields["url_hash"] = url_hash if fields: for field, value in fields.items(): setattr(instance, field, value) instance.save() @receiver(pre_delete, sender=Media) def delete_file_from_storage(sender, **kwargs): instance = kwargs.get("instance") if instance.target_file: instance.target_file.delete() # No real better place to put this... @receiver(post_save, sender=get_user_model()) def create_auth_token(sender, instance=None, created=False, **kwargs): if created: Token.objects.create(user=instance) ================================================ FILE: nimbus/apps/media/serializers.py ================================================ from rest_framework import serializers from subdomains.utils import reverse from nimbus.apps.media.models import Media def _get_target_file_url(obj): if obj.target_file: return obj.raw_url return "" def _get_share_url(obj): return reverse("share", subdomain=None, kwargs={"url_hash": obj.url_hash}) class MediaSerializer(serializers.ModelSerializer): target_file_url = serializers.SerializerMethodField("get_target_file_url") share_url = serializers.SerializerMethodField("get_share_url") def get_target_file_url(self, obj): return _get_target_file_url(obj) def get_share_url(self, obj): return _get_share_url(obj) class Meta: model = Media fields = ("url_hash", "share_url", "name", "target_url", "target_file", "target_file_url", "view_count", "upload_date", "media_type") class ViewCreatedFileSerializer(serializers.ModelSerializer): target_file_url = serializers.SerializerMethodField("get_target_file_url") share_url = serializers.SerializerMethodField("get_share_url") def get_target_file_url(self, obj): return _get_target_file_url(obj) def get_share_url(self, obj): return _get_share_url(obj) class Meta: model = Media fields = ("url_hash", "share_url", "name", "target_file", "target_file_url", "upload_date", "media_type") class CreateLinkSerializer(serializers.ModelSerializer): target_url = serializers.URLField(max_length=2048) class Meta: model = Media fields = ("target_url",) class ViewCreatedLinkSerializer(serializers.ModelSerializer): share_url = serializers.SerializerMethodField("get_share_url") def get_share_url(self, obj): return _get_share_url(obj) class Meta: model = Media fields = ("url_hash", "share_url", "target_url", "upload_date") ================================================ FILE: nimbus/apps/media/urls.py ================================================ from django.conf.urls import url from django.views.generic.base import RedirectView from subdomains.utils import reverse from nimbus.apps import debug_urls from nimbus.apps.media import views urlpatterns = debug_urls() urlpatterns += [ url(r"^$", RedirectView.as_view(url=reverse("index", subdomain="account"))), url(r"^(?P[a-zA-Z0-9]+)$", views.share_view, name="share"), ] ================================================ FILE: nimbus/apps/media/views.py ================================================ from pygments.formatters import HtmlFormatter from django.shortcuts import render, get_object_or_404, redirect from .models import Media def share_view(request, url_hash): media_item = get_object_or_404(Media, url_hash=url_hash) media_item.view_count += 1 media_item.save() if media_item.media_type == "URL": return redirect(media_item.target_url) templates = { "IMG": "nimbus/media/share_img_preview.html", "TXT": "nimbus/media/share_txt_preview.html" } template = templates.setdefault(media_item.media_type, "nimbus/media/share_download.html") context = { "media_item": media_item } if media_item.media_type == "TXT": context["syntax_highlighting_style_defs"] = HtmlFormatter().get_style_defs('.highlight') return render(request, template, context) ================================================ FILE: nimbus/settings/__init__.py ================================================ import os import sys if os.getenv("PRODUCTION", "FALSE") == "TRUE": from production import * else: from local import * ================================================ FILE: nimbus/settings/base.py ================================================ import os import re from fnmatch import fnmatch from boto.s3.connection import VHostCallingFormat from secret import * PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) LOGIN_URL = "/login" LOGIN_REDIRECT_URL = "/" ALLOWED_HOSTS = ["." + HOSTNAME] APPEND_SLASH = False ADMINS = ( # ('Your Name', 'your_email@example.com'), ) MANAGERS = ADMINS # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. # In a Windows environment this must be set to your system time zone. TIME_ZONE = "America/New_York" # Language code for this installation. All choices can be found here: # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = "en-us" SITE_ID = 1 # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. USE_I18N = False # If you set this to False, Django will not format dates, numbers and # calendars according to the current locale. USE_L10N = True # If you set this to False, Django will not use timezone-aware datetimes. USE_TZ = True # Absolute path to the directory static files should be collected to. # Don't put anything in this directory yourself; store your static files # in apps' "static/" subdirectories and in STATICFILES_DIRS. # Example: "/home/media/media.lawrence.com/static/" STATIC_ROOT = os.path.join(PROJECT_ROOT, "collected_static") # URL prefix for static files. # Example: "http://media.lawrence.com/static/" STATIC_URL = "/static/" # Additional locations of static files STATICFILES_DIRS = ( # Put strings here, like "/home/html/static" or "C:/www/django/static". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. os.path.join(PROJECT_ROOT, "static"), ) # List of finder classes that know how to find static files in # various locations. STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 'django.contrib.staticfiles.finders.DefaultStorageFinder', ) # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', # 'django.template.loaders.eggs.Loader', ) TEMPLATE_CONTEXT_PROCESSORS = ( "django.core.context_processors.request", "django.contrib.auth.context_processors.auth", ) MIDDLEWARE_CLASSES = ( "corsheaders.middleware.CorsMiddleware", "subdomains.middleware.SubdomainURLRoutingMiddleware", 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'corsheaders.middleware.CorsPostCsrfMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', # Uncomment the next line for simple clickjacking protection: # 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) ROOT_URLCONF = "nimbus.apps.media.urls" SUBDOMAIN_URLCONFS = { None: "nimbus.apps.media.urls", "account": "nimbus.apps.accounts.urls", "api": "nimbus.apps.api.urls" } # Python dotted path to the WSGI application used by Django's runserver. WSGI_APPLICATION = 'nimbus.wsgi.application' TEMPLATE_DIRS = ( # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. os.path.join(PROJECT_ROOT, "templates"), ) REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( "rest_framework.authentication.TokenAuthentication", #'rest_framework.authentication.SessionAuthentication', "nimbus.utils.SessionAuthentication", ), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ), } INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.admin', "nimbus.apps.accounts", "nimbus.apps.api", "nimbus.apps.media", "subdomains", "rest_framework", "rest_framework.authtoken", "widget_tweaks", "corsheaders", "storages" ) SESSION_COOKIE_DOMAIN = "." + HOSTNAME CSRF_COOKIE_DOMAIN = SESSION_COOKIE_DOMAIN CORS_ORIGIN_REGEX_WHITELIST = (r"^(https?://)?(\w+\.)?{}$".format(re.escape(HOSTNAME)),) CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_HEADERS = ( 'x-requested-with', 'content-type', 'accept', 'origin', 'authorization', 'x-csrftoken', 'cache-control' ) CORS_REPLACE_HTTPS_REFERER = True DEFAULT_FILE_STORAGE = "storages.backends.s3boto.S3BotoStorage" AWS_STORAGE_BUCKET_NAME = "files." + HOSTNAME AWS_S3_CALLING_FORMAT = VHostCallingFormat() AWS_S3_SECURE_URLS = False AWS_S3_CUSTOM_DOMAIN = "files." + HOSTNAME class glob_list(list): """A list of glob-style strings.""" def __contains__(self, key): print(key) """Check if a string matches a glob in the list.""" for elt in self: if fnmatch(key, elt): return True return False INTERNAL_IPS = glob_list([ "127.0.0.1" ]) # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to # the site admins on every HTTP 500 error when DEBUG=False. # See http://docs.djangoproject.com/en/dev/topics/logging for # more details on how to customize your logging configuration. LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'filters': { 'require_debug_false': { '()': 'django.utils.log.RequireDebugFalse' } }, 'handlers': { 'mail_admins': { 'level': 'ERROR', 'filters': ['require_debug_false'], 'class': 'django.utils.log.AdminEmailHandler' } }, 'loggers': { 'django.request': { 'handlers': ['mail_admins'], 'level': 'ERROR', 'propagate': True, }, } } ================================================ FILE: nimbus/settings/local.py ================================================ import os import logging from .base import * logger = logging.getLogger(__name__) DEBUG = True TEMPLATE_DEBUG = DEBUG DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(os.path.dirname(PROJECT_ROOT), "testing_database.db"), } } INSTALLED_APPS += ("django_extensions",) SHOW_DEBUG_TOOLBAR = os.getenv("SHOW_DEBUG_TOOLBAR", "YES") == "YES" if SHOW_DEBUG_TOOLBAR: DEBUG_TOOLBAR_PATCH_SETTINGS = False DEBUG_TOOLBAR_CONFIG = { } DEBUG_TOOLBAR_PANELS = [ "debug_toolbar.panels.versions.VersionsPanel", "debug_toolbar.panels.timer.TimerPanel", # "debug_toolbar.panels.profiling.ProfilingPanel", "debug_toolbar.panels.settings.SettingsPanel", "debug_toolbar.panels.headers.HeadersPanel", "debug_toolbar.panels.request.RequestPanel", "debug_toolbar.panels.sql.SQLPanel", "debug_toolbar.panels.staticfiles.StaticFilesPanel", "debug_toolbar.panels.templates.TemplatesPanel", "debug_toolbar.panels.signals.SignalsPanel", "debug_toolbar.panels.logging.LoggingPanel", "debug_toolbar.panels.redirects.RedirectsPanel", ] MIDDLEWARE_CLASSES = ( "debug_toolbar.middleware.DebugToolbarMiddleware", ) + MIDDLEWARE_CLASSES INSTALLED_APPS += ("debug_toolbar",) ================================================ FILE: nimbus/settings/production.py ================================================ import urlparse from .base import * DEBUG = os.getenv("DEBUG", "FALSE") == "TRUE" TEMPLATE_DEBUG = DEBUG SHOW_DEBUG_TOOLBAR = False urlparse.uses_netloc.append("mysql") url = urlparse.urlparse(DATABASE_URL) DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': url.path[1:], 'USER': url.username, 'PASSWORD': url.password, 'HOST': url.hostname } } SESSION_COOKIE_SECURE = False CSRF_COOKIE_SECURE = False DEFAULT_URL_SCHEME = "https" SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") try: RAVEN_CONFIG INSTALLED_APPS += ( 'raven.contrib.django.raven_compat', ) except NameError: pass ================================================ FILE: nimbus/settings/secret.sample.py ================================================ """ Rename this file to 'secret.py' once all settings are defined """ SECRET_KEY = "..." HOSTNAME = "example.com" DATABASE_URL = "mysql://:@/" AWS_ACCESS_KEY_ID = "12345" AWS_SECRET_ACCESS_KEY = "12345" ================================================ FILE: nimbus/static/css/base.css ================================================ #main-nav { border-radius: 0; } #main-nav .navbar-brand { padding: 7px 15px; } .cloud-logo { vertical-align: middle; font-size: 36px; } input { -webkit-box-shadow: none !important; box-shadow: none !important; } .navbar-text { margin-left: 15px; } input.error { border-color: #d9534f; } .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus, .navbar-default .navbar-nav>li>a:hover, .navbar-default .navbar-nav>li>a:focus { color: #66afe9; } .btn:focus { outline:none!important; } ================================================ FILE: nimbus/static/css/dashboard.css ================================================ body { margin-bottom: 50px; } #media-type-selector a { color: #777; } #media-type-selector li.active a { background-color: #428bca;; color: white; } h2 { font-size: 20px; margin-top: 0; margin-bottom: 20px; float: left; } .delete-selected { float: right; margin-bottom: 10px; display: none; } #main-content { margin-left: 0; margin-right: 0; } #media-list { margin-left: -15px; margin-right: -15px; color: rgb(139, 139, 139); } #media-list input[type="checkbox"] { margin-right: 10px; } #media-list tr input[type="checkbox"]:checked { background-color: green; } .view-count { font-weight: bold; } .dropzone{ text-align: center; border: 1px rgb(211, 211, 211) dashed; border-radius: 8px; background-color: rgb(252, 252, 252); margin-bottom: 1em; cursor: pointer; display: table; color: rgb(211, 211, 211); padding: 10px; width: 100%; } .dropzone.loading { -webkit-animation: loading-pulse 2s infinite; } @-webkit-keyframes loading-pulse { 50% {color: #428bca;} } .dropzone.error { -webkit-animation: error-pulse 0.5s infinite; } @-webkit-keyframes error-pulse { 50% {color: #d9534f;} } .upload-icon { font-size: 60px; display: table-row; } .dz-message { display: table-row; } @media (min-width: 992px) { .nav-stacked-responsive > li { float: none; } .nav-stacked-responsive > li + li { margin-top: 2px; margin-left: 0; } } @media (max-width: 992px) { .nav-stacked-responsive { text-align: center; } .nav-stacked-responsive>li { float: none; display: inline-block; } } @media (max-width: 768px) { .media-type-label { display: none; } } .empty-state { text-align: center; } .pager li { margin: 0 10px; } ================================================ FILE: nimbus/static/css/login.css ================================================ #login-heading { margin-top: 40px; text-align: center; } #cloud-logo { margin: 20px; width: 240px; } h1 { margin-top: -10px; } #login-form .form-control { margin-bottom: 6px; } ================================================ FILE: nimbus/static/css/share_preview.css ================================================ .wrapper { margin: 30px auto; padding: 10px; } #text-preview { max-width: 780px; } #media-name { position: absolute; width: 100%; text-align: center; top: 0; margin-left: -15px; font-weight: bold; pointer-events: none; } #download-original { line-height: 50px; font-size: 25px; } #img-preview { text-align: center; padding: 0; } #file-download { text-align: center; } #file-icon { font-size: 160px; color: #DDD; display: block; margin-top: 50px; margin-bottom: 20px; } #file-download .glyphicon-file { margin-left: 14px; } .navbar-header:after { clear: none; } ================================================ FILE: nimbus/static/js/dashboard.js ================================================ $(function() { var deleteCheckboxClicked = function() { if ($(".delete-checkbox").filter(":checked").length === 0) $(".delete-selected").hide() else $(".delete-selected").show() }; $(".delete-checkbox").click(deleteCheckboxClicked); var resetDropzone = function(el) { var $e = $(el); var $m = $e.children(".dz-message"); $e.removeClass("loading"); $e.removeClass("error"); $m.text($e.data("initial-message")); $m.css("line-height", $m.data("initial-line-height")); } Dropzone.options.uploadDropzone = { clickable: "#upload-dropzone, .upload-icon", uploadMultiple: false, maxFiles: 1, complete: function(file) { this.removeFile(file); }, success: function(file, response) { resetDropzone(this.element); displayedMediaType = $("#media-list").data("media-type-code"); var mediaItem = response; if (displayedMediaType == "ALL" || displayedMediaType == mediaItem.media_type) { var $table = $("#media-list>table"), $tbody = $("#media-list>table>tbody"); if ($table.hasClass("empty-state")) { $tbody.html(mediaItem.html); } else { $tbody.prepend(mediaItem.html); } $table.addClass("table-hover"); $table.removeClass("empty-state"); } $(".delete-checkbox").click(deleteCheckboxClicked); }, error: function(file, error) { console.error(error); $e = $(this.element); $e.removeClass("loading"); $e.addClass("error"); $e.children(".dz-message").text("Error Uploading File"); setTimeout(function(){ resetDropzone($e); }, 2000); }, addedfile: function(file) { var $e = $(this.element); var $m = $e.children(".dz-message"); if (!$e.data("initial-message")) { $e.data("initial-message", $m.text()); } $e.addClass("loading"); var messageHeight = $m.height(); var lineHeight = parseInt($m.css("line-height").replace("px","")); var lines = Math.round(messageHeight / lineHeight); if (!$m.data("initial-line-height")) { $m.data("initial-line-height", $m.css("line-height")); } $m.css("line-height", (lines * lineHeight) + "px"); $m.text("Uploading..."); }, sending: function(file, xhr, formData) { xhr.withCredentials = true; // send cookies even though this is a cross-site request } }; $(".delete-selected").click(function() { $(this).css("width", $(this).outerWidth()); $(this).text("Deleting..."); var $rows = []; var hashes = $.map($(".delete-checkbox").filter(":checked"), function(e){ var $p = $(e).parent().parent(); var hash = $p.data("url_hash"); $rows.push($p); return hash; }); var params = hashes.map(function(hash){return "url_hash=" + hash}).join("&"); var button = this; $.ajax({ url: $(this).data("delete-api-endpoint") + "?" + params, type: "DELETE", xhrFields: { withCredentials: true }, beforeSend: function(xhr, settings) { xhr.setRequestHeader("X-CSRFToken", $.cookie("csrftoken")); }, success: function() { $(button).text("Delete Selected"); $.map($rows, function($r) { $r.remove(); }); $(button).hide(); var $table = $("#media-list>table"), $tbody = $("#media-list>table>tbody"); if ($("#media-list tr").length == 0) { location.reload(); // $tbody.append("Nothing here yet"); // $table.removeClass("table-hover"); // $table.addClass("empty-state"); } }, error: function(xhr, status, error) { console.log(xhr); $(button).text("Error!"); setTimeout(function() { $(button).text("Delete Selected"); }, 1000); } }); }); }); ================================================ FILE: nimbus/static/js/jquery-photo-resize.js ================================================ (function ($) { $.fn.photoResize = function (options) { var element = $(this), defaults = { padding: 10 }; $(element).load(function () { updatePhotoSize(); $(window).bind("resize", function () { updatePhotoSize(); }); }); options = $.extend(defaults, options); function updatePhotoSize() { var verticalShrink = $(window).height() - 52 - 2 * options.padding, horizontalShrink = ($(window).width() - 2 * options.padding) * $(element).height() / $(element).width(), noShrink = element[0].naturalHeight; $(element).attr("height", Math.min(verticalShrink, horizontalShrink, noShrink)); } }; }(jQuery)); ================================================ FILE: nimbus/templates/admin/base_site.html ================================================ {% extends "admin/base.html" %} {% block title %} Nimbus Admin - {{ title|escape }} {% endblock %} {% block branding %}

Nimbus Admin

{% endblock %} {% block nav-global %}{% endblock %} ================================================ FILE: nimbus/templates/nimbus/accounts/dashboard.html ================================================ {% extends "nimbus/page_base.html" %} {% load staticfiles %} {% load subdomainurls %} {% block title %} {{ block.super }} - Dashboard {% endblock %} {% block css %} {{ block.super }} {% endblock %} {% block js %} {{ block.super }} {% endblock %} {% block body %}
{% csrf_token %} Drop file or click here to upload

{% if media_type = "all" %}All {% endif %}Shared Items{% if media_type != "all" %} - {{ media_type.title }}{% endif %}

{% if media %} {% for media_item in media %} {% include "nimbus/accounts/media_table_row.html" %} {% endfor %}
{% else %}
Nothing here yet
{% endif %}
{% if media.paginator.num_pages > 1 %}
  • ← Previous
  • Page {{ media.number }} of {{ media.paginator.num_pages }}
  • Next →
{% endif %}
{{ block.super }} {% endblock %} ================================================ FILE: nimbus/templates/nimbus/accounts/login.html ================================================ {% extends "nimbus/page_base.html" %} {% load staticfiles %} {% load widget_tweaks %} {% block title %} {{ block.super }} - Login {% endblock %} {% block css %} {{ block.super }} {% endblock %} {% block js %} {{ block.super }} {% endblock %} {% block body %} Fork me on GitHub
{{ block.super }} {% endblock %} ================================================ FILE: nimbus/templates/nimbus/accounts/media_table_row.html ================================================ {% load subdomainurls %} {{ media_item.name|truncatechars:60 }} {{ media_item.view_count }} ================================================ FILE: nimbus/templates/nimbus/media/share_base.html ================================================ {% extends "nimbus/page_base.html" %} {% load staticfiles %} {% block title %}{{ media_item.name }}{% endblock %} {% block css %} {{ block.super }} {% endblock %} {% block body %} {% block media_preview %} {{ media_item.name }} {% endblock %} {% endblock %} ================================================ FILE: nimbus/templates/nimbus/media/share_download.html ================================================ {% extends "nimbus/media/share_base.html" %} {% block media_preview %} {% endblock %} ================================================ FILE: nimbus/templates/nimbus/media/share_img_preview.html ================================================ {% extends "nimbus/media/share_base.html" %} {% load staticfiles %} {% block js %} {{ block.super }} {% endblock %} {% block media_preview %}
{{ media_item.name }}
{% endblock %} ================================================ FILE: nimbus/templates/nimbus/media/share_txt_preview.html ================================================ {% extends "nimbus/media/share_base.html" %} {% block css %} {{ block.super }} {% endblock %} {% block media_preview %}
{{ media_item.syntax_highlighted|safe }}
{% endblock %} ================================================ FILE: nimbus/templates/nimbus/page_base.html ================================================ {% load staticfiles %} {% block title %}Nimbus{% endblock %} {% block css %} {% endblock %} {% block js %} {% endblock %} {% block head %} {% endblock %} {% block body %} {% endblock %} ================================================ FILE: nimbus/templates/rest_framework/api.html ================================================ {% extends "rest_framework/base.html" %} {% block title %} Nimbus API - {{request.path}} {% endblock %} {% block branding %} Nimbus API {% endblock %} ================================================ FILE: nimbus/templates/rest_framework/login.html ================================================ {% extends "rest_framework/login_base.html" %} {% block branding %}

Nimbus API

{% endblock %} {% block style %} {# hacky way to add to head tag #} Nimbus API - Login {{ block.super }} {% endblock %} ================================================ FILE: nimbus/utils.py ================================================ from corsheaders import defaults as settings from corsheaders.middleware import ( CorsPostCsrfMiddleware, CorsMiddleware) from rest_framework.authentication import ( SessionAuthentication as RFSessionAuthentication) class PatchDepatchRefererCsrf(CorsMiddleware, CorsPostCsrfMiddleware): """ Helper class to provide access to _https_referer_replace and _https_referer_replace_reverse. """ def patch(self, request): if self.is_enabled(request) and settings.CORS_REPLACE_HTTPS_REFERER: self._https_referer_replace(request) def depatch(self, request): self._https_referer_replace_reverse(request) class SessionAuthentication(RFSessionAuthentication): """ SessionAuthentication that patchess the HTTP_REFERER before checking CSRF and depatch it after checking it. corsheaders supplies middleware but DRF doesn't use the middleware when checking CSRF for itself. This creates a problem when using https. """ def enforce_csrf(self, request): patch_depatch = PatchDepatchRefererCsrf() patch_depatch.patch(request) super(SessionAuthentication, self).enforce_csrf(request) patch_depatch.depatch(request) ================================================ FILE: nimbus/wsgi.py ================================================ """ WSGI config for nimbus project. This module contains the WSGI application used by Django's development server and any production WSGI deployments. It should expose a module-level variable named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover this application via the ``WSGI_APPLICATION`` setting. Usually you will have the standard Django WSGI application here, but it also might make sense to replace the whole Django WSGI application with a custom one that later delegates to the Django one. For example, you could introduce WSGI middleware here, or combine a Django application with an application of another framework. """ import os try: import newrelic.agent newrelic.agent.initialize("newrelic.ini") except ImportError: pass os.environ.setdefault("DJANGO_SETTINGS_MODULE", "nimbus.settings") # This application object is used by any WSGI server configured to use this # file. This includes Django's development server, if the WSGI_APPLICATION # setting points here. from django.core.wsgi import get_wsgi_application application = get_wsgi_application() # Apply WSGI middleware here. # from helloworld.wsgi import HelloWorldApplication # application = HelloWorldApplication(application) ================================================ FILE: requirements/base.txt ================================================ Django==1.6.5 Markdown==2.4.1 Pygments==1.6 boto==2.31.1 -e git+https://github.com/ottoyiu/django-cors-headers.git#egg=Package django-storages==1.1.8 django-subdomains==2.0.4 django-widget-tweaks==1.3 djangorestframework==2.3.14 ================================================ FILE: requirements/local.txt ================================================ -r base.txt django-debug-toolbar==1.2.1 django-extensions==1.3.8 ================================================ FILE: requirements/production.txt ================================================ -r base.txt gunicorn==19.0.0 MySQL-python==1.2.5 newrelic==2.40.0.34