├── .gitignore ├── AUTHORS ├── Augie.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── Augie ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── augie-menubar.imageset │ │ ├── Contents.json │ │ └── augie-menubar@2x.png ├── Base.lproj │ └── Main.storyboard ├── Info.plist └── main.m ├── CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── PATENTS ├── README.md ├── cmd └── upspin-ui │ ├── copy.go │ ├── doc.go │ ├── gcp.go │ ├── log.go │ ├── main.go │ ├── put.go │ ├── rm.go │ ├── startup.go │ └── static │ ├── .gitignore │ ├── augie.png │ ├── favicon-32x32.png │ ├── gen.go │ ├── index.html │ ├── makestatic.go │ ├── script.js │ └── third_party │ ├── bootstrap │ ├── LICENSE │ ├── css │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap-theme.min.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ └── bootstrap.min.js │ ├── jquery │ ├── LICENSE │ └── jquery.min.js │ └── ladda │ ├── LICENSE │ ├── ladda-themeless.min.css │ ├── ladda.min.js │ └── spin.min.js ├── codereview.cfg ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DS_Store? 3 | .Trashes 4 | .Spotlight-V100 5 | *.swp 6 | 7 | ## Xcode build files 8 | DerivedData/ 9 | build/ 10 | 11 | ## Xcode private settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.xccheckout 25 | *.moved-aside 26 | *.xcuserstate 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This source code refers to The Upspin Authors for copyright purposes. 2 | # The master list of authors is in the main Upspin distribution, 3 | # visible at https://upspin.googlesource.com/upspin/+/master/AUTHORS. 4 | -------------------------------------------------------------------------------- /Augie.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7E3D36D61E8AFFD8003C011C /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E3D36D51E8AFFD8003C011C /* AppDelegate.m */; }; 11 | 7E3D36D91E8AFFD8003C011C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E3D36D81E8AFFD8003C011C /* main.m */; }; 12 | 7E3D36DE1E8AFFD8003C011C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7E3D36DD1E8AFFD8003C011C /* Assets.xcassets */; }; 13 | 7E3D36E11E8AFFD8003C011C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7E3D36DF1E8AFFD8003C011C /* Main.storyboard */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 7E3D36D11E8AFFD8003C011C /* Augie.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Augie.app; sourceTree = BUILT_PRODUCTS_DIR; }; 18 | 7E3D36D41E8AFFD8003C011C /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 19 | 7E3D36D51E8AFFD8003C011C /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 20 | 7E3D36D81E8AFFD8003C011C /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 21 | 7E3D36DD1E8AFFD8003C011C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 22 | 7E3D36E01E8AFFD8003C011C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 23 | 7E3D36E21E8AFFD8003C011C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24 | /* End PBXFileReference section */ 25 | 26 | /* Begin PBXFrameworksBuildPhase section */ 27 | 7E3D36CE1E8AFFD8003C011C /* Frameworks */ = { 28 | isa = PBXFrameworksBuildPhase; 29 | buildActionMask = 2147483647; 30 | files = ( 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | 7E3D36C81E8AFFD8003C011C = { 38 | isa = PBXGroup; 39 | children = ( 40 | 7E3D36D31E8AFFD8003C011C /* Augie */, 41 | 7E3D36D21E8AFFD8003C011C /* Products */, 42 | ); 43 | sourceTree = ""; 44 | }; 45 | 7E3D36D21E8AFFD8003C011C /* Products */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | 7E3D36D11E8AFFD8003C011C /* Augie.app */, 49 | ); 50 | name = Products; 51 | sourceTree = ""; 52 | }; 53 | 7E3D36D31E8AFFD8003C011C /* Augie */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | 7E3D36D41E8AFFD8003C011C /* AppDelegate.h */, 57 | 7E3D36D51E8AFFD8003C011C /* AppDelegate.m */, 58 | 7E3D36DD1E8AFFD8003C011C /* Assets.xcassets */, 59 | 7E3D36DF1E8AFFD8003C011C /* Main.storyboard */, 60 | 7E3D36E21E8AFFD8003C011C /* Info.plist */, 61 | 7E3D36D71E8AFFD8003C011C /* Supporting Files */, 62 | ); 63 | path = Augie; 64 | sourceTree = ""; 65 | }; 66 | 7E3D36D71E8AFFD8003C011C /* Supporting Files */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 7E3D36D81E8AFFD8003C011C /* main.m */, 70 | ); 71 | name = "Supporting Files"; 72 | sourceTree = ""; 73 | }; 74 | /* End PBXGroup section */ 75 | 76 | /* Begin PBXNativeTarget section */ 77 | 7E3D36D01E8AFFD8003C011C /* Augie */ = { 78 | isa = PBXNativeTarget; 79 | buildConfigurationList = 7E3D36E51E8AFFD8003C011C /* Build configuration list for PBXNativeTarget "Augie" */; 80 | buildPhases = ( 81 | 7E3D36CD1E8AFFD8003C011C /* Sources */, 82 | 7E3D36CE1E8AFFD8003C011C /* Frameworks */, 83 | 7E3D36CF1E8AFFD8003C011C /* Resources */, 84 | ); 85 | buildRules = ( 86 | ); 87 | dependencies = ( 88 | ); 89 | name = Augie; 90 | productName = Augie; 91 | productReference = 7E3D36D11E8AFFD8003C011C /* Augie.app */; 92 | productType = "com.apple.product-type.application"; 93 | }; 94 | /* End PBXNativeTarget section */ 95 | 96 | /* Begin PBXProject section */ 97 | 7E3D36C91E8AFFD8003C011C /* Project object */ = { 98 | isa = PBXProject; 99 | attributes = { 100 | LastUpgradeCheck = 0830; 101 | ORGANIZATIONNAME = "Upspin Project"; 102 | TargetAttributes = { 103 | 7E3D36D01E8AFFD8003C011C = { 104 | CreatedOnToolsVersion = 8.3; 105 | ProvisioningStyle = Automatic; 106 | }; 107 | }; 108 | }; 109 | buildConfigurationList = 7E3D36CC1E8AFFD8003C011C /* Build configuration list for PBXProject "Augie" */; 110 | compatibilityVersion = "Xcode 3.2"; 111 | developmentRegion = English; 112 | hasScannedForEncodings = 0; 113 | knownRegions = ( 114 | en, 115 | Base, 116 | ); 117 | mainGroup = 7E3D36C81E8AFFD8003C011C; 118 | productRefGroup = 7E3D36D21E8AFFD8003C011C /* Products */; 119 | projectDirPath = ""; 120 | projectRoot = ""; 121 | targets = ( 122 | 7E3D36D01E8AFFD8003C011C /* Augie */, 123 | ); 124 | }; 125 | /* End PBXProject section */ 126 | 127 | /* Begin PBXResourcesBuildPhase section */ 128 | 7E3D36CF1E8AFFD8003C011C /* Resources */ = { 129 | isa = PBXResourcesBuildPhase; 130 | buildActionMask = 2147483647; 131 | files = ( 132 | 7E3D36DE1E8AFFD8003C011C /* Assets.xcassets in Resources */, 133 | 7E3D36E11E8AFFD8003C011C /* Main.storyboard in Resources */, 134 | ); 135 | runOnlyForDeploymentPostprocessing = 0; 136 | }; 137 | /* End PBXResourcesBuildPhase section */ 138 | 139 | /* Begin PBXSourcesBuildPhase section */ 140 | 7E3D36CD1E8AFFD8003C011C /* Sources */ = { 141 | isa = PBXSourcesBuildPhase; 142 | buildActionMask = 2147483647; 143 | files = ( 144 | 7E3D36D91E8AFFD8003C011C /* main.m in Sources */, 145 | 7E3D36D61E8AFFD8003C011C /* AppDelegate.m in Sources */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | /* End PBXSourcesBuildPhase section */ 150 | 151 | /* Begin PBXVariantGroup section */ 152 | 7E3D36DF1E8AFFD8003C011C /* Main.storyboard */ = { 153 | isa = PBXVariantGroup; 154 | children = ( 155 | 7E3D36E01E8AFFD8003C011C /* Base */, 156 | ); 157 | name = Main.storyboard; 158 | sourceTree = ""; 159 | }; 160 | /* End PBXVariantGroup section */ 161 | 162 | /* Begin XCBuildConfiguration section */ 163 | 7E3D36E31E8AFFD8003C011C /* Debug */ = { 164 | isa = XCBuildConfiguration; 165 | buildSettings = { 166 | ALWAYS_SEARCH_USER_PATHS = NO; 167 | CLANG_ANALYZER_NONNULL = YES; 168 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 169 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 170 | CLANG_CXX_LIBRARY = "libc++"; 171 | CLANG_ENABLE_MODULES = YES; 172 | CLANG_ENABLE_OBJC_ARC = YES; 173 | CLANG_WARN_BOOL_CONVERSION = YES; 174 | CLANG_WARN_CONSTANT_CONVERSION = YES; 175 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 176 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 177 | CLANG_WARN_EMPTY_BODY = YES; 178 | CLANG_WARN_ENUM_CONVERSION = YES; 179 | CLANG_WARN_INFINITE_RECURSION = YES; 180 | CLANG_WARN_INT_CONVERSION = YES; 181 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 182 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 183 | CLANG_WARN_UNREACHABLE_CODE = YES; 184 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 185 | CODE_SIGN_IDENTITY = "-"; 186 | COPY_PHASE_STRIP = NO; 187 | DEBUG_INFORMATION_FORMAT = dwarf; 188 | ENABLE_STRICT_OBJC_MSGSEND = YES; 189 | ENABLE_TESTABILITY = YES; 190 | GCC_C_LANGUAGE_STANDARD = gnu99; 191 | GCC_DYNAMIC_NO_PIC = NO; 192 | GCC_NO_COMMON_BLOCKS = YES; 193 | GCC_OPTIMIZATION_LEVEL = 0; 194 | GCC_PREPROCESSOR_DEFINITIONS = ( 195 | "DEBUG=1", 196 | "$(inherited)", 197 | ); 198 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 199 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 200 | GCC_WARN_UNDECLARED_SELECTOR = YES; 201 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 202 | GCC_WARN_UNUSED_FUNCTION = YES; 203 | GCC_WARN_UNUSED_VARIABLE = YES; 204 | MACOSX_DEPLOYMENT_TARGET = 10.12; 205 | MTL_ENABLE_DEBUG_INFO = YES; 206 | ONLY_ACTIVE_ARCH = YES; 207 | SDKROOT = macosx; 208 | }; 209 | name = Debug; 210 | }; 211 | 7E3D36E41E8AFFD8003C011C /* Release */ = { 212 | isa = XCBuildConfiguration; 213 | buildSettings = { 214 | ALWAYS_SEARCH_USER_PATHS = NO; 215 | CLANG_ANALYZER_NONNULL = YES; 216 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 217 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 218 | CLANG_CXX_LIBRARY = "libc++"; 219 | CLANG_ENABLE_MODULES = YES; 220 | CLANG_ENABLE_OBJC_ARC = YES; 221 | CLANG_WARN_BOOL_CONVERSION = YES; 222 | CLANG_WARN_CONSTANT_CONVERSION = YES; 223 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 224 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 225 | CLANG_WARN_EMPTY_BODY = YES; 226 | CLANG_WARN_ENUM_CONVERSION = YES; 227 | CLANG_WARN_INFINITE_RECURSION = YES; 228 | CLANG_WARN_INT_CONVERSION = YES; 229 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 230 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 231 | CLANG_WARN_UNREACHABLE_CODE = YES; 232 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 233 | CODE_SIGN_IDENTITY = "-"; 234 | COPY_PHASE_STRIP = NO; 235 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 236 | ENABLE_NS_ASSERTIONS = NO; 237 | ENABLE_STRICT_OBJC_MSGSEND = YES; 238 | GCC_C_LANGUAGE_STANDARD = gnu99; 239 | GCC_NO_COMMON_BLOCKS = YES; 240 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 241 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 242 | GCC_WARN_UNDECLARED_SELECTOR = YES; 243 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 244 | GCC_WARN_UNUSED_FUNCTION = YES; 245 | GCC_WARN_UNUSED_VARIABLE = YES; 246 | MACOSX_DEPLOYMENT_TARGET = 10.12; 247 | MTL_ENABLE_DEBUG_INFO = NO; 248 | SDKROOT = macosx; 249 | }; 250 | name = Release; 251 | }; 252 | 7E3D36E61E8AFFD8003C011C /* Debug */ = { 253 | isa = XCBuildConfiguration; 254 | buildSettings = { 255 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 256 | COMBINE_HIDPI_IMAGES = YES; 257 | INFOPLIST_FILE = Augie/Info.plist; 258 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 259 | PRODUCT_BUNDLE_IDENTIFIER = com.upspinproject.Augie; 260 | PRODUCT_NAME = "$(TARGET_NAME)"; 261 | }; 262 | name = Debug; 263 | }; 264 | 7E3D36E71E8AFFD8003C011C /* Release */ = { 265 | isa = XCBuildConfiguration; 266 | buildSettings = { 267 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 268 | COMBINE_HIDPI_IMAGES = YES; 269 | INFOPLIST_FILE = Augie/Info.plist; 270 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 271 | PRODUCT_BUNDLE_IDENTIFIER = com.upspinproject.Augie; 272 | PRODUCT_NAME = "$(TARGET_NAME)"; 273 | }; 274 | name = Release; 275 | }; 276 | /* End XCBuildConfiguration section */ 277 | 278 | /* Begin XCConfigurationList section */ 279 | 7E3D36CC1E8AFFD8003C011C /* Build configuration list for PBXProject "Augie" */ = { 280 | isa = XCConfigurationList; 281 | buildConfigurations = ( 282 | 7E3D36E31E8AFFD8003C011C /* Debug */, 283 | 7E3D36E41E8AFFD8003C011C /* Release */, 284 | ); 285 | defaultConfigurationIsVisible = 0; 286 | defaultConfigurationName = Release; 287 | }; 288 | 7E3D36E51E8AFFD8003C011C /* Build configuration list for PBXNativeTarget "Augie" */ = { 289 | isa = XCConfigurationList; 290 | buildConfigurations = ( 291 | 7E3D36E61E8AFFD8003C011C /* Debug */, 292 | 7E3D36E71E8AFFD8003C011C /* Release */, 293 | ); 294 | defaultConfigurationIsVisible = 0; 295 | }; 296 | /* End XCConfigurationList section */ 297 | }; 298 | rootObject = 7E3D36C91E8AFFD8003C011C /* Project object */; 299 | } 300 | -------------------------------------------------------------------------------- /Augie.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Augie/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Upspin Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | #import 6 | 7 | @interface AppDelegate : NSObject 8 | 9 | @end 10 | 11 | -------------------------------------------------------------------------------- /Augie/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Upspin Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | #import "AppDelegate.h" 6 | 7 | @interface AppDelegate() 8 | @property(nonatomic, strong) NSStatusItem* statusItem; 9 | @end 10 | 11 | @implementation AppDelegate 12 | 13 | - (void)applicationDidFinishLaunching:(NSNotification*)aNotification { 14 | self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; 15 | self.statusItem.image = [NSImage imageNamed:@"augie-menubar"]; 16 | 17 | NSMenu* menu = [NSMenu new]; 18 | [menu addItem:[[NSMenuItem alloc] initWithTitle:@"Quit" 19 | action:@selector(terminate:) 20 | keyEquivalent:@""]]; 21 | self.statusItem.menu = menu; 22 | } 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /Augie/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Augie/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Augie/Assets.xcassets/augie-menubar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "mac", 9 | "filename" : "augie-menubar@2x.png", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | }, 17 | "properties" : { 18 | "template-rendering-intent" : "template" 19 | } 20 | } -------------------------------------------------------------------------------- /Augie/Assets.xcassets/augie-menubar.imageset/augie-menubar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upspin/augie/798945a1f13394a5ab2d79bfdf1a68d87b797151/Augie/Assets.xcassets/augie-menubar.imageset/augie-menubar@2x.png -------------------------------------------------------------------------------- /Augie/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Augie/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSUIElement 6 | 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIconFile 12 | 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2017 Upspin Project. All rights reserved. 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /Augie/main.m: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Upspin Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | #import 6 | 7 | int main(int argc, const char* argv[]) { 8 | return NSApplicationMain(argc, argv); 9 | } 10 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [upspin@googlegroups.com](mailto:upspin@googlegroups.com). 59 | All complaints will be reviewed and investigated and will result in a response 60 | that is deemed necessary and appropriate to the circumstances. The project team 61 | is obligated to maintain confidentiality with regard to the reporter of an 62 | incident. Further details of specific enforcement policies may be posted 63 | separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at [http://contributor-covenant.org/version/1/4][version] 73 | 74 | [homepage]: http://contributor-covenant.org 75 | [version]: http://contributor-covenant.org/version/1/4/ 76 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Upspin 2 | 3 | Upspin is an open source project. 4 | 5 | It is the work of many contributors. We appreciate your help! 6 | 7 | 8 | ## Filing issues 9 | 10 | When filing an issue, make sure to answer these five questions: 11 | 12 | 1. What version of Upspin are you using? 13 | 2. What operating system and processor architecture are you using? 14 | 3. What did you do? 15 | 4. What did you expect to see? 16 | 5. What did you see instead? 17 | 18 | Sensitive security-related issues should be reported to the private 19 | [upspin-security@googlegroups.com](mailto:upspin-security@googlegroups.com) 20 | mailing list. 21 | 22 | 23 | ## Contributing code 24 | 25 | We do not use GitHub pull requests 26 | (we use [an instance](https://upspin-review.googlesource.com/) of the 27 | [Gerrit](https://www.gerritcodereview.com/) code review system instead). 28 | 29 | Our code review process is the same as that used by the Go project. 30 | Please read the Code Review section of 31 | [Go's Contribution Guidelines](https://golang.org/doc/contribute.html#Code_review) 32 | to learn how to send patches to Upspin. 33 | That document covers registering with 34 | [our Gerrit instance]((https://upspin-review.googlesource.com/)), 35 | configuring the `git-codereview` tool, 36 | signing a Contributor License Agreement, 37 | and the process of creating, reviewing, and submitting changes. 38 | 39 | Unless otherwise noted, the Upspin source files are distributed under 40 | the BSD-style license found in the LICENSE file. 41 | 42 | 43 | ## Code of Conduct 44 | 45 | Please note that this project is released with a [Contributor Code of Conduct](CONDUCT.md). 46 | By participating in this project you agree to abide by its terms. 47 | 48 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This source code was written by the Upspin contributors. 2 | # The master list of contributors is in the main Upspin distribution, 3 | # visible at https://upspin.googlesource.com/upspin/+/master/CONTRIBUTORS. 4 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "cloud.google.com/go" 6 | packages = ["compute/metadata"] 7 | revision = "eaddaf6dd7ee35fd3c2420c8d27478db176b0485" 8 | version = "v0.15.0" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/golang/protobuf" 13 | packages = ["proto"] 14 | revision = "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9" 15 | 16 | [[projects]] 17 | branch = "master" 18 | name = "golang.org/x/crypto" 19 | packages = ["hkdf"] 20 | revision = "2509b142fb2b797aa7587dad548f113b2c0f20ce" 21 | 22 | [[projects]] 23 | branch = "master" 24 | name = "golang.org/x/net" 25 | packages = [ 26 | "context", 27 | "context/ctxhttp", 28 | "xsrftoken" 29 | ] 30 | revision = "4b14673ba32bee7f5ac0f990a48f033919fd418b" 31 | 32 | [[projects]] 33 | branch = "master" 34 | name = "golang.org/x/oauth2" 35 | packages = [ 36 | ".", 37 | "google", 38 | "internal", 39 | "jws", 40 | "jwt" 41 | ] 42 | revision = "bb50c06baba3d0c76f9d125c0719093e315b5b44" 43 | 44 | [[projects]] 45 | branch = "master" 46 | name = "golang.org/x/text" 47 | packages = [ 48 | "cases", 49 | "internal", 50 | "internal/gen", 51 | "internal/tag", 52 | "internal/triegen", 53 | "internal/ucd", 54 | "language", 55 | "runes", 56 | "secure/bidirule", 57 | "secure/precis", 58 | "transform", 59 | "unicode/bidi", 60 | "unicode/cldr", 61 | "unicode/norm", 62 | "unicode/rangetable", 63 | "width" 64 | ] 65 | revision = "6eab0e8f74e86c598ec3b6fad4888e0c11482d48" 66 | 67 | [[projects]] 68 | branch = "master" 69 | name = "google.golang.org/api" 70 | packages = [ 71 | "compute/v1", 72 | "gensupport", 73 | "googleapi", 74 | "googleapi/internal/uritemplates", 75 | "iam/v1", 76 | "servicemanagement/v1", 77 | "storage/v1" 78 | ] 79 | revision = "52fedcc3d56e51c5f1a571d48092238ab2746c73" 80 | 81 | [[projects]] 82 | name = "google.golang.org/appengine" 83 | packages = [ 84 | ".", 85 | "internal", 86 | "internal/app_identity", 87 | "internal/base", 88 | "internal/datastore", 89 | "internal/log", 90 | "internal/modules", 91 | "internal/remote_api", 92 | "internal/urlfetch", 93 | "urlfetch" 94 | ] 95 | revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" 96 | version = "v1.0.0" 97 | 98 | [[projects]] 99 | branch = "v2" 100 | name = "gopkg.in/yaml.v2" 101 | packages = ["."] 102 | revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f" 103 | 104 | [[projects]] 105 | branch = "master" 106 | name = "upspin.io" 107 | packages = [ 108 | "access", 109 | "bind", 110 | "cache", 111 | "client", 112 | "client/clientutil", 113 | "client/file", 114 | "cloud/mail", 115 | "cmd/cacheserver/cacheutil", 116 | "config", 117 | "dir/inprocess", 118 | "dir/remote", 119 | "dir/unassigned", 120 | "errors", 121 | "factotum", 122 | "flags", 123 | "key/inprocess", 124 | "key/keygen", 125 | "key/proquint", 126 | "key/remote", 127 | "key/sha256key", 128 | "key/transports", 129 | "key/unassigned", 130 | "key/usercache", 131 | "log", 132 | "metric", 133 | "pack", 134 | "pack/ee", 135 | "pack/eeintegrity", 136 | "pack/internal", 137 | "pack/packutil", 138 | "pack/plain", 139 | "path", 140 | "rpc", 141 | "rpc/local", 142 | "serverutil", 143 | "serverutil/signup", 144 | "shutdown", 145 | "store/inprocess", 146 | "store/remote", 147 | "store/transports", 148 | "store/unassigned", 149 | "subcmd", 150 | "transports", 151 | "upspin", 152 | "upspin/proto", 153 | "user", 154 | "valid", 155 | "version" 156 | ] 157 | revision = "c137ad0d6be9a0c1fef0f54a356311a95bcd09ce" 158 | 159 | [solve-meta] 160 | analyzer-name = "dep" 161 | analyzer-version = 1 162 | inputs-digest = "bcf10ba2e1ab59d558a8ac4113a3f5cf7b0891581b73708abfa17b27a11ec696" 163 | solver-name = "gps-cdcl" 164 | solver-version = 1 165 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # golang.org/x/text/gen.go imports this package; we don't depend on it. 2 | ignored = ["golang.org/x/text/collate"] 3 | 4 | [[constraint]] 5 | branch = "master" 6 | name = "golang.org/x/net" 7 | 8 | [[constraint]] 9 | branch = "master" 10 | name = "golang.org/x/oauth2" 11 | 12 | [[constraint]] 13 | branch = "master" 14 | name = "google.golang.org/api" 15 | 16 | [[constraint]] 17 | branch = "master" 18 | name = "upspin.io" 19 | 20 | [prune] 21 | non-go = true 22 | unused-packages = true 23 | go-tests = true 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The Upspin Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional IP Rights Grant (Patents) 2 | 3 | "This implementation" means the copyrightable works distributed by 4 | Google as part of the Upspin project. 5 | 6 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 7 | no-charge, royalty-free, irrevocable (except as stated in this section) 8 | patent license to make, have made, use, offer to sell, sell, import, 9 | transfer and otherwise run, modify and propagate the contents of this 10 | implementation of Upspin, where such license applies only to those patent 11 | claims, both currently owned or controlled by Google and acquired in 12 | the future, licensable by Google that are necessarily infringed by this 13 | implementation of Upspin. This grant does not include claims that would be 14 | infringed only as a consequence of further modification of this 15 | implementation. If you or your agent or exclusive licensee institute or 16 | order or agree to the institution of patent litigation against any 17 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 18 | that this implementation of Upspin or any code incorporated within this 19 | implementation of Upspin constitutes direct or contributory patent 20 | infringement, or inducement of patent infringement, then any patent 21 | rights granted to you under this License for this implementation of Upspin 22 | shall terminate as of the date such litigation is filed. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Upspin `augie` repository 2 | 3 | This repository contains a MacOS menu bar widget for Upspin users. 4 | 5 | See the [master repository](https://github.com/upspin/upspin#readme) for more information. 6 | 7 | -------------------------------------------------------------------------------- /cmd/upspin-ui/copy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Upspin Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // TODO(adg): How to handle partially copied trees? 6 | // TODO(adg): Do permissions checking up front? 7 | 8 | package main 9 | 10 | import ( 11 | "upspin.io/errors" 12 | "upspin.io/path" 13 | "upspin.io/upspin" 14 | ) 15 | 16 | // copy recursively copies the specified source paths to the given destination. 17 | // It uses Client.PutDuplicate to copy files, so file content is not copied; 18 | // the underlying DirBlocks do not change. 19 | func (s *server) copy(dst upspin.PathName, srcs []upspin.PathName) error { 20 | // Check that the destination exists and is a directory. 21 | dstEntry, err := s.cli.Lookup(dst, true) 22 | if err != nil { 23 | return err 24 | } 25 | if !dstEntry.IsDir() { 26 | return errors.E(dst, errors.NotDir) 27 | } 28 | 29 | // Iterate through sources and copy them recursively. 30 | for _, src := range srcs { 31 | // Lookup src, but don't follow links. 32 | // We will make a copy of those links, not traverse them. 33 | srcEntry, err := s.cli.Lookup(src, false) 34 | if err != nil { 35 | return err 36 | } 37 | if err := s.copyEntry(dst, srcEntry); err != nil { 38 | return err 39 | } 40 | } 41 | return nil 42 | } 43 | 44 | // copyEntry copies the given entry to the given destination directory. 45 | // If the entry is a directory then the directory is copied recursively. 46 | // If the entry is a link then an equivalent link is created in dstDir. 47 | // This function assuems that dstDir exists and is a directory. 48 | func (s *server) copyEntry(dstDir upspin.PathName, srcEntry *upspin.DirEntry) error { 49 | srcPath, err := path.Parse(srcEntry.Name) 50 | if err != nil { 51 | return err 52 | } 53 | if srcPath.NElem() == 0 { 54 | // The browser user interface doesn't allow you to select a 55 | // root for a copy, so this shouldn't come up in practice. 56 | return errors.E(srcEntry.Name, "cannot copy a root") 57 | } 58 | dstDirPath, _ := path.Parse(dstDir) 59 | if dstDirPath.HasPrefix(srcPath) { 60 | return errors.E(srcEntry.Name, "cannot copy a directory into one of its sub-directories") 61 | } 62 | 63 | dst := path.Join(dstDir, srcPath.Elem(srcPath.NElem()-1)) 64 | 65 | switch { 66 | case srcEntry.IsDir(): 67 | // Recur into directories. 68 | if _, err := s.cli.MakeDirectory(dst); err != nil { 69 | return err 70 | } 71 | dir, err := s.cli.DirServer(srcEntry.Name) 72 | if err != nil { 73 | return err 74 | } 75 | des, err := dir.Glob(upspin.AllFilesGlob(srcEntry.Name)) 76 | if err != nil && err != upspin.ErrFollowLink { 77 | return err 78 | } 79 | for _, de := range des { 80 | if err := s.copyEntry(dst, de); err != nil { 81 | return err 82 | } 83 | } 84 | case srcEntry.IsLink(): 85 | if _, err := s.cli.PutLink(srcEntry.Link, dst); err != nil { 86 | return err 87 | } 88 | default: 89 | if _, err := s.cli.PutDuplicate(srcEntry.Name, dst); err != nil { 90 | return err 91 | } 92 | } 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /cmd/upspin-ui/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Upspin Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Command upspin-ui presents a web interface to the Upspin name space, 7 | and also provides a facility to sign up an Upspin user and deploy 8 | an upspinserver to Google Cloud Platform. 9 | It operates as the user in the specified config. 10 | If no config is available at the specified path, 11 | the user is prompted to sign up an Upspin user. 12 | 13 | Browser features 14 | 15 | The Upspin browser presents two navigation panes. 16 | 17 | Each browser pane lists the contents of an Upspin directory. 18 | The directory is shown in a text box at the top of each pane. 19 | 20 | You can navigate directly to a specific Upspin path by typing (or pasting) it 21 | into the text box and pressing the enter key. 22 | The button to the left of the text box navigates to the parent of the current 23 | directory. 24 | 25 | Clicking the name of an entry will attempt to download the entry with your web 26 | browser or, if the entry is a directory, will navigate to that directory. 27 | 28 | At startup, the left pane displays the current user's root and the right pane 29 | displays the path augie@upspin.io. 30 | 31 | The checkboxes beside each entry permit the (de-)selection of entries. 32 | The checkbox at the top of each list of entries (de-)selects all entries in 33 | that directory. 34 | 35 | The "Delete" button recursively deletes the selected files and directories. 36 | 37 | The "Copy" button recurisively copies the selected files and directories to 38 | the directory displayed in the opposite pane. 39 | 40 | The "Make directory" button creates a directory in the pane's current 41 | directory. 42 | 43 | The "Refresh" button reloads the contents of the directory and displays it. 44 | 45 | The info buttons (a little "i" in a circle, to the right of each file) display 46 | extended information for a given directory entry. 47 | 48 | Files created by upspin-ui 49 | 50 | The signup process creates a config file at the location provided by the 51 | -config flag. The flag's default value is $HOME/upspin/config. 52 | Signup also generates key files and puts them in the directory 53 | $HOME/.ssh/$USER, where $USER is the Upspin user name. 54 | 55 | The upspinserver deployment process records its state in a file with the same 56 | name as the config file with the additional suffix ".gcpState". 57 | This state file is used to resume the deployment process should the upspin-ui 58 | program crash or be terminated by the user. 59 | Once deployment is complete this file may be removed. 60 | Deployment also generates key files which it puts in $HOME/.ssh/$USER, 61 | where $USER is the Upspin user name of the server being deployed. 62 | 63 | During the signup and deployment processes, upspin-ui logs debugging 64 | information to $HOME/upspin/log/upspin-ui.log. 65 | */ 66 | package main 67 | -------------------------------------------------------------------------------- /cmd/upspin-ui/gcp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Upspin Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // TODO: tell the user to remove/deactivate the Owners service account once 6 | // we're done with it. (Or maybe we can do this mechanically?) 7 | 8 | package main 9 | 10 | import ( 11 | "bytes" 12 | "context" 13 | "encoding/json" 14 | "errors" 15 | "fmt" 16 | "io/ioutil" 17 | "net/http" 18 | "path/filepath" 19 | "sort" 20 | "strings" 21 | "time" 22 | 23 | "upspin.io/flags" 24 | "upspin.io/subcmd" 25 | "upspin.io/upspin" 26 | 27 | "golang.org/x/oauth2/google" 28 | "golang.org/x/oauth2/jwt" 29 | compute "google.golang.org/api/compute/v1" 30 | "google.golang.org/api/googleapi" 31 | iam "google.golang.org/api/iam/v1" 32 | servicemanagement "google.golang.org/api/servicemanagement/v1" 33 | storage "google.golang.org/api/storage/v1" 34 | ) 35 | 36 | // gcpState represents the state of a GCP deployment. As the process proceeds, 37 | // the fields are populated with nonzero values from top to bottom. 38 | type gcpState struct { 39 | JWTConfig *jwt.Config 40 | ProjectID string 41 | 42 | APIsEnabled bool 43 | 44 | Region string 45 | Zone string 46 | 47 | Storage struct { 48 | ServiceAccount string 49 | PrivateKeyData string 50 | Bucket string 51 | } 52 | 53 | Server struct { 54 | IPAddr string 55 | 56 | Created bool 57 | 58 | KeyDir string 59 | UserName upspin.UserName 60 | 61 | HostName string 62 | 63 | Configured bool 64 | } 65 | } 66 | 67 | // serverConfig returns a *subcmd.ServerConfig that can be used to configure 68 | // the running upspinserver. 69 | func (s *gcpState) serverConfig() *subcmd.ServerConfig { 70 | return &subcmd.ServerConfig{ 71 | Addr: upspin.NetAddr(s.Server.HostName), 72 | User: s.Server.UserName, 73 | StoreConfig: s.storeConfig(), 74 | } 75 | } 76 | 77 | // storeConfig returns the StoreServer configuration for the upspinserver. 78 | func (s *gcpState) storeConfig() []string { 79 | return []string{ 80 | "backend=GCS", 81 | "defaultACL=publicRead", 82 | "gcpBucketName=" + s.Storage.Bucket, 83 | "privateKeyData=" + s.Storage.PrivateKeyData, 84 | } 85 | } 86 | 87 | // gcpStateFromFile loads the JSON-encoded GCP deployment state file from 88 | // flags.Config+".gcpstate". 89 | func gcpStateFromFile() (*gcpState, error) { 90 | name := flags.Config + ".gcpState" 91 | b, err := ioutil.ReadFile(name) 92 | if err != nil { 93 | return nil, err 94 | } 95 | var s gcpState 96 | if err := json.Unmarshal(b, &s); err != nil { 97 | return nil, err 98 | } 99 | return &s, nil 100 | } 101 | 102 | // save writes the JSON-encoded GCP deployment state to 103 | // flags.Config+".gcpstate". 104 | func (s *gcpState) save() error { 105 | name := flags.Config + ".gcpState" 106 | b, err := json.Marshal(s) 107 | if err != nil { 108 | return err 109 | } 110 | return ioutil.WriteFile(name, b, 0644) 111 | } 112 | 113 | // gcpStateFromPrivateKeyJSON instantiates a new gcpState from the given 114 | // Google Cloud Platform Service Account JSON Private Key file. 115 | func gcpStateFromPrivateKeyJSON(b []byte) (*gcpState, error) { 116 | cfg, err := google.JWTConfigFromJSON(b, compute.CloudPlatformScope) 117 | if err != nil { 118 | return nil, err 119 | } 120 | projectID, err := serviceAccountEmailToProjectID(cfg.Email) 121 | if err != nil { 122 | return nil, err 123 | } 124 | s := &gcpState{ 125 | JWTConfig: cfg, 126 | ProjectID: projectID, 127 | } 128 | if !s.APIsEnabled { 129 | if err := s.enableAPIs(); err != nil { 130 | return nil, err 131 | } 132 | s.APIsEnabled = true 133 | } 134 | if err := s.save(); err != nil { 135 | return nil, err 136 | } 137 | return s, nil 138 | } 139 | 140 | // serviceAccountEmailToProjectID takes a service account email address and 141 | // extracts the project ID component from its domain part. 142 | func serviceAccountEmailToProjectID(email string) (string, error) { 143 | i := strings.Index(email, "@") 144 | if i < 0 { 145 | return "", fmt.Errorf("service account email %q has no @ sign", email) 146 | } 147 | const domain = ".iam.gserviceaccount.com" 148 | if !strings.HasSuffix(email, domain) { 149 | return "", fmt.Errorf("service account email %q does not have expected form", email) 150 | } 151 | return email[i+1 : len(email)-len(domain)], nil 152 | } 153 | 154 | // enableAPIs enables the Compute, Storage, and IAM APIs required to deploy 155 | // upspinserver to GCP. 156 | func (s *gcpState) enableAPIs() error { 157 | client := s.JWTConfig.Client(context.Background()) 158 | svc, err := servicemanagement.New(client) 159 | if err != nil { 160 | return err 161 | } 162 | apis := []string{ 163 | "compute_component", // For the virtual machine. 164 | "storage_api", // For storage bucket. 165 | "iam.googleapis.com", // For creating a service account. 166 | } 167 | for _, api := range apis { 168 | if err := s.enableAPI(api, svc); err != nil { 169 | return err 170 | } 171 | } 172 | return nil 173 | } 174 | 175 | // enableAPI enables the named GCP API using the provided service. 176 | func (s *gcpState) enableAPI(name string, svc *servicemanagement.APIService) error { 177 | op, err := svc.Services.Enable(name, &servicemanagement.EnableServiceRequest{ConsumerId: "project:" + s.ProjectID}).Do() 178 | if err != nil { 179 | return err 180 | } 181 | for !op.Done { 182 | time.Sleep(250 * time.Millisecond) 183 | op, err = svc.Operations.Get(op.Name).Do() 184 | if err != nil { 185 | return err 186 | } 187 | } 188 | if op.Error != nil { 189 | return errors.New(op.Error.Message) 190 | } 191 | return err 192 | } 193 | 194 | func (s *gcpState) listZones() ([]string, error) { 195 | client := s.JWTConfig.Client(context.Background()) 196 | svc, err := compute.New(client) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | list, err := svc.Regions.List(s.ProjectID).Do() 202 | if err != nil { 203 | return nil, err 204 | } 205 | var zones []string 206 | for _, region := range list.Items { 207 | if region.Status == "DOWN" { 208 | continue 209 | } 210 | for _, z := range region.Zones { 211 | i := strings.LastIndex(z, "/") 212 | if i < 0 { 213 | continue 214 | } 215 | zones = append(zones, region.Name+z[i:]) 216 | } 217 | } 218 | sort.Strings(zones) 219 | return zones, nil 220 | } 221 | 222 | func (s *gcpState) listStorageLocations() ([]string, error) { 223 | // There's no API for this. Scraped from: 224 | // https://cloud.google.com/storage/docs/bucket-locations 225 | return []string{ 226 | // Multi-regional locations. 227 | "asia", 228 | "eu", 229 | "us", 230 | // Regional locations. 231 | "asia-east1", 232 | "asia-northeast1", 233 | "asia-southeast1", 234 | "australia-southeast1", 235 | "europe-west1", 236 | "europe-west2", 237 | "europe-west3", 238 | "us-central1", 239 | "us-east1", 240 | "us-east4", 241 | "us-west1", 242 | }, nil 243 | } 244 | 245 | // create creates a Storage bucket with the given name, a service account to 246 | // access the bucket, and a Compute instance running on a static IP address. 247 | func (s *gcpState) create(region, zone, bucketName, bucketLoc string) error { 248 | s.Region = region 249 | s.Zone = zone 250 | 251 | if s.Storage.ServiceAccount == "" { 252 | email, key, err := s.createServiceAccount() 253 | if err != nil { 254 | return err 255 | } 256 | s.Storage.ServiceAccount = email 257 | s.Storage.PrivateKeyData = key 258 | if err := s.save(); err != nil { 259 | return err 260 | } 261 | } 262 | if s.Storage.Bucket == "" { 263 | err := s.createBucket(bucketName, bucketLoc) 264 | if err != nil { 265 | return err 266 | } 267 | s.Storage.Bucket = bucketName 268 | if err := s.save(); err != nil { 269 | return err 270 | } 271 | } 272 | if s.Server.IPAddr == "" { 273 | ip, err := s.createAddress() 274 | if err != nil { 275 | return err 276 | } 277 | s.Server.IPAddr = ip 278 | if err := s.save(); err != nil { 279 | return err 280 | } 281 | } 282 | if !s.Server.Created { 283 | err := s.createInstance() 284 | if err != nil { 285 | return err 286 | } 287 | s.Server.Created = true 288 | if err := s.save(); err != nil { 289 | return err 290 | } 291 | } 292 | return nil 293 | } 294 | 295 | // createAddress reserves a static IP address with the name "upspinserver". 296 | func (s *gcpState) createAddress() (ip string, err error) { 297 | client := s.JWTConfig.Client(context.Background()) 298 | svc, err := compute.New(client) 299 | if err != nil { 300 | return "", err 301 | } 302 | 303 | const addressName = "upspinserver" 304 | addr := &compute.Address{ 305 | Description: "Public IP address for upspinserver", 306 | Name: addressName, 307 | } 308 | op, err := svc.Addresses.Insert(s.ProjectID, s.Region, addr).Do() 309 | if err = okReason("alreadyExists", s.waitOp(svc, op, err)); err != nil { 310 | return "", err 311 | } 312 | addr, err = svc.Addresses.Get(s.ProjectID, s.Region, addressName).Do() 313 | if err != nil { 314 | return "", err 315 | } 316 | return addr.Address, nil 317 | } 318 | 319 | // createInstance creates a Compute instance named "upspinserver" running the 320 | // upspinserver Docker image and a firewall rule to allow HTTPS connections to 321 | // that instance. If a firewall rule of the name "allow-http-https" exists it 322 | // is re-used. 323 | func (s *gcpState) createInstance() error { 324 | client := s.JWTConfig.Client(context.Background()) 325 | svc, err := compute.New(client) 326 | if err != nil { 327 | return err 328 | } 329 | 330 | const ( 331 | firewallName = "allow-http-https" 332 | firewallTag = firewallName 333 | 334 | instanceName = "upspinserver" 335 | ) 336 | 337 | // Create a firewall to permit HTTPS connections. 338 | firewall := &compute.Firewall{ 339 | Allowed: []*compute.FirewallAllowed{{ 340 | IPProtocol: "tcp", 341 | Ports: []string{"80", "443"}, 342 | }}, 343 | Description: "Allow HTTP and HTTPS", 344 | Name: firewallName, 345 | SourceRanges: []string{"0.0.0.0/0"}, 346 | TargetTags: []string{firewallTag}, 347 | } 348 | op, err := svc.Firewalls.Insert(s.ProjectID, firewall).Do() 349 | if err = okReason("alreadyExists", s.waitOp(svc, op, err)); err != nil { 350 | return err 351 | } 352 | 353 | // Create a firewall to permit HTTPS connections. 354 | // Create the instance. 355 | userData := cloudInitYAML 356 | instance := &compute.Instance{ 357 | Description: "upspinserver instance", 358 | Disks: []*compute.AttachedDisk{{ 359 | AutoDelete: true, 360 | Boot: true, 361 | DeviceName: "upspinserver", 362 | InitializeParams: &compute.AttachedDiskInitializeParams{ 363 | SourceImage: "projects/cos-cloud/global/images/family/cos-stable", 364 | }, 365 | }}, 366 | MachineType: "zones/" + s.Zone + "/machineTypes/g1-small", 367 | Name: instanceName, 368 | Tags: &compute.Tags{Items: []string{firewallTag}}, 369 | Metadata: &compute.Metadata{ 370 | Items: []*compute.MetadataItems{{ 371 | Key: "user-data", 372 | Value: &userData, 373 | }}, 374 | }, 375 | NetworkInterfaces: []*compute.NetworkInterface{{ 376 | AccessConfigs: []*compute.AccessConfig{{ 377 | NatIP: s.Server.IPAddr, 378 | }}, 379 | }}, 380 | } 381 | op, err = svc.Instances.Insert(s.ProjectID, s.Zone, instance).Do() 382 | return s.waitOp(svc, op, err) 383 | } 384 | 385 | // createServiceAccount creates a service account named "upspinstorage" and 386 | // generates a JSON Private Key for authenticating as that account. 387 | func (s *gcpState) createServiceAccount() (email, privateKeyData string, err error) { 388 | client := s.JWTConfig.Client(context.Background()) 389 | svc, err := iam.New(client) 390 | if err != nil { 391 | return "", "", err 392 | } 393 | 394 | name := "projects/" + s.ProjectID 395 | req := &iam.CreateServiceAccountRequest{ 396 | AccountId: "upspinstorage", 397 | ServiceAccount: &iam.ServiceAccount{ 398 | DisplayName: "Upspin Storage", 399 | }, 400 | } 401 | acct, err := svc.Projects.ServiceAccounts.Create(name, req).Do() 402 | if isExists(err) { 403 | // This should be the name we need to get. 404 | // TODO(adg): make this more robust by listing instead. 405 | guess := name + "/serviceAccounts/upspinstorage@" + s.ProjectID + ".iam.gserviceaccount.com" 406 | acct, err = svc.Projects.ServiceAccounts.Get(guess).Do() 407 | } 408 | if err != nil { 409 | return "", "", err 410 | } 411 | 412 | name += "/serviceAccounts/" + acct.Email 413 | req2 := &iam.CreateServiceAccountKeyRequest{} 414 | key, err := svc.Projects.ServiceAccounts.Keys.Create(name, req2).Do() 415 | if err != nil { 416 | return "", "", err 417 | } 418 | return acct.Email, key.PrivateKeyData, nil 419 | } 420 | 421 | // createBucket creates the named Storage bucket, giving "owner" access to 422 | // Storage.ServiceAccount in gcpState. 423 | func (s *gcpState) createBucket(name, loc string) error { 424 | client := s.JWTConfig.Client(context.Background()) 425 | svc, err := storage.New(client) 426 | if err != nil { 427 | return err 428 | } 429 | 430 | acl := &storage.BucketAccessControl{ 431 | Bucket: name, 432 | Entity: "user-" + s.Storage.ServiceAccount, 433 | Email: s.Storage.ServiceAccount, 434 | Role: "OWNER", 435 | } 436 | _, err = svc.Buckets.Insert(s.ProjectID, &storage.Bucket{ 437 | Acl: []*storage.BucketAccessControl{acl}, 438 | Name: name, 439 | Location: loc, 440 | }).Do() 441 | if !isExists(err) { 442 | return err // May be nil. 443 | } 444 | // Bucket already exists. Check bucket ownership and ACL to make sure 445 | // the service account has access. 446 | bkt, err := svc.Buckets.Get(name).Do() 447 | if err != nil { 448 | return err 449 | } 450 | for _, a := range bkt.Acl { 451 | if a.Email == s.Storage.ServiceAccount && a.Role == "OWNER" { 452 | // The service account has OWNER privileges; we're ok. 453 | return nil 454 | } 455 | } 456 | // The service account doesn't have OWNER privileges; try to add them. 457 | bkt.Acl = append(bkt.Acl, acl) 458 | _, err = svc.Buckets.Update(name, bkt).Do() 459 | return err 460 | } 461 | 462 | // waitOp waits for the given compute operation to complete and returns the 463 | // first error that occurred, if any. 464 | func (s *gcpState) waitOp(svc *compute.Service, op *compute.Operation, err error) error { 465 | for err == nil && (op.Status == "PENDING" || op.Status == "RUNNING") { 466 | time.Sleep(250 * time.Millisecond) 467 | switch { 468 | case op.Zone != "": 469 | op, err = svc.ZoneOperations.Get(s.ProjectID, s.Zone, op.Name).Do() 470 | case op.Region != "": 471 | op, err = svc.RegionOperations.Get(s.ProjectID, s.Region, op.Name).Do() 472 | default: 473 | op, err = svc.GlobalOperations.Get(s.ProjectID, op.Name).Do() 474 | } 475 | } 476 | return opError(op, err) 477 | } 478 | 479 | // opError returns err or the first error in the given Operation, if any. 480 | func opError(op *compute.Operation, err error) error { 481 | if err != nil { 482 | return err 483 | } 484 | if op == nil || op.Error == nil || len(op.Error.Errors) == 0 { 485 | return nil 486 | } 487 | return errors.New(op.Error.Errors[0].Message) 488 | } 489 | 490 | // isExists reports whether err is an "already exists" Google API error. 491 | func isExists(err error) bool { 492 | return err != nil && (okReason("alreadyExists", err) == nil || okReason("conflict", err) == nil) 493 | } 494 | 495 | // okReason checks whether err is a Google API error with the given reason and 496 | // returns nil if so. Otherwise, it returns the given error. 497 | func okReason(reason string, err error) error { 498 | if ge, ok := err.(*googleapi.Error); ok && len(ge.Errors) > 0 { 499 | for _, e := range ge.Errors { 500 | if e.Reason != reason { 501 | return err 502 | } 503 | } 504 | return nil 505 | } 506 | return err 507 | } 508 | 509 | // configureServer configures an unconfigured upspinserver instance using 510 | // the state from gcpState and the given set of writers. 511 | // It is analagous to running "upspin setupserver". 512 | // 513 | // TODO(adg): this needn't be a method on gcpState as it's not at all to do 514 | // with GCP. Move it elsewhere if/when we decide to generalize this to support 515 | // other cloud service providers. 516 | func (s *gcpState) configureServer(writers []upspin.UserName) error { 517 | files := map[string][]byte{} 518 | 519 | var buf bytes.Buffer 520 | for _, u := range writers { 521 | fmt.Fprintln(&buf, u) 522 | } 523 | files["Writers"] = buf.Bytes() 524 | 525 | for _, name := range []string{"public.upspinkey", "secret.upspinkey"} { 526 | b, err := ioutil.ReadFile(filepath.Join(s.Server.KeyDir, name)) 527 | if err != nil { 528 | return err 529 | } 530 | files[name] = b 531 | } 532 | 533 | scfg := s.serverConfig() 534 | b, err := json.Marshal(scfg) 535 | if err != nil { 536 | return err 537 | } 538 | files["serverconfig.json"] = b 539 | 540 | b, err = json.Marshal(files) 541 | if err != nil { 542 | return err 543 | } 544 | u := "https://" + string(scfg.Addr) + "/setupserver" 545 | resp, err := http.Post(u, "application/octet-stream", bytes.NewReader(b)) 546 | if err != nil { 547 | return err 548 | } 549 | b, _ = ioutil.ReadAll(resp.Body) 550 | resp.Body.Close() 551 | if resp.StatusCode != http.StatusOK { 552 | return fmt.Errorf("upspinserver returned status %v:\n%s", resp.Status, b) 553 | } 554 | return nil 555 | } 556 | 557 | // cloudInitYAML is the cloud-init configuration file for the virtual machine 558 | // running Google's Container-Optimized OS. It instructs the machine to accept 559 | // incoming TCP connections on ports 80 and 443 and to run the 560 | // gcr.io/upspin-containers-upspinserver Docker image, exposing the 561 | // upspinserver service on ports 80 and 443. 562 | const cloudInitYAML = `#cloud-config 563 | 564 | users: 565 | - name: upspin 566 | uid: 2000 567 | 568 | runcmd: 569 | - iptables -w -A INPUT -p tcp --dport 80 -j ACCEPT 570 | - iptables -w -A INPUT -p tcp --dport 443 -j ACCEPT 571 | 572 | write_files: 573 | - path: /etc/systemd/system/upspinserver.service 574 | permissions: 0644 575 | owner: root 576 | content: | 577 | [Unit] 578 | Description=An upspinserver container instance 579 | Wants=gcr-online.target 580 | After=gcr-online.target 581 | [Service] 582 | Environment="HOME=/home/upspin" 583 | ExecStartPre=/usr/bin/docker-credential-gcr configure-docker 584 | ExecStartPre=/usr/bin/docker pull gcr.io/upspin-containers/upspinserver:latest 585 | ExecStart=/usr/bin/docker run --rm -u=2000 --volume=/home/upspin:/upspin -p=80:8080 -p=443:8443 --name=upspinserver gcr.io/upspin-containers/upspinserver:latest 586 | ExecStop=/usr/bin/docker stop upspinserver 587 | ExecStopPost=/usr/bin/docker rm upspinserver 588 | Restart=on-failure 589 | 590 | runcmd: 591 | - systemctl daemon-reload 592 | - systemctl start upspinserver.service 593 | 594 | ` 595 | -------------------------------------------------------------------------------- /cmd/upspin-ui/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Upspin Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "sync" 12 | 13 | "upspin.io/config" 14 | upLog "upspin.io/log" 15 | "upspin.io/shutdown" 16 | ) 17 | 18 | // logf logs a formatted log message to $HOME/upspin/log/browser.log, 19 | // or to standard error if that file cannot be opened. 20 | func logf(format string, args ...interface{}) { 21 | logger.Lock() 22 | defer logger.Unlock() 23 | logger.Printf(format, args...) 24 | } 25 | 26 | // logger is the log.Logger used by logf. 27 | var logger struct { 28 | sync.Mutex 29 | *log.Logger 30 | } 31 | 32 | func init() { 33 | l, err := newLogger() 34 | if err != nil { 35 | // Fall back to standard error if we can't log to a file. 36 | l = log.New(os.Stderr, "upspin-ui: ", log.LstdFlags) 37 | l.Print(err) 38 | } 39 | logger.Logger = l 40 | } 41 | 42 | // newLogger initializes a log.Logger that writes to 43 | // $HOME/upspin/log/upspin-ui.log and redirects the Upspin logger and the 44 | // standard logger to that file. 45 | func newLogger() (*log.Logger, error) { 46 | home, err := config.Homedir() 47 | if err != nil { 48 | return nil, err 49 | } 50 | dir := filepath.Join(home, "upspin", "log") 51 | if err := os.MkdirAll(dir, 0755); err != nil { 52 | return nil, err 53 | } 54 | file := filepath.Join(dir, "upspin-ui.log") 55 | const flags = os.O_WRONLY | os.O_CREATE | os.O_APPEND 56 | f, err := os.OpenFile(file, flags, 0600) 57 | if err != nil { 58 | return nil, err 59 | } 60 | shutdown.Handle(func() { 61 | f.Close() 62 | }) 63 | upLog.SetOutput(f) 64 | log.SetOutput(f) 65 | return log.New(f, "", log.LstdFlags), nil 66 | } 67 | -------------------------------------------------------------------------------- /cmd/upspin-ui/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Upspin Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main // import "augie.upspin.io/cmd/upspin-ui" 6 | 7 | // TODO(adg): Flesh out the inspector (show blocks, etc). 8 | // TODO(adg): Update the URL in the browser window to reflect the UI. 9 | // TODO(adg): Facility to add/edit Access files in UI. 10 | // TODO(adg): Awareness of Access files during copy and remove. 11 | // TODO(adg): Show progress of removes/copies in the user interface. 12 | // TODO(adg): Display links and handle their navigation properly. 13 | 14 | import ( 15 | "crypto/rand" 16 | "encoding/json" 17 | "flag" 18 | "fmt" 19 | "net" 20 | "net/http" 21 | "os" 22 | "os/exec" 23 | "path" 24 | "runtime" 25 | "strings" 26 | "sync" 27 | "time" 28 | 29 | "golang.org/x/net/xsrftoken" 30 | 31 | "augie.upspin.io/cmd/upspin-ui/static" 32 | 33 | "upspin.io/errors" 34 | "upspin.io/flags" 35 | "upspin.io/upspin" 36 | "upspin.io/version" 37 | 38 | _ "upspin.io/transports" 39 | ) 40 | 41 | const defaultPath = "augie@upspin.io/" // Show this tree on startup. 42 | 43 | func main() { 44 | httpAddr := flag.String("http", "localhost:8000", "HTTP listen `address` (must be loopback)") 45 | versionFlag := flag.Bool("version", false, "print version string and exit") 46 | flags.Parse(flags.Client) 47 | 48 | if *versionFlag { 49 | fmt.Print(version.Version()) 50 | return 51 | } 52 | 53 | // Disallow listening on non-loopback addresses until we have a better 54 | // security model. (Even this is not really secure enough.) 55 | if err := isLocal(*httpAddr); err != nil { 56 | exit(err) 57 | } 58 | 59 | s, err := newServer() 60 | if err != nil { 61 | exit(err) 62 | } 63 | http.Handle("/", s) 64 | 65 | l, err := net.Listen("tcp", *httpAddr) 66 | if err != nil { 67 | exit(err) 68 | } 69 | url := fmt.Sprintf("http://%s/#key=%s", *httpAddr, s.key) 70 | if !startBrowser(url) { 71 | fmt.Printf("Open %s in your web browser.\n", url) 72 | } else { 73 | fmt.Printf("Serving at %s\n", url) 74 | } 75 | exit(http.Serve(l, nil)) 76 | } 77 | 78 | func exit(err error) { 79 | fmt.Fprintln(os.Stderr, err) 80 | os.Exit(1) 81 | } 82 | 83 | // server implements an http.Handler that performs various Upspin operations 84 | // using a config. It is the back end for the JavaScript Upspin browser. 85 | type server struct { 86 | // key to prevent request forgery; static for server's lifetime. 87 | key string 88 | 89 | mu sync.Mutex 90 | cfg upspin.Config // Non-nil if signup flow has been completed. 91 | cli upspin.Client 92 | } 93 | 94 | func newServer() (*server, error) { 95 | key, err := generateKey() 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | return &server{ 101 | key: key, 102 | }, nil 103 | } 104 | 105 | func (s *server) hasConfig() bool { 106 | s.mu.Lock() 107 | defer s.mu.Unlock() 108 | return s.cfg != nil && s.cli != nil 109 | } 110 | 111 | func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 112 | p := r.URL.Path 113 | if p == "/_upspin" { 114 | s.serveAPI(w, r) 115 | return 116 | } 117 | if strings.Contains(p, "@") { 118 | s.serveContent(w, r) 119 | return 120 | } 121 | s.serveStatic(w, r) 122 | } 123 | 124 | func (s *server) serveStatic(w http.ResponseWriter, r *http.Request) { 125 | p := r.URL.Path[1:] 126 | if p == "" { 127 | p = "index.html" 128 | } 129 | b, err := static.File(p) 130 | if errors.Match(errors.E(errors.NotExist), err) { 131 | http.NotFound(w, r) 132 | return 133 | } 134 | if err != nil { 135 | http.Error(w, err.Error(), http.StatusInternalServerError) 136 | return 137 | } 138 | http.ServeContent(w, r, path.Base(p), time.Now(), strings.NewReader(b)) 139 | } 140 | 141 | func (s *server) serveContent(w http.ResponseWriter, r *http.Request) { 142 | if !s.hasConfig() { 143 | http.Error(w, "No configuration", http.StatusServiceUnavailable) 144 | return 145 | } 146 | 147 | p := r.URL.Path[1:] 148 | if !xsrftoken.Valid(r.FormValue("token"), s.key, string(s.cfg.UserName()), p) { 149 | http.Error(w, "Invalid XSRF token", http.StatusForbidden) 150 | return 151 | } 152 | 153 | name := upspin.PathName(p) 154 | de, err := s.cli.Lookup(name, true) 155 | if err != nil { 156 | httpError(w, err) 157 | return 158 | } 159 | f, err := s.cli.Open(name) 160 | if err != nil { 161 | httpError(w, err) 162 | return 163 | } 164 | http.ServeContent(w, r, path.Base(p), de.Time.Go(), f) 165 | f.Close() 166 | } 167 | 168 | func (s *server) serveAPI(w http.ResponseWriter, r *http.Request) { 169 | if r.Method != "POST" { 170 | http.Error(w, "Expected POST request", http.StatusMethodNotAllowed) 171 | return 172 | } 173 | 174 | // Require a valid key. 175 | if r.FormValue("key") != s.key { 176 | http.Error(w, "Invalid key", http.StatusForbidden) 177 | return 178 | } 179 | 180 | method := r.FormValue("method") 181 | 182 | // Don't permit accesses of non-startup methods if there is no config 183 | // nor client; those methods need them. 184 | if method != "startup" && !s.hasConfig() { 185 | http.Error(w, "No configuration", http.StatusServiceUnavailable) 186 | return 187 | } 188 | 189 | var resp interface{} 190 | switch method { 191 | case "startup": 192 | sResp, cfg, err := s.startup(r) 193 | var errString string 194 | if err != nil { 195 | errString = err.Error() 196 | } 197 | var ( 198 | user upspin.UserName 199 | left, right upspin.PathName 200 | ) 201 | if cfg != nil { 202 | user = cfg.UserName() 203 | right = defaultPath 204 | // If the user has a directory endpoint then open to 205 | // their tree. Otherwise, open both panels to augie. 206 | if cfg.DirEndpoint().Transport == upspin.Remote { 207 | left = upspin.PathName(user + "/") 208 | } else { 209 | left = right 210 | } 211 | } 212 | ver := "upspin-ui" 213 | if sha := version.GitSHA; sha != "" { 214 | ver = fmt.Sprintf("%s commit %.7s built %s", ver, sha, version.BuildTime.Format("2 Jan 06")) 215 | } 216 | resp = struct { 217 | Startup *startupResponse 218 | UserName upspin.UserName 219 | LeftPath upspin.PathName 220 | RightPath upspin.PathName 221 | Version string 222 | Error string 223 | }{sResp, user, left, right, ver, errString} 224 | case "list": 225 | path := upspin.PathName(r.FormValue("path")) 226 | des, err := s.cli.Glob(upspin.AllFilesGlob(path)) 227 | var errString string 228 | if err != nil { 229 | errString = err.Error() 230 | } 231 | var entries []entryWithToken 232 | for _, de := range des { 233 | tok := xsrftoken.Generate(s.key, string(s.cfg.UserName()), string(de.Name)) 234 | entries = append(entries, entryWithToken{ 235 | DirEntry: de, 236 | FileToken: tok, 237 | }) 238 | } 239 | resp = struct { 240 | Entries []entryWithToken 241 | Error string 242 | }{entries, errString} 243 | case "mkdir": 244 | _, err := s.cli.MakeDirectory(upspin.PathName(r.FormValue("path"))) 245 | var errString string 246 | if err != nil { 247 | errString = err.Error() 248 | } 249 | resp = struct { 250 | Error string 251 | }{errString} 252 | case "rm": 253 | var errString string 254 | for _, p := range r.Form["paths[]"] { 255 | if err := s.rm(upspin.PathName(p)); err != nil { 256 | errString = err.Error() 257 | break 258 | } 259 | } 260 | resp = struct { 261 | Error string 262 | }{errString} 263 | case "copy": 264 | dst := upspin.PathName(r.FormValue("dest")) 265 | var paths []upspin.PathName 266 | for _, p := range r.Form["paths[]"] { 267 | paths = append(paths, upspin.PathName(p)) 268 | } 269 | var errString string 270 | if err := s.copy(dst, paths); err != nil { 271 | errString = err.Error() 272 | } 273 | resp = struct { 274 | Error string 275 | }{errString} 276 | case "put": 277 | const maxMultipartSize = 500e6 278 | if err := r.ParseMultipartForm(maxMultipartSize); err != nil { 279 | http.Error(w, "Parse error: "+err.Error(), http.StatusBadRequest) 280 | return 281 | } 282 | if len(r.MultipartForm.File) == 0 { 283 | http.Error(w, "missing file", http.StatusBadRequest) 284 | return 285 | } 286 | var errString string 287 | for _, fhs := range r.MultipartForm.File { 288 | if len(fhs) == 0 { 289 | http.Error(w, "missing file handle", http.StatusBadRequest) 290 | return 291 | } 292 | err := s.put(upspin.PathName(r.FormValue("dir")), fhs[0]) 293 | if err != nil { 294 | errString = err.Error() 295 | break 296 | } 297 | } 298 | resp = struct { 299 | Error string 300 | }{errString} 301 | } 302 | b, err := json.Marshal(resp) 303 | if err != nil { 304 | http.Error(w, err.Error(), http.StatusInternalServerError) 305 | return 306 | } 307 | w.Write(b) 308 | } 309 | 310 | type entryWithToken struct { 311 | *upspin.DirEntry 312 | FileToken string 313 | } 314 | 315 | func generateKey() (string, error) { 316 | b := make([]byte, 32) 317 | _, err := rand.Read(b) 318 | if err != nil { 319 | return "", err 320 | } 321 | return fmt.Sprintf("%x", b), nil 322 | } 323 | 324 | // isLocal returns an error if the given address is not a loopback address. 325 | func isLocal(addr string) error { 326 | host, _, err := net.SplitHostPort(addr) 327 | if err != nil { 328 | return err 329 | } 330 | ips, err := net.LookupIP(host) 331 | if err != nil { 332 | return err 333 | } 334 | for _, ip := range ips { 335 | if !ip.IsLoopback() { 336 | return fmt.Errorf("cannot listen on non-loopback address %q", addr) 337 | } 338 | } 339 | return nil 340 | } 341 | 342 | // ifError checks if the error is the expected one, and if so writes back an 343 | // HTTP error of the corresponding code. 344 | func ifError(w http.ResponseWriter, got error, want errors.Kind, code int) bool { 345 | if !errors.Match(errors.E(want), got) { 346 | return false 347 | } 348 | http.Error(w, http.StatusText(code), code) 349 | return true 350 | } 351 | 352 | func httpError(w http.ResponseWriter, err error) { 353 | // This construction sets the HTTP error to the first type that matches. 354 | switch { 355 | case ifError(w, err, errors.Private, http.StatusForbidden): 356 | case ifError(w, err, errors.Permission, http.StatusForbidden): 357 | case ifError(w, err, errors.NotExist, http.StatusNotFound): 358 | case ifError(w, err, errors.BrokenLink, http.StatusNotFound): 359 | default: 360 | http.Error(w, err.Error(), http.StatusInternalServerError) 361 | } 362 | } 363 | 364 | // startBrowser tries to open the URL in a web browser, 365 | // and reports whether it succeed. 366 | func startBrowser(url string) bool { 367 | var args []string 368 | switch runtime.GOOS { 369 | case "darwin": 370 | args = []string{"open"} 371 | case "windows": 372 | args = []string{"cmd", "/c", "start"} 373 | default: 374 | args = []string{"xdg-open"} 375 | } 376 | cmd := exec.Command(args[0], append(args[1:], url)...) 377 | return cmd.Start() == nil 378 | } 379 | -------------------------------------------------------------------------------- /cmd/upspin-ui/put.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Upspin Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "io" 9 | "mime/multipart" 10 | 11 | "upspin.io/path" 12 | "upspin.io/upspin" 13 | ) 14 | 15 | // put reads a mime/multipart-encoded file and saves it as an Upspin file in 16 | // the given directory. 17 | func (s *server) put(dir upspin.PathName, fh *multipart.FileHeader) error { 18 | src, err := fh.Open() 19 | if err != nil { 20 | return err 21 | } 22 | dst, err := s.cli.Create(path.Join(dir, fh.Filename)) 23 | if err != nil { 24 | return err 25 | } 26 | _, err = io.Copy(dst, src) 27 | if err != nil { 28 | return err 29 | } 30 | err = dst.Close() 31 | src.Close() 32 | return err 33 | } 34 | -------------------------------------------------------------------------------- /cmd/upspin-ui/rm.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Upspin Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import "upspin.io/upspin" 8 | 9 | // rm recursively removes the given path. 10 | func (s *server) rm(name upspin.PathName) error { 11 | de, err := s.cli.Lookup(name, false) 12 | if err != nil { 13 | return err 14 | } 15 | return s.rmEntry(de) 16 | } 17 | 18 | // rmEntry removes the given entry. If the entry is a directory it removes its 19 | // contents before removing the directory itself. 20 | func (s *server) rmEntry(de *upspin.DirEntry) error { 21 | if de.IsDir() { 22 | dir, err := s.cli.DirServer(de.Name) 23 | if err != nil { 24 | return err 25 | } 26 | des, err := dir.Glob(upspin.AllFilesGlob(de.Name)) 27 | if err != nil { 28 | return err 29 | } 30 | for _, de := range des { 31 | if err := s.rmEntry(de); err != nil { 32 | return err 33 | } 34 | } 35 | } 36 | return s.cli.Delete(de.Name) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/upspin-ui/static/.gitignore: -------------------------------------------------------------------------------- 1 | # Don't commit static.go; it should be generated before building a release 2 | # version of the browser binary. Only files generated by the build process 3 | # should be included here. 4 | static.go 5 | -------------------------------------------------------------------------------- /cmd/upspin-ui/static/augie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upspin/augie/798945a1f13394a5ab2d79bfdf1a68d87b797151/cmd/upspin-ui/static/augie.png -------------------------------------------------------------------------------- /cmd/upspin-ui/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upspin/augie/798945a1f13394a5ab2d79bfdf1a68d87b797151/cmd/upspin-ui/static/favicon-32x32.png -------------------------------------------------------------------------------- /cmd/upspin-ui/static/gen.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Upspin Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package static provides access to static assets, such as HTML, CSS, 6 | // JavaScript, and image files. 7 | package static // import "augie.upspin.io/cmd/upspin-ui/static" 8 | 9 | import ( 10 | "go/build" 11 | "io/ioutil" 12 | "os" 13 | "path/filepath" 14 | "sync" 15 | 16 | "upspin.io/errors" 17 | ) 18 | 19 | //go:generate go run makestatic.go 20 | 21 | var files map[string]string 22 | 23 | var static struct { 24 | once sync.Once 25 | dir string 26 | } 27 | 28 | // File returns the file rooted at "augie.upspin.io/cmd/upspin-ui/static" either 29 | // from an in-memory map or, if no map was generated, the contents of the file 30 | // from disk. 31 | func File(name string) (string, error) { 32 | if files != nil { 33 | b, ok := files[name] 34 | if !ok { 35 | return "", errors.E(errors.NotExist, errors.Str("file not found")) 36 | } 37 | return b, nil 38 | 39 | } 40 | static.once.Do(func() { 41 | pkg, _ := build.Default.Import("augie.upspin.io/cmd/upspin-ui/static", "", build.FindOnly) 42 | if pkg == nil { 43 | return 44 | } 45 | static.dir = pkg.Dir 46 | }) 47 | if static.dir == "" { 48 | return "", errors.E(errors.NotExist, errors.Str("could not find static assets")) 49 | } 50 | b, err := ioutil.ReadFile(filepath.Join(static.dir, name)) 51 | if err != nil { 52 | if os.IsNotExist(err) { 53 | return "", errors.E(errors.NotExist, err) 54 | } 55 | return "", err 56 | } 57 | return string(b), nil 58 | } 59 | -------------------------------------------------------------------------------- /cmd/upspin-ui/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | Upspin browser 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 43 | 44 | 45 | 46 |
47 | 57 |
58 |
59 |
60 | 61 | 62 | 63 |
64 |
65 |
66 |
67 | 72 | 77 |
78 | 83 | 88 |
89 | 90 |
91 |
92 | 93 | 96 | 97 | 98 |
99 | 102 | 105 |
106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 127 | 128 | 129 | 130 | 131 |
NameSizeTime
123 | 126 |
The directory is empty.
132 |
133 |
134 | 135 | 136 | 137 | 156 | 157 | 158 | 159 | 192 | 193 | 194 | 195 | 230 | 231 | 232 | 233 | 261 | 262 | 263 | 264 | 306 | 307 | 308 | 309 | 339 | 340 | 341 | 342 | 384 | 385 | 386 | 387 | 442 | 443 | 444 | 445 | 446 | 481 | 482 | 483 | 484 | 518 | 519 | 520 | 521 | 562 | 563 | 564 | 565 | 602 | 603 | 604 | 605 | 632 | 633 | 634 | 635 | 676 | 677 | 678 | 679 | 716 | 717 | 718 | 719 | 742 | 743 | 744 | 745 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | -------------------------------------------------------------------------------- /cmd/upspin-ui/static/makestatic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the https://golang.org/LICENSE file. 4 | 5 | // +build ignore 6 | 7 | // This file is adapted from golang.org/x/tools/godoc/static/makestatic.go. 8 | 9 | // Command makestatic reads a set of files and writes a Go source file to 10 | // "static.go" that declares a map of string constants containing contents of 11 | // the input files. It is intended to be invoked via "go generate". 12 | package main 13 | 14 | import ( 15 | "bytes" 16 | "fmt" 17 | "go/format" 18 | "io/ioutil" 19 | "os" 20 | "time" 21 | "unicode/utf8" 22 | ) 23 | 24 | var files = []string{ 25 | "augie.png", 26 | "favicon-32x32.png", 27 | "index.html", 28 | "script.js", 29 | "third_party/bootstrap/css/bootstrap-theme.min.css", 30 | "third_party/bootstrap/css/bootstrap-theme.min.css.map", 31 | "third_party/bootstrap/css/bootstrap.min.css", 32 | "third_party/bootstrap/css/bootstrap.min.css.map", 33 | "third_party/bootstrap/fonts/glyphicons-halflings-regular.eot", 34 | "third_party/bootstrap/fonts/glyphicons-halflings-regular.svg", 35 | "third_party/bootstrap/fonts/glyphicons-halflings-regular.ttf", 36 | "third_party/bootstrap/fonts/glyphicons-halflings-regular.woff", 37 | "third_party/bootstrap/fonts/glyphicons-halflings-regular.woff2", 38 | "third_party/bootstrap/js/bootstrap.min.js", 39 | "third_party/jquery/jquery.min.js", 40 | "third_party/ladda/ladda-themeless.min.css", 41 | "third_party/ladda/ladda.min.js", 42 | "third_party/ladda/spin.min.js", 43 | } 44 | 45 | func main() { 46 | if err := makestatic(); err != nil { 47 | fmt.Fprintln(os.Stderr, err) 48 | os.Exit(1) 49 | } 50 | } 51 | 52 | func makestatic() error { 53 | f, err := os.Create("static.go") 54 | if err != nil { 55 | return err 56 | } 57 | defer f.Close() 58 | buf := new(bytes.Buffer) 59 | fmt.Fprintf(buf, "%v\n\n%v\n\npackage static\n\n", license, warning) 60 | fmt.Fprintf(buf, "func init() { files = map[string]string{\n") 61 | for _, fn := range files { 62 | b, err := ioutil.ReadFile(fn) 63 | if err != nil { 64 | return err 65 | } 66 | fmt.Fprintf(buf, "\t%q: ", fn) 67 | if utf8.Valid(b) { 68 | fmt.Fprintf(buf, "`%s`", sanitize(b)) 69 | } else { 70 | fmt.Fprintf(buf, "%q", b) 71 | } 72 | fmt.Fprintln(buf, ",\n") 73 | } 74 | fmt.Fprintln(buf, "}}") 75 | fmtbuf, err := format.Source(buf.Bytes()) 76 | if err != nil { 77 | return err 78 | } 79 | return ioutil.WriteFile("static.go", fmtbuf, 0666) 80 | } 81 | 82 | // sanitize prepares a valid UTF-8 string as a raw string constant. 83 | func sanitize(b []byte) []byte { 84 | // Replace ` with `+"`"+` 85 | b = bytes.Replace(b, []byte("`"), []byte("`+\"`\"+`"), -1) 86 | 87 | // Replace BOM with `+"\xEF\xBB\xBF"+` 88 | // (A BOM is valid UTF-8 but not permitted in Go source files. 89 | // I wouldn't bother handling this, but for some insane reason 90 | // jquery.js has a BOM somewhere in the middle.) 91 | return bytes.Replace(b, []byte("\xEF\xBB\xBF"), []byte("`+\"\\xEF\\xBB\\xBF\"+`"), -1) 92 | } 93 | 94 | const warning = `// Code generated by "makestatic"; DO NOT EDIT.` 95 | 96 | var license = fmt.Sprintf(`// Copyright %d The Upspin Authors. All rights reserved. 97 | // Use of this source code is governed by a BSD-style 98 | // license that can be found in the LICENSE file.`, time.Now().UTC().Year()) 99 | -------------------------------------------------------------------------------- /cmd/upspin-ui/static/script.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Upspin Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // FormatEntryTime returns the Time for the given DirEntry as a string. 6 | function FormatEntryTime(entry) { 7 | if (!entry.Time) { 8 | return "-"; 9 | } 10 | // TODO(adg): better date formatting. 11 | return (new Date(entry.Time*1000)).toLocaleString(); 12 | } 13 | 14 | // FormatEntrySize returns the computed size of the given entry as a string. 15 | function FormatEntrySize(entry) { 16 | if (!entry.Blocks) { 17 | return "-"; 18 | } 19 | var size = 0; 20 | for (var j=0; j").text(paths[i])); 85 | } 86 | 87 | if (dest) { 88 | el.find(".up-dest-message").show(); 89 | el.find(".up-dest").text(dest); 90 | } else { 91 | el.find(".up-dest-message").hide(); 92 | } 93 | 94 | el.modal("show"); 95 | } 96 | 97 | // Mkdir displays a modal that prompts the user for a directory to create. 98 | // The basePath is the path to pre-fill in the input box. 99 | // The mkdir argument is a function that creates a directory and takes 100 | // the path name as its single argument. 101 | function Mkdir(basePath, mkdir) { 102 | var el = $("#mMkdir"); 103 | var input = el.find(".up-path").val(basePath); 104 | 105 | el.find(".up-mkdir-button").off("click").click(function() { 106 | el.modal("hide"); 107 | mkdir(input.val()); 108 | }); 109 | 110 | el.modal("show").on("shown.bs.modal", function() { 111 | input.focus(); 112 | }); 113 | } 114 | 115 | // Browser instantiates an Upspin tree browser and appends it to parentEl. 116 | function Browser(parentEl, page) { 117 | var browser = { 118 | path: "", 119 | entries: [], 120 | navigate: navigate, 121 | refresh: refresh, 122 | reportError: reportError 123 | }; 124 | 125 | var el = $("body > .up-template.up-browser").clone().removeClass("up-template"); 126 | el.appendTo(parentEl); 127 | 128 | var firstNav = true; 129 | function navigate(path) { 130 | browser.path = path; 131 | drawPath(); 132 | drawLoading("Loading directory..."); 133 | page.list(path, function(entries) { 134 | var isOwnRoot = path == page.username()+"/"; 135 | var noEntries = !entries || entries.length == 0; 136 | if (firstNav && isOwnRoot && noEntries) { 137 | $("#mWelcome").modal("show"); 138 | } 139 | firstNav = false; 140 | drawEntries(entries); 141 | }, function(error) { 142 | reportError(error); 143 | }); 144 | } 145 | 146 | function refresh() { 147 | navigate(browser.path); 148 | } 149 | 150 | el.on("dragover", function(e) { 151 | e.preventDefault(); 152 | el.addClass("drag"); 153 | }); 154 | el.on("dragleave", function(e) { 155 | e.preventDefault(); 156 | el.removeClass("drag"); 157 | }); 158 | el.on("drop", function(e) { 159 | e.preventDefault(); 160 | el.removeClass("drag"); 161 | 162 | if (!e.originalEvent.dataTransfer || e.originalEvent.dataTransfer.files.length == 0) { 163 | return; 164 | } 165 | 166 | drawLoading("Uploading files..."); 167 | 168 | var files = e.originalEvent.dataTransfer.files; 169 | page.put(browser.path, files, function() { 170 | inputs.attr("disabled", false); 171 | refresh(); 172 | }, function(err) { 173 | inputs.attr("disabled", false); 174 | reportError(err); 175 | }); 176 | }); 177 | 178 | el.find(".up-delete").click(function() { 179 | var paths = checkedPaths(); 180 | if (paths.length == 0) { 181 | return; 182 | } 183 | Confirm("delete", paths, null, function() { 184 | page.rm(paths, function() { 185 | refresh(); 186 | }, function(err) { 187 | reportError(err); 188 | // Refresh the pane because entries may have 189 | // been deleted even if an error occurred. 190 | refresh(); 191 | }); 192 | }); 193 | }); 194 | 195 | el.find(".up-copy").click(function() { 196 | var paths = checkedPaths(); 197 | if (paths.length == 0) { 198 | return; 199 | } 200 | var dest = page.copyDestination(); 201 | Confirm("copy", paths, dest, function() { 202 | page.copy(paths, dest, function() { 203 | page.refreshDestination(); 204 | }, function(error) { 205 | reportError(error); 206 | // Refresh the destination pane as files may 207 | // have been copied even if an error occurred. 208 | page.refreshDestination(); 209 | }); 210 | }); 211 | }); 212 | 213 | el.find(".up-refresh").click(function() { 214 | refresh(); 215 | }); 216 | 217 | el.find(".up-mkdir").click(function() { 218 | Mkdir(browser.path+"/", function(path) { 219 | page.mkdir(path, function() { 220 | refresh(); 221 | }, function(error) { 222 | reportError(error); 223 | }); 224 | }); 225 | }); 226 | 227 | el.find(".up-select-all").on("change", function() { 228 | var checked = $(this).is(":checked"); 229 | el.find(".up-entry").not(".up-template").find(".up-entry-select").each(function() { 230 | $(this).prop("checked", checked); 231 | }); 232 | }); 233 | 234 | function checkedPaths() { 235 | var paths = []; 236 | el.find(".up-entry").not(".up-template").each(function() { 237 | var checked = $(this).find(".up-entry-select").is(":checked"); 238 | if (checked) { 239 | paths.push($(this).data("up-entry").Name); 240 | } 241 | }); 242 | return paths; 243 | } 244 | 245 | function atRoot() { 246 | var p = browser.path; 247 | var i = p.indexOf("/"); 248 | return i == -1 || i == p.length-1; 249 | } 250 | 251 | var parentEl = el.find(".up-parent").click(function() { 252 | if (atRoot()) return; 253 | 254 | var p = browser.path; 255 | var i = p.lastIndexOf("/"); 256 | navigate(p.slice(0, i)); 257 | }); 258 | 259 | var pathEl = el.find(".up-path").change(function() { 260 | navigate($(this).val()); 261 | }); 262 | 263 | function drawPath() { 264 | var p = browser.path; 265 | pathEl.val(p); 266 | 267 | var i = p.indexOf("/") 268 | parentEl.prop("disabled", atRoot()); 269 | } 270 | 271 | var loadingEl = el.find(".up-loading"), 272 | errorEl = el.find(".up-error"), 273 | entriesEl = el.find(".up-entries"), 274 | inputs = el.find("button, input"); 275 | 276 | function drawLoading(text) { 277 | inputs.attr("disabled", true); 278 | loadingEl.show().text(text); 279 | errorEl.hide(); 280 | entriesEl.hide(); 281 | } 282 | 283 | function reportError(err) { 284 | inputs.attr("disabled", false); 285 | loadingEl.hide(); 286 | errorEl.show().text(err); 287 | } 288 | 289 | function drawEntries(entries) { 290 | entries = entries || []; 291 | 292 | inputs.attr("disabled", false); 293 | loadingEl.hide(); 294 | errorEl.hide(); 295 | entriesEl.show(); 296 | 297 | el.find(".up-select-all").prop("checked", false); 298 | 299 | var tmpl = el.find(".up-template.up-entry"); 300 | var parent = tmpl.parent(); 301 | parent.children().filter(".up-entry").not(tmpl).remove(); 302 | for (var i=0; i") 332 | .text(shortName) 333 | .attr("href", "/" + name + "?token=" + entry.FileToken) 334 | .attr("target", "_blank") 335 | .appendTo(nameEl); 336 | } 337 | 338 | var sizeEl = entryEl.find(".up-entry-size"); 339 | if (isDir) { 340 | sizeEl.text("-"); 341 | } else{ 342 | sizeEl.text(FormatEntrySize(entry)); 343 | } 344 | 345 | entryEl.find(".up-entry-time").text(FormatEntryTime(entry)); 346 | 347 | var inspectEl = entryEl.find(".up-entry-inspect"); 348 | inspectEl.data("up-entry", entry); 349 | inspectEl.click(function() { 350 | Inspect($(this).closest(".up-entry").data("up-entry")); 351 | }); 352 | 353 | parent.append(entryEl); 354 | } 355 | var emptyEl = parent.find(".up-empty"); 356 | if (entries.length == 0) { 357 | emptyEl.show(); 358 | } else { 359 | emptyEl.hide(); 360 | } 361 | } 362 | 363 | return browser; 364 | } 365 | 366 | // Startup manages the signup process and fetches the name of the logged-in 367 | // user and the XSRF token for making subsequent requests. 368 | function Startup(xhr, doneCallback) { 369 | 370 | $("#mSignup").find("button").click(function() { 371 | action({ 372 | action: "signup", 373 | username: $("#signupUserName").val(), 374 | }); 375 | }); 376 | 377 | $("#mSecretSeed").find("button").click(function() { 378 | action(); 379 | }); 380 | 381 | $("#mVerify").find("button.up-resend").click(function() { 382 | action({action: "register"}); 383 | }); 384 | $("#mVerify").find("button.up-proceed").click(function() { 385 | action(); 386 | }); 387 | 388 | $("#mServerSelect").find("button").click(function() { 389 | switch (true) { 390 | case $("#serverSelectExisting").is(":checked"): 391 | show({Step: "serverExisting"}); 392 | break; 393 | case $("#serverSelectGCP").is(":checked"): 394 | show({Step: "serverGCP"}); 395 | break; 396 | case $("#serverSelectNone").is(":checked"): 397 | action({action: "specifyNoEndpoints"}); 398 | break; 399 | } 400 | }); 401 | 402 | $("#mServerExisting").find("button").click(function() { 403 | action({ 404 | action: "specifyEndpoints", 405 | dirServer: $("#serverExistingDirServer").val(), 406 | storeServer: $("#serverExistingStoreServer").val() 407 | }); 408 | }); 409 | 410 | $("#mServerGCP").find("button").click(function() { 411 | var fileEl = $("#serverGCPKeyFile"); 412 | if (fileEl[0].files.length != 1) { 413 | error("You must provide a JSON Private Key file."); 414 | return; 415 | } 416 | var r = new FileReader(); 417 | r.onerror = function() { 418 | error("An error occurred uploading the file."); 419 | }; 420 | r.onload = function(state) { 421 | action({ 422 | action: "specifyGCP", 423 | privateKeyData: r.result 424 | }); 425 | }; 426 | r.readAsText(fileEl[0].files[0]); 427 | }); 428 | 429 | $("#mGCPDetails").find("button").click(function() { 430 | action({ 431 | action: "createGCP", 432 | bucketName: $("#gcpDetailsBucketName").val(), 433 | bucketLoc: $("#gcpDetailsBucketLoc").val(), 434 | regionZone: $("#gcpDetailsRegionZone").val() 435 | }); 436 | }); 437 | 438 | $("#mServerUserName").find("button").click(function() { 439 | action({ 440 | action: "configureServerUserName", 441 | userNameSuffix: $("#serverUserNameSuffix").val() 442 | }); 443 | }); 444 | 445 | $("#mServerSecretSeed").find("button").click(function() { 446 | // Performing an empty action will bounce the user to the next 447 | // screen, serverHostName, with the server IP address populated 448 | // by the server side. 449 | action({}); 450 | }); 451 | 452 | $("#mServerHostName").find("button").click(function() { 453 | action({ 454 | action: "configureServerHostName", 455 | hostName: $("#serverHostName").val() 456 | }); 457 | }); 458 | 459 | $("#mWaitServerHostName").find("button.btn-primary").click(function() { 460 | action({action: "checkServerHostName"}); 461 | }); 462 | $("#mWaitServerHostName").find("button.btn-danger").click(function() { 463 | action({ 464 | action: "checkServerHostName", 465 | reset: "true" 466 | }); 467 | }); 468 | 469 | $("#mServerWriters").find("button").click(function() { 470 | action({ 471 | action: "configureServer", 472 | writers: $("#serverWriters").val() 473 | }); 474 | }); 475 | 476 | var step; // String representation of the current step. 477 | var el; // jQuery element of the current step's modal. 478 | function show(data) { 479 | // If we've moved onto another step, hide the previous one. 480 | if (el && data.Step != step) { 481 | el.modal("hide"); 482 | } 483 | 484 | // Set el and step and do step-specific setup. 485 | switch (data.Step) { 486 | case "loading": 487 | el = $("#mLoading"); 488 | break; 489 | case "signup": 490 | el = $("#mSignup"); 491 | break; 492 | case "secretSeed": 493 | el = $("#mSecretSeed"); 494 | $("#secretSeedKeyDir").text(data.KeyDir); 495 | $("#secretSeedSecretSeed").text(data.SecretSeed); 496 | break; 497 | case "verify": 498 | el = $("#mVerify"); 499 | el.find(".up-username").text(data.UserName); 500 | break; 501 | case "serverSelect": 502 | el = $("#mServerSelect"); 503 | break; 504 | case "serverExisting": 505 | el = $("#mServerExisting"); 506 | break; 507 | case "serverGCP": 508 | el = $("#mServerGCP"); 509 | break; 510 | case "gcpDetails": 511 | el = $("#mGCPDetails"); 512 | 513 | $("#gcpDetailsBucketName").val(data.BucketName); 514 | 515 | var locs = $("#gcpDetailsBucketLoc").empty(); 516 | for (var i=0; i < data.Locations.length; i++) { 517 | var loc = data.Locations[i]; 518 | var label = loc; 519 | if (loc.indexOf("-") >= 0) { 520 | label += " (Regional)"; 521 | } else { 522 | label += " (Multi-regional)"; 523 | } 524 | var opt = $("