From 948c9d923312dcd7fed63cf8d44b743ad559a872 Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Thu, 12 May 2016 13:16:17 -0700 Subject: [PATCH] vendor: Switch from godep to glide tool * Use glide for dependency management * https://github.com/coreos/docs/issues/775 --- Documentation/dev/develop.md | 19 +- glide.lock | 95 + glide.yaml | 77 + .../camlistore/camlistore/.gitignore | 34 + .../camlistore/.hackfests/2010-12-01.txt | 23 + .../camlistore/.hackfests/2012-11-03.txt | 4 + .../camlistore/.hackfests/2012-12-23.txt | 1 + .../camlistore/.hackfests/2013-01-20.txt | 5 + .../camlistore/.hackfests/2013-12-27.txt | 8 + .../github.com/camlistore/camlistore/.header | 20 + .../github.com/camlistore/camlistore/AUTHORS | 65 + .../github.com/camlistore/camlistore/BUILDING | 12 + .../camlistore/camlistore/CONTRIBUTORS | 93 + .../LICENSE => camlistore/camlistore/COPYING} | 0 .../camlistore/camlistore/Dockerfile | 56 + .../github.com/camlistore/camlistore/HACKING | 110 + .../github.com/camlistore/camlistore/Makefile | 36 + .../github.com/camlistore/camlistore/README | 23 + vendor/github.com/camlistore/camlistore/TESTS | 34 + vendor/github.com/camlistore/camlistore/TODO | 253 + .../camlistore/camlistore/app/hello/main.go | 97 + .../camlistore/app/publisher/fileembed.go | 32 + .../camlistore/app/publisher/gallery.html | 53 + .../camlistore/app/publisher/main.go | 1008 + .../camlistore/app/publisher/pics.css | 112 + .../camlistore/app/publisher/publish_test.go | 234 + .../camlistore/app/publisher/zip.go | 308 + .../camlistore/clients/android/.classpath | 9 + .../camlistore/clients/android/.gitignore | 8 + .../camlistore/clients/android/.project | 33 + .../.settings/org.eclipse.jdt.core.prefs | 301 + .../.settings/org.eclipse.jdt.ui.prefs | 60 + .../clients/android/AndroidManifest.xml | 74 + .../camlistore/clients/android/Makefile | 17 + .../clients/android/build-in-docker.pl | 64 + .../clients/android/build.properties | 2 + .../camlistore/clients/android/build.xml | 72 + .../clients/android/check-environment.pl | 14 + .../clients/android/default.properties | 12 + .../clients/android/devenv/Dockerfile | 38 + .../clients/android/local.properties.TEMPLATE | 3 + .../clients/android/project.properties | 14 + .../android/res/drawable-hdpi/icon.png | Bin 0 -> 10518 bytes .../android/res/drawable-mdpi/icon.png | Bin 0 -> 7395 bytes .../android/res/drawable-xhdpi/icon.png | Bin 0 -> 13842 bytes .../android/res/drawable/icon_file.png | Bin 0 -> 640 bytes .../android/res/drawable/icon_folder.png | Bin 0 -> 1397 bytes .../clients/android/res/layout/main.xml | 77 + .../clients/android/res/values/strings.xml | 39 + .../clients/android/res/xml/preferences.xml | 83 + .../integration/android/IntentIntegrator.java | 506 + .../integration/android/IntentResult.java | 95 + .../src/org/camlistore/CamliActivity.java | 341 + .../src/org/camlistore/CamliFileObserver.java | 65 + .../src/org/camlistore/DummyNullCallback.java | 56 + .../android/src/org/camlistore/HostPort.java | 103 + .../src/org/camlistore/IStatusCallback.aidl | 30 + .../src/org/camlistore/IUploadService.aidl | 49 + .../src/org/camlistore/OnAlarmReceiver.java | 32 + .../src/org/camlistore/OnBootReceiver.java | 43 + .../src/org/camlistore/Preferences.java | 114 + .../src/org/camlistore/QRPreference.java | 31 + .../src/org/camlistore/QueuedFile.java | 84 + .../src/org/camlistore/SettingsActivity.java | 355 + .../src/org/camlistore/UploadApplication.java | 128 + .../src/org/camlistore/UploadService.java | 846 + .../src/org/camlistore/UploadThread.java | 409 + .../android/src/org/camlistore/Util.java | 149 + .../src/org/camlistore/WifiPowerReceiver.java | 90 + .../clients/android/test/.classpath | 10 + .../camlistore/clients/android/test/.project | 34 + .../test/.settings/org.eclipse.jdt.core.prefs | 4 + .../clients/android/test/AndroidManifest.xml | 12 + .../clients/android/test/build.properties | 21 + .../camlistore/clients/android/test/build.xml | 73 + .../clients/android/test/default.properties | 11 + .../clients/android/test/project.properties | 14 + .../android/test/res/drawable-hdpi/icon.png | Bin 0 -> 4147 bytes .../android/test/res/drawable-ldpi/icon.png | Bin 0 -> 1723 bytes .../android/test/res/drawable-mdpi/icon.png | Bin 0 -> 2574 bytes .../clients/android/test/res/layout/main.xml | 12 + .../android/test/res/values/strings.xml | 5 + .../src/org/camlistore/CamliActivityTest.java | 32 + .../clients/chrome/clip-it-good/Crypto.js | 183 + .../clients/chrome/clip-it-good/LICENSE | 13 + .../clients/chrome/clip-it-good/SHA1.js | 109 + .../chrome/clip-it-good/background.html | 256 + .../clients/chrome/clip-it-good/base64.js | 206 + .../chrome/clip-it-good/chrome_ex_oauth.html | 27 + .../chrome/clip-it-good/chrome_ex_oauth.js | 593 + .../clip-it-good/chrome_ex_oauthsimple.js | 458 + .../clients/chrome/clip-it-good/icon128.png | Bin 0 -> 3461 bytes .../clients/chrome/clip-it-good/icon19.png | Bin 0 -> 519 bytes .../clients/chrome/clip-it-good/icon48.png | Bin 0 -> 1229 bytes .../ui-bg_diagonals-thick_18_b81900_40x40.png | Bin 0 -> 384 bytes .../ui-bg_diagonals-thick_20_666666_40x40.png | Bin 0 -> 251 bytes .../images/ui-bg_flat_10_000000_40x100.png | Bin 0 -> 178 bytes .../images/ui-bg_glass_100_f6f6f6_1x400.png | Bin 0 -> 104 bytes .../images/ui-bg_glass_100_fdf5ce_1x400.png | Bin 0 -> 125 bytes .../images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 105 bytes .../ui-bg_gloss-wave_35_f6a828_500x100.png | Bin 0 -> 3762 bytes .../ui-bg_highlight-soft_100_eeeeee_1x100.png | Bin 0 -> 90 bytes .../ui-bg_highlight-soft_75_ffe45c_1x100.png | Bin 0 -> 129 bytes .../images/ui-icons_222222_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_228ef1_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_ef8c08_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_ffd27a_256x240.png | Bin 0 -> 5355 bytes .../images/ui-icons_ffffff_256x240.png | Bin 0 -> 4369 bytes .../chrome/clip-it-good/jquery-1.4.2.min.js | 154 + .../clip-it-good/jquery-ui-1.8.5.custom.css | 572 + .../jquery-ui-1.8.5.custom.min.js | 778 + .../clients/chrome/clip-it-good/json2.js | 481 + .../clients/chrome/clip-it-good/manifest.json | 25 + .../clients/chrome/clip-it-good/options.html | 314 + .../camlistore/clients/curl/example.sh | 42 + .../camlistore/clients/curl/test_data.txt | 1 + .../camlistore/clients/curl/upload-file.pl | 21 + .../camlistore/clients/ios-objc/.gitignore | 1 + .../camlistore/clients/ios-objc/Podfile | 5 + .../camlistore/clients/ios-objc/Podfile.lock | 20 + .../camlistore/clients/ios-objc/Readme.md | 5 + .../photobackup.xcodeproj/project.pbxproj | 617 + .../contents.xcworkspacedata | 1 + .../xcshareddata/photobackup.xccheckout | 41 + .../Base.lproj/Main_iPad.storyboard | 56 + .../Base.lproj/Main_iPhone.storyboard | 193 + .../AppIcon.appiconset/AppIcon29x29@2x.png | Bin 0 -> 12617 bytes .../AppIcon.appiconset/AppIcon40x40@2x.png | Bin 0 -> 17204 bytes .../AppIcon.appiconset/AppIcon60x60@2x.png | Bin 0 -> 25848 bytes .../AppIcon.appiconset/Contents.json | 56 + .../LaunchImage.launchimage/Contents.json | 53 + .../LaunchImage.launchimage/startup-r4.png | Bin 0 -> 271181 bytes .../LaunchImage.launchimage/startup-small.png | Bin 0 -> 229326 bytes .../ios-objc/photobackup/LAAppDelegate.h | 31 + .../ios-objc/photobackup/LAAppDelegate.m | 156 + .../photobackup/LACamliClient/LACamliClient.h | 50 + .../photobackup/LACamliClient/LACamliClient.m | 339 + .../photobackup/LACamliClient/LACamliFile.h | 29 + .../photobackup/LACamliClient/LACamliFile.m | 163 + .../LACamliClient/LACamliUploadOperation.h | 29 + .../LACamliClient/LACamliUploadOperation.m | 337 + .../photobackup/LACamliClient/LACamliUtil.h | 23 + .../photobackup/LACamliClient/LACamliUtil.m | 174 + .../ios-objc/photobackup/LAViewController.h | 22 + .../ios-objc/photobackup/LAViewController.m | 207 + .../photobackup/SettingsViewController.h | 23 + .../photobackup/SettingsViewController.m | 122 + .../ios-objc/photobackup/UploadStatusCell.h | 16 + .../ios-objc/photobackup/UploadStatusCell.m | 31 + .../ios-objc/photobackup/UploadTaskCell.h | 17 + .../ios-objc/photobackup/UploadTaskCell.m | 31 + .../photobackup/en.lproj/InfoPlist.strings | 2 + .../clients/ios-objc/photobackup/main.m | 18 + .../photobackup/photobackup-Info.plist | 49 + .../photobackup/photobackup-Prefix.pch | 23 + .../en.lproj/InfoPlist.strings | 2 + .../photobackupTests-Info.plist | 22 + .../photobackupTests/photobackupTests.m | 34 + .../camlistore/camlistore/clients/js/README | 6 + .../camlistore/clients/js/camel.jpg | Bin 0 -> 19234 bytes .../camlistore/clients/js/client.js | 38 + .../camlistore/clients/js/index.html | 61 + .../camlistore/clients/js/style.css | 27 + .../camlistore/clients/osx/.gitignore | 4 + .../clients/osx/Camlistore/BUILDING | 8 + .../Camlistore.xcodeproj/project.pbxproj | 591 + .../contents.xcworkspacedata | 7 + .../osx/Camlistore/Camlistore/AppDelegate.h | 78 + .../osx/Camlistore/Camlistore/AppDelegate.m | 382 + .../Camlistore/Base.lproj/MainMenu.xib | 106 + .../osx/Camlistore/Camlistore/Camlicon.icns | Bin 0 -> 63675 bytes .../Camlistore/Camlistore-Info.plist | 42 + .../Camlistore/Camlistore-Prefix.pch | 9 + .../osx/Camlistore/Camlistore/Credits.html | 26 + .../osx/Camlistore/Camlistore/FUSEManager.h | 47 + .../osx/Camlistore/Camlistore/FUSEManager.m | 234 + .../AppIcon.appiconset/Contents.json | 58 + .../Camlistore/Camlistore/LoginItemManager.h | 31 + .../Camlistore/Camlistore/LoginItemManager.m | 89 + .../Camlistore/TimeTravelWindowController.h | 28 + .../Camlistore/TimeTravelWindowController.m | 61 + .../Camlistore/TimeTravelWindowController.xib | 122 + .../Camlistore/en.lproj/InfoPlist.strings | 1 + .../clients/osx/Camlistore/Camlistore/main.m | 22 + .../osx/Camlistore/Camlistore/make-dmg.sh | 20 + .../Camlistore/menuicon-selected.png | Bin 0 -> 4234 bytes .../Camlistore/menuicon-selected@2x.png | Bin 0 -> 5231 bytes .../osx/Camlistore/Camlistore/menuicon.png | Bin 0 -> 4603 bytes .../osx/Camlistore/Camlistore/menuicon@2x.png | Bin 0 -> 6314 bytes .../CamlistoreTests-Info.plist | 22 + .../CamlistoreTests/CamlistoreTests.m | 34 + .../en.lproj/InfoPlist.strings | 1 + .../camlistore/clients/python/camliclient.py | 183 + .../camlistore/cmd/camdeploy/camdeploy.go | 27 + .../camlistore/cmd/camdeploy/gce.go | 155 + .../camlistore/cmd/camget/.gitignore | 2 + .../camlistore/cmd/camget/camget.go | 418 + .../camlistore/camlistore/cmd/camget/doc.go | 39 + .../camlistore/camlistore/cmd/camget/graph.go | 156 + .../camlistore/cmd/cammount/.gitignore | 2 + .../camlistore/cmd/cammount/cammount.go | 251 + .../camlistore/cmd/cammount/cammount_other.go | 27 + .../camlistore/camlistore/cmd/cammount/doc.go | 30 + .../camlistore/cmd/camput/.gitignore | 5 + .../camlistore/cmd/camput/androidx.go | 42 + .../camlistore/camlistore/cmd/camput/attr.go | 103 + .../camlistore/camlistore/cmd/camput/blobs.go | 157 + .../camlistore/camlistore/cmd/camput/cache.go | 50 + .../camlistore/cmd/camput/camput.go | 187 + .../camlistore/cmd/camput/camput_test.go | 247 + .../camlistore/cmd/camput/delete.go | 70 + .../camlistore/cmd/camput/discard.go | 48 + .../camlistore/camlistore/cmd/camput/doc.go | 74 + .../camlistore/camlistore/cmd/camput/files.go | 1198 + .../camlistore/camlistore/cmd/camput/init.go | 259 + .../camlistore/cmd/camput/kvcache.go | 398 + .../camlistore/cmd/camput/logging.go | 42 + .../camlistore/cmd/camput/permanode.go | 106 + .../camlistore/cmd/camput/rawobj.go | 82 + .../camlistore/cmd/camput/remove.go | 56 + .../camlistore/camlistore/cmd/camput/share.go | 93 + .../camlistore/cmd/camput/stat_darwin.go | 18 + .../camlistore/cmd/camput/stat_linux.go | 22 + .../camlistore/cmd/camput/uploader.go | 95 + .../camlistore/cmd/camtool/.gitignore | 1 + .../camlistore/cmd/camtool/camtool.go | 53 + .../camlistore/cmd/camtool/claims.go | 79 + .../camlistore/cmd/camtool/dbinit.go | 300 + .../camlistore/cmd/camtool/debug.go | 82 + .../camlistore/cmd/camtool/describe.go | 88 + .../camlistore/cmd/camtool/disco.go | 63 + .../camlistore/camlistore/cmd/camtool/doc.go | 57 + .../camlistore/cmd/camtool/dp_idx_rebuild.go | 128 + .../camlistore/cmd/camtool/dumpconfig.go | 67 + .../camlistore/camlistore/cmd/camtool/env.go | 77 + .../camlistore/camlistore/cmd/camtool/exif.go | 49 + .../camlistore/cmd/camtool/googinit.go | 131 + .../camlistore/cmd/camtool/index.go | 86 + .../camlistore/camlistore/cmd/camtool/list.go | 166 + .../camlistore/cmd/camtool/makestatic.go | 131 + .../camlistore/camlistore/cmd/camtool/mime.go | 34 + .../camlistore/cmd/camtool/packblobs.go | 107 + .../camlistore/cmd/camtool/search.go | 116 + .../camlistore/cmd/camtool/searchdoc.go | 83 + .../camlistore/cmd/camtool/splits.go | 96 + .../camlistore/cmd/camtool/sqlite_cond.go | 27 + .../camlistore/camlistore/cmd/camtool/sync.go | 456 + .../camlistore/cmd/camtool/sync_test.go | 42 + .../config/dev-blobserver-config.json | 18 + .../dev-client-dir-demo/client-config.json | 12 + .../config/dev-client-dir/client-config.json | 12 + .../camlistore/config/dev-indexer-config.json | 18 + .../camlistore/config/dev-server-config.json | 362 + .../camlistore/depcheck/depcheck.go | 19 + .../camlistore/depcheck/min_go_version.go | 28 + .../camlistore/camlistore/dev/camfix.pl | 23 + .../dev/config-dir-local/client-config.json | 12 + .../camlistore/camlistore/dev/demo.sh | 18 + .../camlistore/camlistore/dev/dev-db | 3 + .../camlistore/dev/devcam/appengine.go | 120 + .../camlistore/dev/devcam/camget.go | 118 + .../camlistore/dev/devcam/cammount.go | 127 + .../camlistore/dev/devcam/camput.go | 105 + .../camlistore/dev/devcam/camtool.go | 77 + .../camlistore/dev/devcam/devcam.go | 284 + .../camlistore/camlistore/dev/devcam/doc.go | 47 + .../camlistore/camlistore/dev/devcam/env.go | 168 + .../camlistore/camlistore/dev/devcam/exec.go | 27 + .../camlistore/camlistore/dev/devcam/hook.go | 305 + .../camlistore/dev/devcam/review.go | 126 + .../camlistore/dev/devcam/server.go | 553 + .../camlistore/camlistore/dev/devcam/test.go | 169 + .../camlistore/dev/envvardoc/envvardoc.go | 189 + .../camlistore/camlistore/dev/local.sh | 7 + .../camlistore/camlistore/dev/make-release | 45 + .../github.com/camlistore/camlistore/dev/push | 7 + .../camlistore/dev/update_closure_compiler.go | 139 + .../camlistore/doc/app-environment.txt | 31 + .../camlistore/camlistore/doc/blog-notes.txt | 40 + .../camlistore/doc/environment-vars.txt | 215 + .../camlistore/doc/example-blobs/README.txt | 2 + ...8357ea2ee9dd7031d0ff786840e6deac8b7a6a.dat | 5 + ...8357ea2ee9dd7031d0ff786840e6deac8b7a6a.txt | 1 + ...da9d771661563a27704b91b67989e7ea1e50b8.dat | 30 + ...da9d771661563a27704b91b67989e7ea1e50b8.txt | 2 + .../doc/json-signing/example/public-key.txt | 30 + .../json-signing/example/signing-after.camli | 4 + .../example/signing-before-J.camli | 4 + .../json-signing/example/signing-before.camli | 3 + .../example/signing-before.camli.detachsig | 11 + .../doc/json-signing/example/some-notes.txt | 8 + .../json-signing/example/some-notes.txt.camli | 9 + .../doc/json-signing/example/test-keyring.gpg | Bin 0 -> 1196 bytes .../doc/json-signing/example/test-secring.gpg | Bin 0 -> 2498 bytes .../doc/json-signing/json-signing.txt | 166 + .../camlistore/camlistore/doc/overview.txt | 161 + .../doc/protocol/blob-enumerate-protocol.txt | 74 + .../doc/protocol/blob-get-protocol.txt | 46 + .../doc/protocol/blob-stat-protocol.txt | 83 + .../doc/protocol/blob-upload-protocol.txt | 122 + .../doc/protocol/blob-upload-resume.txt | 36 + .../camlistore/doc/protocol/discovery.txt | 62 + .../camlistore/doc/publishing/README | 16 + .../camlistore/doc/schema/blob-magic.txt | 26 + .../camlistore/doc/schema/bytes.txt | 38 + .../camlistore/doc/schema/claims/TODO | 90 + .../doc/schema/claims/attributes.txt | 80 + .../camlistore/doc/schema/claims/delete.txt | 13 + .../camlistore/doc/schema/claims/share.txt | 31 + .../camlistore/doc/schema/files/directory.txt | 13 + .../camlistore/doc/schema/files/fifo.txt | 9 + .../doc/schema/files/file-common.txt | 24 + .../camlistore/doc/schema/files/file.txt | 15 + .../camlistore/doc/schema/files/inode.txt | 15 + .../camlistore/doc/schema/files/socket.txt | 9 + .../camlistore/doc/schema/files/symlink.txt | 18 + .../camlistore/doc/schema/objects/keep.txt | 16 + .../doc/schema/objects/permanode.txt | 15 + .../doc/schema/objects/static-set.txt | 23 + .../camlistore/camlistore/doc/search-ui.txt | 51 + .../camlistore/camlistore/doc/terminology.txt | 4 + .../internal/chanworker/chanworker.go | 120 + .../camlistore/lib/python/camli/__init__.py | 0 .../camlistore/lib/python/camli/op.py | 390 + .../camlistore/lib/python/camli/schema.py | 337 + .../lib/python/camli/schema_test.py | 74 + .../camlistore/lib/python/fusepy/__init__.py | 8 + .../camlistore/lib/python/fusepy/context.py | 61 + .../camlistore/lib/python/fusepy/fuse.py | 650 + .../camlistore/lib/python/fusepy/fuse24.py | 669 + .../camlistore/lib/python/fusepy/fuse3.py | 637 + .../camlistore/lib/python/fusepy/fusell.py | 619 + .../camlistore/lib/python/fusepy/loopback.py | 98 + .../lib/python/fusepy/low-level/.project | 17 + .../lib/python/fusepy/low-level/.pydevproject | 10 + .../lib/python/fusepy/low-level/README.txt | 22 + .../lib/python/fusepy/low-level/ctypeslib.zip | Bin 0 -> 135190 bytes .../lib/python/fusepy/low-level/fuse_ctypes.h | 10 + .../fusepy/low-level/llfuse/__init__.py | 24 + .../fusepy/low-level/llfuse/interface.py | 897 + .../fusepy/low-level/llfuse/operations.py | 348 + .../python/fusepy/low-level/llfuse_example.py | 115 + .../lib/python/fusepy/low-level/setup.py | 127 + .../camlistore/lib/python/fusepy/memory.py | 123 + .../camlistore/lib/python/fusepy/memory3.py | 128 + .../camlistore/lib/python/fusepy/memoryll.py | 142 + .../camlistore/lib/python/fusepy/sftp.py | 106 + .../camlistore/camlistore/lib/python/setup.py | 16 + .../lib/python/simplejson/__init__.py | 437 + .../lib/python/simplejson/decoder.py | 421 + .../lib/python/simplejson/encoder.py | 501 + .../lib/python/simplejson/ordered_dict.py | 119 + .../lib/python/simplejson/scanner.py | 77 + .../camlistore/lib/python/simplejson/tool.py | 39 + .../github.com/camlistore/camlistore/make.go | 909 + .../camlistore/misc/buildbot/README | 25 + .../misc/buildbot/builder/builder.go | 1120 + .../misc/buildbot/builder/builder_test.go | 128 + .../misc/buildbot/master/bot_test.go | 41 + .../camlistore/misc/buildbot/master/master.go | 1292 ++ .../camlistore/misc/commit-msg.githook | 104 + .../camlistore/camlistore/misc/copyrightifity | 68 + .../camlistore/camlistore/misc/devlib.pl | 47 + .../misc/docker/djpeg-static/Dockerfile | 20 + .../misc/docker/djpeg-static/Makefile | 5 + .../camlistore/camlistore/misc/docker/dock.go | 460 + .../camlistore/misc/docker/go/Dockerfile | 12 + .../camlistore/misc/docker/mysql/Dockerfile | 14 + .../camlistore/misc/docker/mysql/Makefile | 6 + .../camlistore/misc/docker/mysql/run-mysqld | 57 + .../camlistore/misc/docker/release/.gitignore | 1 + .../misc/docker/release/build-binaries.go | 177 + .../camlistore/misc/docker/server/Dockerfile | 13 + .../docker/server/build-camlistore-server.go | 191 + .../camlistore/camlistore/misc/gitversion | 6 + .../camlistore/misc/old-devscripts/README | 6 + .../misc/old-devscripts/dev-camwebdav | 3 + .../misc/old-devscripts/dev-indexer | 43 + .../misc/old-devscripts/dev-synctoindexer | 4 + .../camlistore/misc/release-history-tags | 8 + .../camlistore/camlistore/misc/review | 21 + .../camlistore/camlistore/misc/testfile | 7 + .../camlistore/camlistore/old/README | 1 + .../camlistore/old/camwebdav/main.go | 233 + .../camlistore/old/camwebdav/response.go | 140 + .../camlistore/old/camwebdav/xml.go | 81 + .../camlistore/camlistore/pkg/.gitignore | 1 + .../camlistore/camlistore/pkg/app/app.go | 63 + .../camlistore/camlistore/pkg/app/app_test.go | 132 + .../camlistore/camlistore/pkg/auth/auth.go | 410 + .../camlistore/pkg/auth/auth_test.go | 95 + .../camlistore/camlistore/pkg/blob/blob.go | 142 + .../camlistore/pkg/blob/chanpeek.go | 87 + .../camlistore/camlistore/pkg/blob/fetcher.go | 175 + .../camlistore/camlistore/pkg/blob/ref.go | 634 + .../camlistore/pkg/blob/ref_test.go | 285 + .../pkg/blobserver/archiver/archiver.go | 180 + .../pkg/blobserver/archiver/archiver_test.go | 195 + .../camlistore/pkg/blobserver/blobhub.go | 198 + .../pkg/blobserver/blobhub_appengine.go | 26 + .../camlistore/pkg/blobserver/blobhub_test.go | 102 + .../pkg/blobserver/blobpacked/blobpacked.go | 1161 + .../blobserver/blobpacked/blobpacked_test.go | 818 + .../pkg/blobserver/blobpacked/stream.go | 126 + .../pkg/blobserver/blobpacked/stream_test.go | 162 + .../pkg/blobserver/blobpacked/subfetch.go | 82 + .../blobserver/blobpacked/subfetch_test.go | 59 + .../pkg/blobserver/blobpacked/wholefetch.go | 205 + .../camlistore/pkg/blobserver/cond/cond.go | 206 + .../pkg/blobserver/cond/cond_test.go | 97 + .../camlistore/pkg/blobserver/dir/dir.go | 42 + .../pkg/blobserver/diskpacked/dele.go | 111 + .../pkg/blobserver/diskpacked/diskpacked.go | 757 + .../blobserver/diskpacked/diskpacked_test.go | 298 + .../pkg/blobserver/diskpacked/punch_linux.go | 46 + .../pkg/blobserver/diskpacked/reindex.go | 251 + .../pkg/blobserver/diskpacked/reindex_test.go | 68 + .../pkg/blobserver/diskpacked/stream_test.go | 224 + .../diskpacked/testdata/pack-00000.blobs | 1 + .../diskpacked/testdata/pack-00001.blobs | 1 + .../camlistore/pkg/blobserver/doc.go | 18 + .../pkg/blobserver/encrypt/encrypt.go | 608 + .../pkg/blobserver/encrypt/encrypt_test.go | 106 + .../camlistore/pkg/blobserver/enumerate.go | 77 + .../pkg/blobserver/gethandler/get.go | 135 + .../pkg/blobserver/gethandler/get_test.go | 146 + .../blobserver/google/cloudstorage/.gitignore | 1 + .../google/cloudstorage/cloudstorage_test.go | 192 + .../blobserver/google/cloudstorage/storage.go | 255 + .../pkg/blobserver/google/drive/drive.go | 76 + .../pkg/blobserver/google/drive/drive_test.go | 130 + .../pkg/blobserver/google/drive/enumerate.go | 33 + .../pkg/blobserver/google/drive/fetch.go | 27 + .../pkg/blobserver/google/drive/receive.go | 31 + .../pkg/blobserver/google/drive/remove.go | 31 + .../google/drive/service/service.go | 173 + .../pkg/blobserver/google/drive/stat.go | 33 + .../camlistore/pkg/blobserver/handlers/doc.go | 19 + .../pkg/blobserver/handlers/enumerate.go | 136 + .../pkg/blobserver/handlers/enumerate_test.go | 65 + .../camlistore/pkg/blobserver/handlers/get.go | 29 + .../pkg/blobserver/handlers/remove.go | 90 + .../pkg/blobserver/handlers/stat.go | 144 + .../pkg/blobserver/handlers/upload.go | 257 + .../camlistore/pkg/blobserver/interface.go | 267 + .../pkg/blobserver/local/generation.go | 105 + .../pkg/blobserver/localdisk/enumerate.go | 199 + .../blobserver/localdisk/enumerate_test.go | 176 + .../pkg/blobserver/localdisk/generation.go | 38 + .../pkg/blobserver/localdisk/localdisk.go | 189 + .../blobserver/localdisk/localdisk_test.go | 198 + .../pkg/blobserver/localdisk/path.go | 41 + .../pkg/blobserver/localdisk/path_test.go | 37 + .../pkg/blobserver/localdisk/receive.go | 90 + .../pkg/blobserver/localdisk/receive_posix.go | 23 + .../blobserver/localdisk/receive_windows.go | 54 + .../pkg/blobserver/localdisk/stat.go | 62 + .../pkg/blobserver/localdisk/upgrade32.go | 146 + .../camlistore/pkg/blobserver/memory/mem.go | 311 + .../pkg/blobserver/memory/mem_test.go | 63 + .../camlistore/pkg/blobserver/mergedenum.go | 101 + .../pkg/blobserver/mergedenum_test.go | 144 + .../pkg/blobserver/mongo/enumerate.go | 58 + .../camlistore/pkg/blobserver/mongo/fetch.go | 39 + .../camlistore/pkg/blobserver/mongo/mongo.go | 134 + .../pkg/blobserver/mongo/mongo_test.go | 47 + .../pkg/blobserver/mongo/receive.go | 51 + .../camlistore/pkg/blobserver/mongo/remove.go | 46 + .../camlistore/pkg/blobserver/mongo/stat.go | 47 + .../camlistore/pkg/blobserver/multistream.go | 76 + .../pkg/blobserver/multistream_test.go | 86 + .../camlistore/pkg/blobserver/namespace/ns.go | 170 + .../pkg/blobserver/namespace/ns_test.go | 110 + .../camlistore/pkg/blobserver/noimpl.go | 53 + .../pkg/blobserver/protocol/protocol.go | 58 + .../pkg/blobserver/protocol/protocol_test.go | 47 + .../pkg/blobserver/proxycache/proxycache.go | 195 + .../camlistore/pkg/blobserver/receive.go | 79 + .../camlistore/pkg/blobserver/receive_test.go | 70 + .../camlistore/pkg/blobserver/registry.go | 141 + .../pkg/blobserver/remote/remote.go | 132 + .../pkg/blobserver/replica/replica.go | 338 + .../pkg/blobserver/replica/replica_test.go | 103 + .../camlistore/pkg/blobserver/s3/enumerate.go | 78 + .../camlistore/pkg/blobserver/s3/fetch.go | 40 + .../camlistore/pkg/blobserver/s3/receive.go | 51 + .../camlistore/pkg/blobserver/s3/remove.go | 42 + .../camlistore/pkg/blobserver/s3/s3.go | 136 + .../camlistore/pkg/blobserver/s3/s3_test.go | 88 + .../camlistore/pkg/blobserver/s3/stat.go | 53 + .../camlistore/pkg/blobserver/shard/shard.go | 127 + .../pkg/blobserver/stats/statreceiver.go | 92 + .../pkg/blobserver/storagetest/storagetest.go | 553 + .../camlistore/pkg/blobserver/sync.go | 72 + .../camlistore/pkg/blobserver/sync_test.go | 95 + .../camlistore/pkg/buildinfo/buildinfo.go | 61 + .../pkg/buildinfo/buildinfo_test.go | 44 + .../camlistore/pkg/buildinfo/testinglinked.go | 27 + .../camlistore/pkg/cacher/cacher.go | 123 + .../camlistore/pkg/camerrors/errors.go | 28 + .../camlistore/pkg/client/android/androidx.go | 381 + .../pkg/client/android/androidx_fake.go | 39 + .../pkg/client/android/androidx_real.go | 32 + .../camlistore/pkg/client/client.go | 1095 + .../camlistore/pkg/client/config.go | 580 + .../camlistore/pkg/client/config_test.go | 63 + .../camlistore/pkg/client/enumerate.go | 180 + .../camlistore/camlistore/pkg/client/get.go | 187 + .../camlistore/pkg/client/ignored_test.go | 152 + .../camlistore/pkg/client/remove.go | 96 + .../camlistore/pkg/client/stat_test.go | 68 + .../camlistore/camlistore/pkg/client/stats.go | 44 + .../camlistore/pkg/client/transport_test.go | 175 + .../camlistore/pkg/client/upload.go | 521 + .../camlistore/pkg/cmdmain/cmdmain.go | 304 + .../camlistore/pkg/cmdmain/cmdmain_go12.go | 30 + .../camlistore/pkg/constants/constants.go | 32 + .../camlistore/pkg/constants/google/google.go | 25 + .../camlistore/pkg/context/context.go | 137 + .../camlistore/camlistore/pkg/conv/conv.go | 73 + .../camlistore/pkg/conv/conv_test.go | 74 + .../pkg/deploy/gce/cloud-config.yaml | 19 + .../camlistore/pkg/deploy/gce/debug/main.go | 69 + .../camlistore/pkg/deploy/gce/deploy.go | 770 + .../camlistore/pkg/deploy/gce/handler.go | 1103 + .../camlistore/pkg/deploy/gce/notes.txt | 31 + .../camlistore/camlistore/pkg/env/env.go | 69 + .../camlistore/camlistore/pkg/fault/fault.go | 59 + .../camlistore/pkg/fileembed/fileembed.go | 331 + .../fileembed/genfileembed/genfileembed.go | 372 + .../camlistore/camlistore/pkg/fs/at.go | 113 + .../camlistore/camlistore/pkg/fs/debug.go | 139 + .../camlistore/camlistore/pkg/fs/fs.go | 424 + .../camlistore/camlistore/pkg/fs/fs_test.go | 710 + .../camlistore/camlistore/pkg/fs/mut.go | 898 + .../camlistore/camlistore/pkg/fs/mut_test.go | 49 + .../camlistore/camlistore/pkg/fs/recent.go | 156 + .../camlistore/camlistore/pkg/fs/ro.go | 383 + .../camlistore/camlistore/pkg/fs/root.go | 121 + .../camlistore/camlistore/pkg/fs/roots.go | 337 + .../camlistore/camlistore/pkg/fs/time.go | 151 + .../camlistore/camlistore/pkg/fs/time_test.go | 165 + .../camlistore/camlistore/pkg/fs/util.go | 53 + .../camlistore/camlistore/pkg/fs/xattr.go | 162 + .../camlistore/camlistore/pkg/fs/z_test.go | 34 + .../camlistore/camlistore/pkg/gc/gc.go | 198 + .../camlistore/camlistore/pkg/gc/gc_test.go | 183 + .../camlistore/pkg/geocode/geocode.go | 116 + .../camlistore/pkg/geocode/geocode_test.go | 238 + .../camlistore/pkg/googlestorage/README | 41 + .../pkg/googlestorage/googlestorage.go | 327 + .../pkg/googlestorage/googlestorage_test.go | 245 + .../pkg/googlestorage/testdata/test-enum | 1 + .../pkg/googlestorage/testdata/test-enum-1 | 1 + .../pkg/googlestorage/testdata/test-enum-2 | 1 + .../pkg/googlestorage/testdata/test-enum-3 | 1 + .../pkg/googlestorage/testdata/test-enum-4 | 1 + .../pkg/googlestorage/testdata/test-get | 1 + .../pkg/googlestorage/testdata/test-stat | 1 + .../camlistore/pkg/hashutil/hashutil.go | 40 + .../camlistore/pkg/httputil/auth.go | 101 + .../camlistore/pkg/httputil/auth_test.go | 182 + .../camlistore/pkg/httputil/certs.go | 5383 +++++ .../camlistore/pkg/httputil/certs_test.go | 23 + .../camlistore/pkg/httputil/faketransport.go | 110 + .../camlistore/pkg/httputil/httputil.go | 337 + .../camlistore/pkg/httputil/httputil_test.go | 61 + .../camlistore/pkg/httputil/transport.go | 97 + .../camlistore/pkg/images/bench_test.go | 136 + .../pkg/images/benchfastjpeg_test.go | 138 + .../pkg/images/fastjpeg/fastjpeg.go | 230 + .../pkg/images/fastjpeg/fastjpeg_test.go | 214 + .../pkg/images/fastjpeg/testdata/djpeg | 2 + .../camlistore/pkg/images/images.go | 564 + .../camlistore/pkg/images/images_test.go | 374 + .../pkg/images/resize/bench_test.go | 63 + .../camlistore/pkg/images/resize/resize.go | 320 + .../pkg/images/resize/resize_test.go | 366 + .../testdata/test-resample-128x128-64x64.png | Bin 0 -> 437 bytes .../testdata/test-resample-768x576-128x96.png | Bin 0 -> 1838 bytes .../pkg/images/resize/testdata/test.png | Bin 0 -> 34427 bytes .../pkg/images/testdata/f1-exif.jpg | Bin 0 -> 992 bytes .../camlistore/pkg/images/testdata/f1-s.jpg | Bin 0 -> 960 bytes .../camlistore/pkg/images/testdata/f1.jpg | Bin 0 -> 770 bytes .../pkg/images/testdata/f2-exif.jpg | Bin 0 -> 994 bytes .../camlistore/pkg/images/testdata/f2.jpg | Bin 0 -> 772 bytes .../pkg/images/testdata/f3-exif.jpg | Bin 0 -> 992 bytes .../camlistore/pkg/images/testdata/f3.jpg | Bin 0 -> 770 bytes .../pkg/images/testdata/f4-exif.jpg | Bin 0 -> 994 bytes .../camlistore/pkg/images/testdata/f4.jpg | Bin 0 -> 772 bytes .../pkg/images/testdata/f5-exif.jpg | Bin 0 -> 980 bytes .../camlistore/pkg/images/testdata/f5.jpg | Bin 0 -> 758 bytes .../pkg/images/testdata/f6-exif.jpg | Bin 0 -> 982 bytes .../camlistore/pkg/images/testdata/f6.jpg | Bin 0 -> 760 bytes .../pkg/images/testdata/f7-exif.jpg | Bin 0 -> 980 bytes .../camlistore/pkg/images/testdata/f7.jpg | Bin 0 -> 758 bytes .../pkg/images/testdata/f8-exif.jpg | Bin 0 -> 982 bytes .../camlistore/pkg/images/testdata/f8.jpg | Bin 0 -> 760 bytes .../camlistore/camlistore/pkg/importer/README | 10 + .../pkg/importer/allimporters/importers.go | 28 + .../camlistore/pkg/importer/attrs.go | 74 + .../camlistore/pkg/importer/dummy/dummy.go | 198 + .../camlistore/pkg/importer/feed/atom/atom.go | 61 + .../camlistore/pkg/importer/feed/feed.go | 282 + .../camlistore/pkg/importer/feed/parse.go | 508 + .../camlistore/pkg/importer/feed/rdf/rdf.go | 47 + .../camlistore/pkg/importer/feed/rss/rss.go | 69 + .../camlistore/pkg/importer/flickr/README | 20 + .../camlistore/pkg/importer/flickr/flickr.go | 586 + .../pkg/importer/flickr/flickr_test.go | 254 + .../pkg/importer/flickr/testdata.go | 288 + .../camlistore/pkg/importer/foursquare/README | 19 + .../camlistore/pkg/importer/foursquare/api.go | 108 + .../pkg/importer/foursquare/foursquare.go | 520 + .../importer/foursquare/foursquare_test.go | 47 + .../pkg/importer/foursquare/testdata.go | 269 + .../foursquare/testdata/users-me-res.json | 791 + .../camlistore/pkg/importer/html.go | 211 + .../camlistore/pkg/importer/importer.go | 1335 ++ .../camlistore/pkg/importer/importer_test.go | 66 + .../camlistore/pkg/importer/noop.go | 57 + .../camlistore/pkg/importer/oauth.go | 223 + .../camlistore/pkg/importer/picasa/README | 43 + .../pkg/importer/picasa/oa2_importers.go | 218 + .../camlistore/pkg/importer/picasa/picasa.go | 468 + .../pkg/importer/picasa/picasa_test.go | 61 + .../pkg/importer/picasa/testdata.go | 239 + .../importer/picasa/testdata/users-me-res.xml | 69 + .../pkg/importer/pinboard/pinboard.go | 346 + .../pkg/importer/pinboard/pinboard_test.go | 196 + .../pinboard/testdata/batchresponse.json | 35 + .../camlistore/pkg/importer/twitter/README | 6 + .../pkg/importer/twitter/testdata.go | 267 + .../testdata/verify_credentials-res.json | 9 + .../pkg/importer/twitter/twitter.go | 886 + .../pkg/importer/twitter/twitter_test.go | 50 + .../camlistore/camlistore/pkg/index/corpus.go | 1262 ++ .../camlistore/pkg/index/corpus_bench_test.go | 63 + .../camlistore/pkg/index/corpus_test.go | 485 + .../camlistore/camlistore/pkg/index/doc.go | 46 + .../camlistore/pkg/index/enumstat.go | 96 + .../camlistore/pkg/index/export_test.go | 121 + .../camlistore/camlistore/pkg/index/index.go | 1516 ++ .../camlistore/pkg/index/index_test.go | 500 + .../pkg/index/indextest/testdata/0s.mp3 | Bin 0 -> 1393 bytes .../index/indextest/testdata/dude-exif.jpg | Bin 0 -> 2174 bytes .../pkg/index/indextest/testdata/dude.jpg | Bin 0 -> 1932 bytes .../camlistore/pkg/index/indextest/tests.go | 1378 ++ .../camlistore/pkg/index/interface.go | 143 + .../camlistore/camlistore/pkg/index/keys.go | 396 + .../camlistore/pkg/index/keys_test.go | 44 + .../camlistore/pkg/index/kvfile_test.go | 104 + .../camlistore/pkg/index/memindex.go | 55 + .../camlistore/pkg/index/mongo_test.go | 74 + .../camlistore/pkg/index/mysql_test.go | 76 + .../camlistore/pkg/index/postgres_test.go | 77 + .../camlistore/pkg/index/receive.go | 826 + .../camlistore/pkg/index/reversetime.go | 51 + .../camlistore/camlistore/pkg/index/sniff.go | 106 + .../camlistore/pkg/index/sqlindex/sqlindex.go | 250 + .../camlistore/pkg/index/sqlite/sqlite.go | 3 + .../pkg/index/sqlite/sqlite_test.go | 185 + .../camlistore/camlistore/pkg/index/util.go | 44 + .../camlistore/pkg/jsonconfig/eval.go | 292 + .../camlistore/pkg/jsonconfig/jsonconfig.go | 296 + .../pkg/jsonconfig/jsonconfig_test.go | 104 + .../pkg/jsonconfig/testdata/boolenv.json | 11 + .../pkg/jsonconfig/testdata/include1.json | 3 + .../pkg/jsonconfig/testdata/include2.json | 3 + .../pkg/jsonconfig/testdata/listexpand.json | 4 + .../pkg/jsonconfig/testdata/loop1.json | 3 + .../pkg/jsonconfig/testdata/loop2.json | 3 + .../camlistore/camlistore/pkg/jsonsign/doc.go | 19 + .../camlistore/pkg/jsonsign/jsonsign_test.go | 222 + .../camlistore/pkg/jsonsign/keys.go | 220 + .../camlistore/pkg/jsonsign/sign.go | 219 + .../camlistore/pkg/jsonsign/sign_appengine.go | 29 + .../camlistore/pkg/jsonsign/sign_normal.go | 86 + .../pkg/jsonsign/signhandler/sig.go | 289 + .../testdata/password-foo-keyring.gpg | Bin 0 -> 1187 bytes .../testdata/password-foo-secring.gpg | Bin 0 -> 2565 bytes .../pkg/jsonsign/testdata/test-keyring.gpg | Bin 0 -> 1196 bytes .../pkg/jsonsign/testdata/test-keyring2.gpg | Bin 0 -> 1202 bytes .../pkg/jsonsign/testdata/test-secring.gpg | Bin 0 -> 2498 bytes .../pkg/jsonsign/testdata/test-secring2.gpg | Bin 0 -> 2504 bytes .../camlistore/pkg/jsonsign/verify.go | 239 + .../camlistore/pkg/kvutil/kvutil.go | 64 + .../camlistore/camlistore/pkg/leak/leak.go | 74 + .../camlistore/pkg/leak/leak_test.go | 74 + .../camlistore/camlistore/pkg/legal/legal.go | 50 + .../camlistore/pkg/legal/legal_test.go | 39 + .../pkg/legal/legalprint/legalprint.go | 42 + .../camlistore/camlistore/pkg/lru/cache.go | 109 + .../camlistore/pkg/lru/cache_test.go | 71 + .../camlistore/camlistore/pkg/magic/magic.go | 118 + .../camlistore/pkg/magic/magic_test.go | 95 + .../camlistore/pkg/magic/testdata/foo.tar | Bin 0 -> 10240 bytes .../camlistore/pkg/magic/testdata/foo.tar.gz | Bin 0 -> 163 bytes .../camlistore/pkg/magic/testdata/foo.tar.xz | Bin 0 -> 208 bytes .../camlistore/pkg/magic/testdata/foo.tbz2 | Bin 0 -> 165 bytes .../camlistore/pkg/magic/testdata/foo.zip | Bin 0 -> 300 bytes .../camlistore/pkg/magic/testdata/magic.pdf | Bin 0 -> 21668 bytes .../camlistore/pkg/magic/testdata/smile.bmp | Bin 0 -> 7654 bytes .../camlistore/pkg/magic/testdata/smile.gif | Bin 0 -> 927 bytes .../camlistore/pkg/magic/testdata/smile.ico | Bin 0 -> 4086 bytes .../camlistore/pkg/magic/testdata/smile.jpg | Bin 0 -> 1198 bytes .../camlistore/pkg/magic/testdata/smile.png | Bin 0 -> 1221 bytes .../camlistore/pkg/magic/testdata/smile.psd | Bin 0 -> 5236 bytes .../camlistore/pkg/magic/testdata/smile.tiff | Bin 0 -> 1502 bytes .../camlistore/pkg/magic/testdata/smile.xcf | Bin 0 -> 2951 bytes .../camlistore/camlistore/pkg/media/audio.go | 196 + .../camlistore/pkg/media/audio_test.go | 89 + .../camlistore/pkg/media/testdata/128_cbr.mp3 | Bin 0 -> 4368 bytes .../camlistore/pkg/media/testdata/id3v1.mp3 | Bin 0 -> 4913 bytes .../pkg/media/testdata/xing_header.mp3 | Bin 0 -> 4785 bytes .../camlistore/pkg/misc/amazon/s3/auth.go | 206 + .../pkg/misc/amazon/s3/auth_test.go | 139 + .../camlistore/pkg/misc/amazon/s3/client.go | 445 + .../pkg/misc/amazon/s3/client_test.go | 95 + .../closure/genclosuredeps/genclosuredeps.go | 51 + .../camlistore/pkg/misc/closure/gendeps.go | 224 + .../pkg/misc/closure/gendeps_test.go | 79 + .../pkg/misc/closure/jstest/jstest.go | 133 + .../camlistore/pkg/misc/gpgagent/gpgagent.go | 177 + .../pkg/misc/gpgagent/gpgagent_test.go | 81 + .../camlistore/pkg/misc/pinentry/pinentry.go | 147 + .../camlistore/pkg/netutil/ident.go | 275 + .../camlistore/pkg/netutil/ident_test.go | 248 + .../camlistore/pkg/netutil/netutil.go | 123 + .../camlistore/pkg/netutil/netutil_test.go | 181 + .../camlistore/pkg/oauthutil/oauth.go | 121 + .../camlistore/camlistore/pkg/osutil/cpu.go | 30 + .../camlistore/pkg/osutil/cpu_freebsd.go | 32 + .../camlistore/pkg/osutil/cpu_linux.go | 34 + .../pkg/osutil/findproc_appengine.go | 29 + .../camlistore/pkg/osutil/findproc_normal.go | 43 + .../camlistore/pkg/osutil/gce/gce.go | 98 + .../camlistore/camlistore/pkg/osutil/mem.go | 28 + .../camlistore/pkg/osutil/mem_unix.go | 39 + .../camlistore/pkg/osutil/openurl.go | 34 + .../camlistore/pkg/osutil/osutil.go | 36 + .../camlistore/camlistore/pkg/osutil/paths.go | 283 + .../camlistore/pkg/osutil/paths_test.go | 121 + .../camlistore/pkg/osutil/restart_freebsd.go | 51 + .../camlistore/pkg/osutil/restart_stub.go | 39 + .../camlistore/pkg/osutil/restart_unix.go | 68 + .../camlistore/pkg/osutil/restart_windows.go | 54 + .../pkg/osutil/syscall_appengine.go | 22 + .../camlistore/pkg/osutil/syscall_posix.go | 52 + .../camlistore/pkg/osutil/syscall_solaris.go | 53 + .../camlistore/pkg/osutil/syscall_windows.go | 20 + .../camlistore/camlistore/pkg/pools/pools.go | 41 + .../camlistore/pkg/publish/types.go | 89 + .../pkg/readerutil/countingreader.go | 32 + .../camlistore/pkg/readerutil/opener.go | 125 + .../camlistore/pkg/readerutil/opener_test.go | 77 + .../camlistore/pkg/readerutil/readersize.go | 52 + .../pkg/readerutil/readersize_test.go | 68 + .../camlistore/pkg/rollsum/rollsum.go | 81 + .../camlistore/pkg/rollsum/rollsum_test.go | 79 + .../camlistore/pkg/schema/.gitignore | 3 + .../camlistore/camlistore/pkg/schema/blob.go | 589 + .../camlistore/pkg/schema/dirreader.go | 164 + .../camlistore/pkg/schema/fileread_test.go | 453 + .../camlistore/pkg/schema/filereader.go | 395 + .../camlistore/pkg/schema/filewriter.go | 469 + .../camlistore/pkg/schema/filewriter_test.go | 169 + .../camlistore/pkg/schema/lookup.go | 171 + .../pkg/schema/nodeattr/nodeattr.go | 106 + .../camlistore/pkg/schema/schema.go | 1056 + .../camlistore/pkg/schema/schema_darwin.go | 28 + .../camlistore/pkg/schema/schema_linux.go | 28 + .../camlistore/pkg/schema/schema_posix.go | 28 + .../pkg/schema/schema_public_test.go | 57 + .../camlistore/pkg/schema/schema_test.go | 705 + .../camlistore/camlistore/pkg/schema/sign.go | 130 + .../camlistore/pkg/schema/sign_test.go | 51 + .../pkg/schema/testdata/coffee-sf.jpg | Bin 0 -> 28083 bytes .../pkg/schema/testdata/gocon-tokyo.jpg | Bin 0 -> 27493 bytes .../camlistore/pkg/search/describe.go | 884 + .../camlistore/pkg/search/describe_test.go | 253 + .../camlistore/pkg/search/export_test.go | 31 + .../camlistore/camlistore/pkg/search/expr.go | 362 + .../camlistore/pkg/search/expr_test.go | 988 + .../camlistore/pkg/search/handler.go | 811 + .../camlistore/pkg/search/handler_test.go | 760 + .../camlistore/camlistore/pkg/search/lexer.go | 315 + .../camlistore/pkg/search/lexer_test.go | 191 + .../camlistore/pkg/search/match_test.go | 75 + .../camlistore/pkg/search/predicate.go | 689 + .../camlistore/pkg/search/predicate_test.go | 669 + .../camlistore/camlistore/pkg/search/query.go | 1616 ++ .../camlistore/pkg/search/query_test.go | 1382 ++ .../camlistore/pkg/search/search.go | 29 + .../camlistore/pkg/search/websocket.go | 287 + .../camlistore/pkg/server/app/app.go | 274 + .../camlistore/pkg/server/app/app_test.go | 220 + .../camlistore/pkg/server/cgo_probe.go | 23 + .../camlistore/camlistore/pkg/server/doc.go | 19 + .../camlistore/pkg/server/download.go | 199 + .../camlistore/pkg/server/favicon.ico | Bin 0 -> 1150 bytes .../camlistore/pkg/server/fileembed.go | 35 + .../camlistore/pkg/server/filetree.go | 87 + .../camlistore/camlistore/pkg/server/help.go | 124 + .../camlistore/camlistore/pkg/server/image.go | 418 + .../camlistore/camlistore/pkg/server/root.go | 276 + .../camlistore/pkg/server/root_appengine.go | 24 + .../camlistore/pkg/server/root_normal.go | 36 + .../camlistore/camlistore/pkg/server/share.go | 250 + .../camlistore/pkg/server/share_test.go | 116 + .../camlistore/pkg/server/status.go | 297 + .../camlistore/camlistore/pkg/server/sync.go | 1013 + .../camlistore/pkg/server/thumbcache.go | 84 + .../camlistore/camlistore/pkg/server/ui.go | 658 + .../camlistore/pkg/server/uploadhelper.go | 93 + .../camlistore/pkg/server/wizard-html.go | 39 + .../camlistore/pkg/server/wizard.go | 287 + .../camlistore/pkg/serverinit/devmode.go | 106 + .../camlistore/pkg/serverinit/env.go | 95 + .../camlistore/pkg/serverinit/export_test.go | 29 + .../camlistore/pkg/serverinit/genconfig.go | 947 + .../pkg/serverinit/genconfig_test.go | 46 + .../camlistore/pkg/serverinit/serverinit.go | 714 + .../pkg/serverinit/serverinit_test.go | 478 + .../pkg/serverinit/testdata/baseurl-want.json | 118 + .../pkg/serverinit/testdata/baseurl.json | 11 + .../pkg/serverinit/testdata/baseurlbad.err | 1 + .../pkg/serverinit/testdata/baseurlbad.json | 10 + .../testdata/blobpacked_googlecloud-want.json | 156 + .../testdata/blobpacked_googlecloud.json | 17 + .../testdata/blobpacked_localdisk-want.json | 134 + .../testdata/blobpacked_localdisk.json | 11 + .../pkg/serverinit/testdata/default-want.json | 117 + .../pkg/serverinit/testdata/default.json | 10 + .../serverinit/testdata/diskpacked-want.json | 125 + .../pkg/serverinit/testdata/diskpacked.json | 11 + .../pkg/serverinit/testdata/flickr-want.json | 121 + .../pkg/serverinit/testdata/flickr.json | 11 + .../serverinit/testdata/gen_client_config.in | 9 + .../serverinit/testdata/gen_client_config.out | 14 + .../testdata/google_nolocaldisk-want.json | 117 + .../testdata/google_nolocaldisk.json | 12 + .../google_nolocaldisk_subdir-want.json | 117 + .../testdata/google_nolocaldisk_subdir.json | 12 + .../testdata/google_queues_on_db-want.json | 123 + .../testdata/google_queues_on_db.json | 14 + .../testdata/google_service_account-want.json | 117 + .../testdata/google_service_account.json | 12 + .../google_service_account_subdir-want.json | 117 + .../google_service_account_subdir.json | 12 + .../serverinit/testdata/justblobs-want.json | 52 + .../pkg/serverinit/testdata/justblobs.json | 10 + .../pkg/serverinit/testdata/leveldb-want.json | 117 + .../pkg/serverinit/testdata/leveldb.json | 10 + .../serverinit/testdata/listenbase-want.json | 117 + .../pkg/serverinit/testdata/listenbase.json | 10 + .../pkg/serverinit/testdata/mem-want.json | 180 + .../pkg/serverinit/testdata/mem.json | 16 + .../serverinit/testdata/memindex-want.json | 116 + .../pkg/serverinit/testdata/memindex.json | 10 + .../testdata/memory_storage-want.json | 106 + .../serverinit/testdata/memory_storage.json | 10 + .../pkg/serverinit/testdata/mongo-want.json | 120 + .../pkg/serverinit/testdata/mongo.json | 11 + .../testdata/multipublish-want.json | 140 + .../pkg/serverinit/testdata/multipublish.json | 23 + .../pkg/serverinit/testdata/noindex.err | 1 + .../pkg/serverinit/testdata/noindex.json | 7 + .../serverinit/testdata/s3_alt_host-want.json | 115 + .../pkg/serverinit/testdata/s3_alt_host.json | 12 + .../testdata/s3_google_nolocaldisk.err | 1 + .../testdata/s3_google_nolocaldisk.json | 13 + .../testdata/s3_nolocaldisk-want.json | 114 + .../serverinit/testdata/s3_nolocaldisk.json | 12 + .../testdata/s3_nolocaldisk_mysql-want.json | 114 + .../testdata/s3_nolocaldisk_mysql.json | 13 + .../pkg/serverinit/testdata/sqlite-want.json | 116 + .../pkg/serverinit/testdata/sqlite.json | 10 + .../testdata/thumbcache_on_db-want.json | 130 + .../serverinit/testdata/thumbcache_on_db.json | 15 + .../pkg/serverinit/testdata/tls-want.json | 131 + .../pkg/serverinit/testdata/tls.json | 21 + .../serverinit/testdata/with_blog-want.json | 128 + .../pkg/serverinit/testdata/with_blog.json | 20 + .../testdata/with_gallery-want.json | 130 + .../pkg/serverinit/testdata/with_gallery.json | 22 + .../testdata/with_sourceroot-want.json | 136 + .../serverinit/testdata/with_sourceroot.json | 14 + .../pkg/singleflight/singleflight.go | 64 + .../pkg/singleflight/singleflight_test.go | 85 + .../camlistore/pkg/sorted/buffer/buffer.go | 319 + .../pkg/sorted/buffer/buffer_test.go | 104 + .../camlistore/camlistore/pkg/sorted/kv.go | 237 + .../camlistore/pkg/sorted/kvfile/kvfile.go | 269 + .../pkg/sorted/kvfile/kvfile_test.go | 45 + .../camlistore/pkg/sorted/kvtest/kvtest.go | 173 + .../camlistore/pkg/sorted/leveldb/leveldb.go | 258 + .../pkg/sorted/leveldb/leveldb_test.go | 46 + .../camlistore/camlistore/pkg/sorted/mem.go | 176 + .../camlistore/pkg/sorted/mem_test.go | 43 + .../camlistore/pkg/sorted/mongo/mongokv.go | 280 + .../pkg/sorted/mongo/mongokv_test.go | 44 + .../camlistore/pkg/sorted/mysql/cloudsql.go | 69 + .../camlistore/pkg/sorted/mysql/dbschema.go | 47 + .../camlistore/pkg/sorted/mysql/mysqlkv.go | 211 + .../pkg/sorted/mysql/mysqlkv_test.go | 49 + .../pkg/sorted/postgres/dbschema.go | 108 + .../pkg/sorted/postgres/postgreskv.go | 143 + .../pkg/sorted/postgres/postgreskv_test.go | 47 + .../camlistore/pkg/sorted/sqlite/dbschema.go | 110 + .../pkg/sorted/sqlite/sqlite_cond.go | 11 + .../camlistore/pkg/sorted/sqlite/sqlitekv.go | 122 + .../pkg/sorted/sqlite/sqlitekv_test.go | 48 + .../camlistore/pkg/sorted/sqlkv/sqlkv.go | 296 + .../camlistore/pkg/sorted/sqlkv/sqlkv_test.go | 61 + .../camlistore/pkg/strutil/intern.go | 39 + .../camlistore/pkg/strutil/strconv.go | 117 + .../camlistore/pkg/strutil/strutil.go | 200 + .../camlistore/pkg/strutil/strutil_test.go | 230 + .../camlistore/pkg/syncutil/gate.go | 42 + .../camlistore/pkg/syncutil/group.go | 64 + .../camlistore/pkg/syncutil/lock.go | 191 + .../camlistore/pkg/syncutil/once.go | 60 + .../camlistore/pkg/syncutil/once_test.go | 57 + .../camlistore/camlistore/pkg/syncutil/sem.go | 64 + .../camlistore/pkg/syncutil/sem_test.go | 33 + .../camlistore/pkg/syncutil/syncutil_test.go | 30 + .../camlistore/pkg/test/asserts/asserts.go | 108 + .../camlistore/camlistore/pkg/test/blob.go | 93 + .../camlistore/camlistore/pkg/test/diff.go | 52 + .../camlistore/camlistore/pkg/test/doc.go | 18 + .../camlistore/pkg/test/dockertest/docker.go | 275 + .../camlistore/pkg/test/fakeindex.go | 225 + .../camlistore/camlistore/pkg/test/fetcher.go | 89 + .../camlistore/pkg/test/fetcher_test.go | 31 + .../pkg/test/integration/camget_test.go | 215 + .../pkg/test/integration/camlistore_test.go | 242 + .../pkg/test/integration/camput_test.go | 105 + .../pkg/test/integration/diskpacked_test.go | 101 + .../pkg/test/integration/integration.go | 3 + .../pkg/test/integration/non-utf8_test.go | 121 + .../pkg/test/integration/share_test.go | 84 + .../camlistore/pkg/test/integration/z_test.go | 32 + .../camlistore/camlistore/pkg/test/loader.go | 100 + .../camlistore/camlistore/pkg/test/test.go | 70 + .../camlistore/pkg/test/test_test.go | 56 + .../bench-diskpacked-server-config.json | 20 + .../bench-localdisk-server-config.json | 20 + .../pkg/test/testdata/server-config.json | 90 + .../camlistore/camlistore/pkg/test/testdep.go | 34 + .../camlistore/camlistore/pkg/test/wait.go | 33 + .../camlistore/camlistore/pkg/test/world.go | 377 + .../camlistore/pkg/throttle/throttle.go | 137 + .../camlistore/pkg/types/atomics.go | 55 + .../camlistore/pkg/types/camtypes/camtypes.go | 20 + .../pkg/types/camtypes/discovery.go | 103 + .../camlistore/pkg/types/camtypes/errors.go | 86 + .../camlistore/pkg/types/camtypes/search.go | 254 + .../pkg/types/camtypes/search_test.go | 42 + .../camlistore/pkg/types/camtypes/sign.go | 29 + .../pkg/types/camtypes/statustype.go | 22 + .../pkg/types/clientconfig/config.go | 163 + .../camlistore/pkg/types/example_test.go | 22 + .../camlistore/pkg/types/fakeseeker.go | 70 + .../camlistore/pkg/types/fakeseeker_test.go | 55 + .../pkg/types/serverconfig/config.go | 115 + .../camlistore/camlistore/pkg/types/types.go | 260 + .../camlistore/pkg/types/types_test.go | 152 + .../camlistore/pkg/video/thumbnail/handler.go | 79 + .../pkg/video/thumbnail/handler_test.go | 75 + .../camlistore/pkg/video/thumbnail/service.go | 161 + .../pkg/video/thumbnail/service_test.go | 154 + .../pkg/video/thumbnail/testdata/small.webm | Bin 0 -> 229455 bytes .../pkg/video/thumbnail/thumbnailer.go | 92 + .../camlistore/pkg/webserver/envpipe_unix.go | 38 + .../pkg/webserver/envpipe_windows.go | 28 + .../camlistore/pkg/webserver/webserver.go | 271 + .../camlistore/camlistore/pkg/wkfs/gcs/gcs.go | 204 + .../camlistore/camlistore/pkg/wkfs/wkfs.go | 132 + .../camlistore/camlistore/server/.gitignore | 1 + .../camlistore/server/appengine/README | 9 + .../camlistore/server/appengine/app.yaml | 8 + .../camlistore/server/appengine/build_test.go | 119 + .../server/appengine/camli/aeindex.go | 227 + .../server/appengine/camli/common.go | 39 + .../server/appengine/camli/contextpool.go | 115 + .../camlistore/server/appengine/camli/main.go | 108 + .../server/appengine/camli/ownerauth.go | 82 + .../server/appengine/camli/storage.go | 377 + .../camlistore/server/appengine/config.json | 96 + .../server/appengine/test-secring.gpg | 1 + .../camlistore/server/camlistored/.gitignore | 1 + .../camlistore/server/camlistored/README | 7 + .../server/camlistored/camlistored.go | 415 + .../camlistore/server/camlistored/run_test.go | 101 + .../camlistore/server/camlistored/setup.go | 51 + .../camlistore/server/camlistored/ui/TODO | 14 + .../server/camlistored/ui/animation_loop.js | 89 + .../camlistore/server/camlistored/ui/blob.js | 67 + .../server/camlistored/ui/blob_detail.js | 204 + .../server/camlistored/ui/blob_item.css | 120 + .../camlistored/ui/blob_item_container.css | 53 + .../ui/blob_item_container_react.js | 398 + .../ui/blob_item_container_test.html | 136 + .../camlistored/ui/blob_item_demo_content.js | 99 + .../camlistored/ui/blob_item_foursquare.css | 74 + .../ui/blob_item_foursquare_content.js | 139 + .../ui/blob_item_generic_content.js | 137 + .../camlistored/ui/blob_item_image_content.js | 153 + .../ui/blob_item_progress_test.html | 17 + .../server/camlistored/ui/blob_item_react.js | 92 + .../camlistored/ui/blob_item_twitter.css | 66 + .../ui/blob_item_twitter_content.js | 129 + .../server/camlistored/ui/blob_item_video.css | 53 + .../camlistored/ui/blob_item_video_content.js | 197 + .../server/camlistored/ui/blob_test.js | 105 + .../server/camlistored/ui/blobref.js | 20 + .../server/camlistored/ui/blog.html | 6 + .../camlistored/ui/cache_buster_iframe.js | 107 + .../server/camlistored/ui/checkmark2.svg | 72 + .../server/camlistored/ui/checkmark2_blue.svg | 71 + .../server/camlistored/ui/circled_plus.svg | 87 + .../server/camlistored/ui/clear.svg | 81 + .../server/camlistored/ui/close.svg | 68 + .../camlistored/ui/closure-toolbar-bg.png | Bin 0 -> 166 bytes .../server/camlistored/ui/closure/closure.go | 143 + .../server/camlistored/ui/date_utils.js | 47 + .../server/camlistored/ui/debug.html | 47 + .../server/camlistored/ui/debug_console.html | 54 + .../server/camlistored/ui/debug_console.js | 259 + .../server/camlistored/ui/detail.css | 113 + .../server/camlistored/ui/detail.js | 213 + .../server/camlistored/ui/dialog.css | 49 + .../server/camlistored/ui/dialog.js | 61 + .../server/camlistored/ui/directory_detail.js | 47 + .../camlistore/server/camlistored/ui/down.svg | 82 + .../camlistore/server/camlistored/ui/file.png | Bin 0 -> 8713 bytes .../server/camlistored/ui/fileembed.go | 35 + .../camlistored/ui/fileembed_appengine.go | 28 + .../server/camlistored/ui/fileembed_normal.go | 29 + .../server/camlistored/ui/filetree.css | 32 + .../server/camlistored/ui/filetree.html | 23 + .../server/camlistored/ui/filetree.js | 223 + .../server/camlistored/ui/folder.png | Bin 0 -> 98565 bytes .../server/camlistored/ui/foursquare-logo.png | Bin 0 -> 6303 bytes .../server/camlistored/ui/hash_worker.js | 30 + .../server/camlistored/ui/header.css | 167 + .../server/camlistored/ui/header.js | 320 + .../server/camlistored/ui/icon_16716.svg | 58 + .../server/camlistored/ui/icon_27307.svg | 68 + .../server/camlistored/ui/image_detail.js | 183 + .../server/camlistored/ui/index.css | 49 + .../server/camlistored/ui/index.html | 100 + .../camlistore/server/camlistored/ui/index.js | 1084 + .../server/camlistored/ui/js-notes.txt | 16 + .../camlistored/ui/magnifying_glass.svg | 73 + .../camlistore/server/camlistored/ui/math.js | 20 + .../server/camlistored/ui/mobile.html | 53 + .../server/camlistored/ui/mobile_setup.css | 70 + .../server/camlistored/ui/mobile_setup.js | 129 + .../server/camlistored/ui/navigator.js | 127 + .../server/camlistored/ui/navigator_test.js | 146 + .../server/camlistored/ui/new_permanode.svg | 107 + .../camlistore/server/camlistored/ui/node.png | Bin 0 -> 3770 bytes .../server/camlistored/ui/object.js | 15 + .../server/camlistored/ui/permanode.css | 51 + .../server/camlistored/ui/permanode.html | 103 + .../camlistored/ui/permanode_detail.css | 82 + .../server/camlistored/ui/permanode_detail.js | 307 + .../server/camlistored/ui/permanode_utils.js | 35 + .../camlistored/ui/permanode_utils_test.js | 52 + .../server/camlistored/ui/prefix-free.css | 389 + .../server/camlistored/ui/property_sheet.css | 54 + .../server/camlistored/ui/property_sheet.js | 56 + .../camlistored/ui/pyramid_throbber.css | 98 + .../server/camlistored/ui/pyramid_throbber.js | 46 + .../server/camlistored/ui/react_util.js | 87 + .../server/camlistored/ui/safe-no-wheel.svg | 23 + .../server/camlistored/ui/safe-wheel.svg | 28 + .../server/camlistored/ui/safe1-16.png | Bin 0 -> 449 bytes .../server/camlistored/ui/safe1-32.png | Bin 0 -> 895 bytes .../server/camlistored/ui/safe1.svg | 13 + .../server/camlistored/ui/search_session.js | 279 + .../camlistored/ui/search_session_test.js | 169 + .../camlistored/ui/server_connection.js | 633 + .../server/camlistored/ui/server_type.js | 136 + .../server/camlistored/ui/sidebar.css | 100 + .../server/camlistored/ui/sidebar.js | 147 + .../server/camlistored/ui/sigdebug.js | 128 + .../server/camlistored/ui/spinner.css | 30 + .../server/camlistored/ui/spinner.js | 90 + .../server/camlistored/ui/spinner_test.html | 45 + .../camlistored/ui/sprited_animation.js | 70 + .../server/camlistored/ui/sprited_image.js | 56 + .../camlistore/server/camlistored/ui/style.js | 87 + .../server/camlistored/ui/tags_control.css | 114 + .../server/camlistored/ui/tags_control.js | 340 + .../server/camlistored/ui/target.svg | 85 + .../server/camlistored/ui/thumber.js | 65 + .../server/camlistored/ui/thumber_test.js | 62 + .../server/camlistored/ui/trash.svg | 147 + .../server/camlistored/ui/twitter-logo.png | Bin 0 -> 3462 bytes .../server/camlistored/ui/ui_test.go | 27 + .../camlistore/server/camlistored/ui/up.svg | 82 + .../camlistored/ui/worker_message_router.js | 101 + .../server/camlistored/ui/wsdebug.html | 74 + .../server/gae-py-blobserver/README | 5 + .../server/gae-py-blobserver/app.yaml | 27 + .../server/gae-py-blobserver/config.py | 7 + .../server/gae-py-blobserver/index.yaml | 9 + .../server/gae-py-blobserver/main.py | 346 + .../server/gae-py-blobserver/static/style.css | 1 + .../server/gae-py-blobserver/test_data.txt | 1 + .../camlistore/server/sigserver/.gitignore | 4 + .../camlistore/server/sigserver/camsigd.go | 83 + .../camlistore/server/sigserver/client.pl | 50 + .../camlistore/server/sigserver/run.sh | 4 + .../camlistore/server/sigserver/sign.go | 54 + .../camlistore/server/sigserver/spec.txt | 44 + .../server/sigserver/test/00-start.t | 20 + .../server/sigserver/test/10-sign.t | 96 + .../server/sigserver/test/CamsigdTest.pm | 78 + .../camlistore/server/sigserver/test/doc.tmp | 1 + ...f3494f698aa498d5906349c0aa0a183d89a6.camli | 30 + .../camlistore/server/sigserver/test/sig.tmp | 6 + .../server/sigserver/test/test-keyring.gpg | Bin 0 -> 1196 bytes .../server/sigserver/test/test-keyring2.gpg | Bin 0 -> 1202 bytes .../server/sigserver/test/test-secring.gpg | Bin 0 -> 2498 bytes .../server/sigserver/test/test-secring2.gpg | Bin 0 -> 2504 bytes .../server/sigserver/test/test.json | 5 + .../camlistore/server/sigserver/verify.go | 64 + .../camlistore/server/tester/bs-test.pl | 430 + .../camlistore/camlistore/third_party/README | 10 + .../third_party/bazil.org/fuse/.gitattributes | 2 + .../third_party/bazil.org/fuse/.gitignore | 8 + .../third_party/bazil.org/fuse/LICENSE | 93 + .../third_party/bazil.org/fuse/README.md | 23 + .../third_party/bazil.org/fuse/debug.go | 21 + .../third_party/bazil.org/fuse/doc/.gitignore | 4 + .../third_party/bazil.org/fuse/doc/README.md | 6 + .../fuse/doc/mount-linux-error-init.seq | 32 + .../fuse/doc/mount-linux-error-init.seq.png | Bin 0 -> 29163 bytes .../bazil.org/fuse/doc/mount-linux.seq | 41 + .../bazil.org/fuse/doc/mount-linux.seq.png | Bin 0 -> 44615 bytes .../fuse/doc/mount-osx-error-init.seq | 32 + .../fuse/doc/mount-osx-error-init.seq.png | Bin 0 -> 32618 bytes .../bazil.org/fuse/doc/mount-osx.seq | 45 + .../bazil.org/fuse/doc/mount-osx.seq.png | Bin 0 -> 51408 bytes .../bazil.org/fuse/doc/mount-sequence.md | 30 + .../bazil.org/fuse/doc/writing-docs.md | 16 + .../bazil.org/fuse/error_darwin.go | 36 + .../third_party/bazil.org/fuse/error_std.go | 7 + .../bazil.org/fuse/fs/bench/bench_test.go | 267 + .../bazil.org/fuse/fs/bench/doc.go | 5 + .../bazil.org/fuse/fs/fstestutil/debug.go | 65 + .../bazil.org/fuse/fs/fstestutil/mounted.go | 113 + .../bazil.org/fuse/fs/fstestutil/mountinfo.go | 14 + .../fuse/fs/fstestutil/mountinfo_darwin.go | 41 + .../fuse/fs/fstestutil/mountinfo_linux.go | 57 + .../fuse/fs/fstestutil/record/buffer.go | 28 + .../fuse/fs/fstestutil/record/record.go | 381 + .../fuse/fs/fstestutil/record/wait.go | 54 + .../bazil.org/fuse/fs/fstestutil/testfs.go | 29 + .../third_party/bazil.org/fuse/fs/serve.go | 1317 ++ .../bazil.org/fuse/fs/serve_test.go | 1767 ++ .../third_party/bazil.org/fuse/fs/tree.go | 96 + .../third_party/bazil.org/fuse/fuse.go | 2043 ++ .../third_party/bazil.org/fuse/fuse_kernel.go | 639 + .../bazil.org/fuse/fuse_kernel_darwin.go | 86 + .../bazil.org/fuse/fuse_kernel_linux.go | 70 + .../bazil.org/fuse/fuse_kernel_std.go | 1 + .../bazil.org/fuse/fuse_kernel_test.go | 31 + .../bazil.org/fuse/fuseutil/fuseutil.go | 20 + .../bazil.org/fuse/hellofs/hello.go | 95 + .../bazil.org/fuse/mount_darwin.go | 126 + .../third_party/bazil.org/fuse/mount_linux.go | 72 + .../third_party/bazil.org/fuse/options.go | 100 + .../bazil.org/fuse/options_darwin.go | 13 + .../bazil.org/fuse/options_darwin_test.go | 27 + .../bazil.org/fuse/options_linux.go | 13 + .../bazil.org/fuse/options_test.go | 141 + .../bazil.org/fuse/syscallx/doc.go | 13 + .../bazil.org/fuse/syscallx/generate | 34 + .../bazil.org/fuse/syscallx/msync.go | 9 + .../bazil.org/fuse/syscallx/msync_386.go | 24 + .../bazil.org/fuse/syscallx/msync_amd64.go | 24 + .../bazil.org/fuse/syscallx/syscallx.go | 4 + .../bazil.org/fuse/syscallx/syscallx_std.go | 26 + .../bazil.org/fuse/syscallx/xattr_darwin.go | 38 + .../fuse/syscallx/xattr_darwin_386.go | 97 + .../fuse/syscallx/xattr_darwin_amd64.go | 97 + .../third_party/bazil.org/fuse/unmount.go | 6 + .../bazil.org/fuse/unmount_linux.go | 21 + .../third_party/bazil.org/fuse/unmount_std.go | 17 + .../third_party/closure/lib/AUTHORS | 17 + .../third_party/closure/lib/LICENSE | 176 + .../camlistore/third_party/closure/lib/README | 6 + .../lib/closure/goog/a11y/aria/aria.js | 364 + .../lib/closure/goog/a11y/aria/attributes.js | 389 + .../lib/closure/goog/a11y/aria/datatables.js | 68 + .../lib/closure/goog/a11y/aria/roles.js | 216 + .../closure/lib/closure/goog/array/array.js | 1526 ++ .../lib/closure/goog/asserts/asserts.js | 315 + .../lib/closure/goog/async/nexttick.js | 176 + .../closure/lib/closure/goog/async/run.js | 118 + .../lib/closure/goog/async/throttle.js | 191 + .../closure/lib/closure/goog/base.js | 1631 ++ .../lib/closure/goog/bootstrap/nodejs.js | 91 + .../lib/closure/goog/bootstrap/webworkers.js | 37 + .../closure/lib/closure/goog/crypt/crypt.js | 155 + .../closure/lib/closure/goog/crypt/hash.js | 62 + .../closure/lib/closure/goog/crypt/sha1.js | 276 + .../closure/lib/closure/goog/css/common.css | 41 + .../closure/lib/closure/goog/css/toolbar.css | 400 + .../closure/lib/closure/goog/debug/debug.js | 531 + .../closure/goog/debug/entrypointregistry.js | 158 + .../closure/lib/closure/goog/debug/error.js | 51 + .../lib/closure/goog/debug/errorhandler.js | 364 + .../lib/closure/goog/debug/logbuffer.js | 148 + .../closure/lib/closure/goog/debug/logger.js | 873 + .../lib/closure/goog/debug/logrecord.js | 271 + .../closure/lib/closure/goog/debug/tracer.js | 724 + .../closure/lib/closure/goog/deps.js | 956 + .../lib/closure/goog/disposable/disposable.js | 297 + .../closure/goog/disposable/idisposable.js | 45 + .../lib/closure/goog/dom/browserfeature.js | 67 + .../closure/lib/closure/goog/dom/classes.js | 227 + .../closure/lib/closure/goog/dom/classlist.js | 271 + .../closure/lib/closure/goog/dom/dom.js | 2901 +++ .../closure/lib/closure/goog/dom/nodetype.js | 48 + .../closure/lib/closure/goog/dom/tagname.js | 159 + .../closure/lib/closure/goog/dom/vendor.js | 96 + .../lib/closure/goog/events/browserevent.js | 391 + .../lib/closure/goog/events/browserfeature.js | 85 + .../closure/lib/closure/goog/events/event.js | 158 + .../lib/closure/goog/events/eventhandler.js | 443 + .../lib/closure/goog/events/eventid.js | 47 + .../closure/lib/closure/goog/events/events.js | 991 + .../lib/closure/goog/events/eventtarget.js | 398 + .../lib/closure/goog/events/eventtype.js | 213 + .../closure/goog/events/filedrophandler.js | 222 + .../lib/closure/goog/events/keycodes.js | 416 + .../lib/closure/goog/events/keyhandler.js | 556 + .../lib/closure/goog/events/listenable.js | 339 + .../lib/closure/goog/events/listener.js | 131 + .../lib/closure/goog/events/listenermap.js | 301 + .../closure/lib/closure/goog/format/format.js | 503 + .../lib/closure/goog/functions/functions.js | 311 + .../lib/closure/goog/i18n/graphemebreak.js | 214 + .../closure/lib/closure/goog/iter/iter.js | 1261 ++ .../closure/lib/closure/goog/json/json.js | 354 + .../lib/closure/goog/labs/promise/promise.js | 939 + .../lib/closure/goog/labs/promise/thenable.js | 112 + .../closure/lib/closure/goog/log/log.js | 192 + .../closure/lib/closure/goog/math/box.js | 369 + .../lib/closure/goog/math/coordinate.js | 233 + .../closure/lib/closure/goog/math/math.js | 400 + .../closure/lib/closure/goog/math/rect.js | 463 + .../closure/lib/closure/goog/math/size.js | 206 + .../closure/lib/closure/goog/net/errorcode.js | 130 + .../closure/lib/closure/goog/net/eventtype.js | 37 + .../lib/closure/goog/net/httpstatus.js | 111 + .../closure/goog/net/wrapperxmlhttpfactory.js | 71 + .../closure/lib/closure/goog/net/xhrio.js | 1222 + .../closure/lib/closure/goog/net/xhrlike.js | 124 + .../closure/lib/closure/goog/net/xmlhttp.js | 229 + .../lib/closure/goog/net/xmlhttpfactory.js | 67 + .../closure/lib/closure/goog/object/object.js | 635 + .../lib/closure/goog/reflect/reflect.js | 77 + .../closure/lib/closure/goog/string/string.js | 1413 ++ .../lib/closure/goog/structs/collection.js | 56 + .../lib/closure/goog/structs/inversionmap.js | 155 + .../closure/lib/closure/goog/structs/map.js | 445 + .../closure/lib/closure/goog/structs/set.js | 280 + .../lib/closure/goog/structs/simplepool.js | 200 + .../lib/closure/goog/structs/structs.js | 351 + .../closure/lib/closure/goog/style/style.js | 2107 ++ .../lib/closure/goog/testing/watchers.js | 46 + .../closure/lib/closure/goog/timer/timer.js | 292 + .../closure/lib/closure/goog/ui/component.js | 1318 ++ .../closure/lib/closure/goog/ui/control.js | 1387 ++ .../lib/closure/goog/ui/controlcontent.js | 28 + .../lib/closure/goog/ui/controlrenderer.js | 855 + .../closure/lib/closure/goog/ui/decorate.js | 38 + .../lib/closure/goog/ui/idgenerator.js | 48 + .../closure/lib/closure/goog/ui/registry.js | 167 + .../closure/lib/closure/goog/uri/uri.js | 1503 ++ .../closure/lib/closure/goog/uri/utils.js | 1053 + .../lib/closure/goog/useragent/useragent.js | 599 + .../third_party/closure/updatelibrary.go | 319 + .../p/go-charset/charset/big5.go | 88 + .../p/go-charset/charset/charset.go | 301 + .../p/go-charset/charset/charset_test.go | 279 + .../p/go-charset/charset/codepage.go | 133 + .../p/go-charset/charset/cp932.go | 195 + .../p/go-charset/charset/example_test.go | 36 + .../p/go-charset/charset/file.go | 40 + .../p/go-charset/charset/iconv/iconv.go | 183 + .../p/go-charset/charset/iconv/iconv_test.go | 85 + .../p/go-charset/charset/iconv/list_query.go | 77 + .../p/go-charset/charset/iconv/list_static.go | 176 + .../p/go-charset/charset/local.go | 162 + .../p/go-charset/charset/utf16.go | 110 + .../p/go-charset/charset/utf8.go | 51 + .../p/go-charset/data/data_big5.dat.go | 18 + .../p/go-charset/data/data_charsets.json.go | 18 + .../p/go-charset/data/data_cp932.dat.go | 18 + .../p/go-charset/data/data_ibm437.cp.go | 18 + .../p/go-charset/data/data_ibm850.cp.go | 18 + .../p/go-charset/data/data_ibm866.cp.go | 18 + .../p/go-charset/data/data_iso-8859-1.cp.go | 18 + .../p/go-charset/data/data_iso-8859-10.cp.go | 18 + .../p/go-charset/data/data_iso-8859-15.cp.go | 18 + .../p/go-charset/data/data_iso-8859-2.cp.go | 18 + .../p/go-charset/data/data_iso-8859-3.cp.go | 18 + .../p/go-charset/data/data_iso-8859-4.cp.go | 18 + .../p/go-charset/data/data_iso-8859-5.cp.go | 18 + .../p/go-charset/data/data_iso-8859-6.cp.go | 18 + .../p/go-charset/data/data_iso-8859-7.cp.go | 18 + .../p/go-charset/data/data_iso-8859-8.cp.go | 18 + .../p/go-charset/data/data_iso-8859-9.cp.go | 18 + .../go-charset/data/data_jisx0201kana.dat.go | 18 + .../p/go-charset/data/data_koi8-r.cp.go | 18 + .../p/go-charset/data/data_windows-1250.cp.go | 18 + .../p/go-charset/data/data_windows-1251.cp.go | 18 + .../p/go-charset/data/data_windows-1252.cp.go | 18 + .../code.google.com/p/go-charset/data/doc.go | 6 + .../code.google.com/p/go.crypto/AUTHORS | 3 + .../code.google.com/p/go.crypto/CONTRIBUTORS | 3 + .../code.google.com/p/go.crypto/README | 3 + .../p/go.crypto/bcrypt/base64.go | 35 + .../p/go.crypto/bcrypt/bcrypt.go | 282 + .../p/go.crypto/bcrypt/bcrypt_test.go | 194 + .../p/go.crypto/blowfish/block.go | 192 + .../p/go.crypto/blowfish/blowfish_test.go | 210 + .../p/go.crypto/blowfish/cipher.go | 100 + .../p/go.crypto/blowfish/const.go | 199 + .../p/go.crypto/cast5/cast5.go | 534 + .../p/go.crypto/cast5/cast5_test.go | 104 + .../p/go.crypto/codereview.cfg | 2 + .../code.google.com/p/go.crypto/md4/md4.go | 118 + .../p/go.crypto/md4/md4_test.go | 71 + .../p/go.crypto/md4/md4block.go | 89 + .../code.google.com/p/go.crypto/ocsp/ocsp.go | 191 + .../p/go.crypto/ocsp/ocsp_test.go | 107 + .../p/go.crypto/openpgp/armor/armor.go | 219 + .../p/go.crypto/openpgp/armor/armor_test.go | 95 + .../p/go.crypto/openpgp/armor/encode.go | 160 + .../p/go.crypto/openpgp/canonical_text.go | 59 + .../go.crypto/openpgp/canonical_text_test.go | 52 + .../go.crypto/openpgp/clearsign/clearsign.go | 362 + .../openpgp/clearsign/clearsign_test.go | 161 + .../p/go.crypto/openpgp/elgamal/elgamal.go | 122 + .../go.crypto/openpgp/elgamal/elgamal_test.go | 49 + .../p/go.crypto/openpgp/errors/errors.go | 64 + .../p/go.crypto/openpgp/keys.go | 563 + .../p/go.crypto/openpgp/keys_test.go | 41 + .../p/go.crypto/openpgp/packet/compressed.go | 123 + .../openpgp/packet/compressed_test.go | 41 + .../p/go.crypto/openpgp/packet/config.go | 70 + .../go.crypto/openpgp/packet/encrypted_key.go | 168 + .../openpgp/packet/encrypted_key_test.go | 125 + .../p/go.crypto/openpgp/packet/literal.go | 89 + .../p/go.crypto/openpgp/packet/ocfb.go | 143 + .../p/go.crypto/openpgp/packet/ocfb_test.go | 46 + .../openpgp/packet/one_pass_signature.go | 73 + .../p/go.crypto/openpgp/packet/opaque.go | 161 + .../p/go.crypto/openpgp/packet/opaque_test.go | 67 + .../p/go.crypto/openpgp/packet/packet.go | 535 + .../p/go.crypto/openpgp/packet/packet_test.go | 255 + .../p/go.crypto/openpgp/packet/private_key.go | 310 + .../openpgp/packet/private_key_test.go | 58 + .../p/go.crypto/openpgp/packet/public_key.go | 664 + .../openpgp/packet/public_key_test.go | 202 + .../go.crypto/openpgp/packet/public_key_v3.go | 274 + .../openpgp/packet/public_key_v3_test.go | 82 + .../p/go.crypto/openpgp/packet/reader.go | 62 + .../p/go.crypto/openpgp/packet/signature.go | 637 + .../openpgp/packet/signature_test.go | 42 + .../go.crypto/openpgp/packet/signature_v3.go | 146 + .../openpgp/packet/signature_v3_test.go | 92 + .../openpgp/packet/symmetric_key_encrypted.go | 163 + .../packet/symmetric_key_encrypted_test.go | 102 + .../openpgp/packet/symmetrically_encrypted.go | 290 + .../packet/symmetrically_encrypted_test.go | 123 + .../go.crypto/openpgp/packet/userattribute.go | 92 + .../openpgp/packet/userattribute_test.go | 110 + .../p/go.crypto/openpgp/packet/userid.go | 160 + .../p/go.crypto/openpgp/packet/userid_test.go | 87 + .../p/go.crypto/openpgp/read.go | 431 + .../p/go.crypto/openpgp/read_test.go | 375 + .../p/go.crypto/openpgp/s2k/s2k.go | 196 + .../p/go.crypto/openpgp/s2k/s2k_test.go | 118 + .../p/go.crypto/openpgp/write.go | 374 + .../p/go.crypto/openpgp/write_test.go | 234 + .../p/go.crypto/pbkdf2/pbkdf2.go | 77 + .../p/go.crypto/pbkdf2/pbkdf2_test.go | 157 + .../p/go.crypto/ripemd160/ripemd160.go | 120 + .../p/go.crypto/ripemd160/ripemd160_test.go | 64 + .../p/go.crypto/ripemd160/ripemd160block.go | 161 + .../p/go.crypto/scrypt/scrypt.go | 243 + .../p/go.crypto/scrypt/scrypt_test.go | 160 + .../p/go.crypto/ssh/channel.go | 318 + .../code.google.com/p/go.crypto/ssh/cipher.go | 88 + .../p/go.crypto/ssh/cipher_test.go | 62 + .../code.google.com/p/go.crypto/ssh/client.go | 505 + .../p/go.crypto/ssh/client_auth.go | 316 + .../p/go.crypto/ssh/client_auth_test.go | 257 + .../p/go.crypto/ssh/client_func_test.go | 61 + .../code.google.com/p/go.crypto/ssh/common.go | 239 + .../p/go.crypto/ssh/common_test.go | 26 + .../code.google.com/p/go.crypto/ssh/doc.go | 127 + .../p/go.crypto/ssh/messages.go | 640 + .../p/go.crypto/ssh/messages_test.go | 125 + .../code.google.com/p/go.crypto/ssh/server.go | 683 + .../p/go.crypto/ssh/server_terminal.go | 81 + .../p/go.crypto/ssh/session.go | 494 + .../p/go.crypto/ssh/session_test.go | 374 + .../code.google.com/p/go.crypto/ssh/tcpip.go | 132 + .../p/go.crypto/ssh/tcpip_func_test.go | 59 + .../p/go.crypto/ssh/transport.go | 369 + .../p/go.crypto/ssh/transport_test.go | 51 + .../p/go.crypto/twofish/twofish.go | 355 + .../p/go.crypto/twofish/twofish_test.go | 129 + .../code.google.com/p/go.crypto/xtea/block.go | 66 + .../p/go.crypto/xtea/cipher.go | 89 + .../p/go.crypto/xtea/xtea_test.go | 246 + .../p/go.net/html/atom/atom.go | 78 + .../p/go.net/html/atom/atom_test.go | 109 + .../code.google.com/p/go.net/html/atom/gen.go | 636 + .../p/go.net/html/atom/table.go | 694 + .../p/go.net/html/atom/table_test.go | 341 + .../code.google.com/p/go.net/html/const.go | 100 + .../code.google.com/p/go.net/html/doc.go | 106 + .../code.google.com/p/go.net/html/doctype.go | 156 + .../code.google.com/p/go.net/html/entity.go | 2253 ++ .../p/go.net/html/entity_test.go | 29 + .../code.google.com/p/go.net/html/escape.go | 258 + .../p/go.net/html/escape_test.go | 97 + .../p/go.net/html/example_test.go | 40 + .../code.google.com/p/go.net/html/foreign.go | 226 + .../code.google.com/p/go.net/html/node.go | 193 + .../p/go.net/html/node_test.go | 146 + .../code.google.com/p/go.net/html/parse.go | 2092 ++ .../p/go.net/html/parse_test.go | 388 + .../code.google.com/p/go.net/html/render.go | 271 + .../p/go.net/html/render_test.go | 156 + .../code.google.com/p/go.net/html/token.go | 1217 + .../p/go.net/html/token_test.go | 743 + .../code.google.com/p/goauth2/.hgtags | 2 + .../code.google.com/p/goauth2/AUTHORS | 11 + .../code.google.com/p/goauth2/CONTRIBUTORS | 37 + .../code.google.com/p/goauth2/LICENSE | 27 + .../code.google.com/p/goauth2/PATENTS | 22 + .../p/goauth2/lib/codereview/codereview.cfg | 1 + .../code.google.com/p/goauth2/oauth/oauth.go | 461 + .../p/goauth2/oauth/oauth_test.go | 219 + .../code.google.com/p/leveldb-go/.hgignore | 29 + .../code.google.com/p/leveldb-go/AUTHORS | 12 + .../code.google.com/p/leveldb-go/CONTRIBUTORS | 31 + .../code.google.com/p/leveldb-go/LICENSE | 27 + .../code.google.com/p/leveldb-go/README | 11 + .../p/leveldb-go/leveldb/crc/crc.go | 35 + .../p/leveldb-go/leveldb/db/comparer.go | 86 + .../p/leveldb-go/leveldb/db/comparer_test.go | 50 + .../p/leveldb-go/leveldb/db/db.go | 121 + .../p/leveldb-go/leveldb/db/options.go | 132 + .../p/leveldb-go/leveldb/leveldb.go | 18 + .../p/leveldb-go/leveldb/memdb/memdb.go | 318 + .../p/leveldb-go/leveldb/memdb/memdb_test.go | 222 + .../p/leveldb-go/leveldb/record/record.go | 377 + .../leveldb-go/leveldb/record/record_test.go | 314 + .../p/leveldb-go/leveldb/table/reader.go | 403 + .../p/leveldb-go/leveldb/table/table.go | 137 + .../p/leveldb-go/leveldb/table/table_test.go | 279 + .../p/leveldb-go/leveldb/table/writer.go | 309 + .../leveldb-go/lib/codereview/codereview.cfg | 1 + .../leveldb-go/testdata/db-stage-1/000003.log | 0 .../p/leveldb-go/testdata/db-stage-1/CURRENT | 1 + .../p/leveldb-go/testdata/db-stage-1/LOCK | 0 .../p/leveldb-go/testdata/db-stage-1/LOG | 1 + .../testdata/db-stage-1/MANIFEST-000002 | Bin 0 -> 65536 bytes .../leveldb-go/testdata/db-stage-2/000003.log | Bin 0 -> 65536 bytes .../p/leveldb-go/testdata/db-stage-2/CURRENT | 1 + .../p/leveldb-go/testdata/db-stage-2/LOCK | 0 .../p/leveldb-go/testdata/db-stage-2/LOG | 1 + .../testdata/db-stage-2/MANIFEST-000002 | Bin 0 -> 65536 bytes .../leveldb-go/testdata/db-stage-3/000005.sst | Bin 0 -> 165 bytes .../leveldb-go/testdata/db-stage-3/000006.log | 0 .../p/leveldb-go/testdata/db-stage-3/CURRENT | 1 + .../p/leveldb-go/testdata/db-stage-3/LOCK | 0 .../p/leveldb-go/testdata/db-stage-3/LOG | 5 + .../p/leveldb-go/testdata/db-stage-3/LOG.old | 1 + .../testdata/db-stage-3/MANIFEST-000004 | Bin 0 -> 65536 bytes .../leveldb-go/testdata/db-stage-4/000005.sst | Bin 0 -> 165 bytes .../leveldb-go/testdata/db-stage-4/000006.log | Bin 0 -> 65536 bytes .../p/leveldb-go/testdata/db-stage-4/CURRENT | 1 + .../p/leveldb-go/testdata/db-stage-4/LOCK | 0 .../p/leveldb-go/testdata/db-stage-4/LOG | 5 + .../p/leveldb-go/testdata/db-stage-4/LOG.old | 1 + .../testdata/db-stage-4/MANIFEST-000004 | Bin 0 -> 65536 bytes .../leveldb-go/testdata/h.no-compression.sst | Bin 0 -> 13105 bytes .../p/leveldb-go/testdata/h.sst | Bin 0 -> 11028 bytes .../p/leveldb-go/testdata/h.txt | 1710 ++ .../p/leveldb-go/testdata/hamlet-act-1.txt | 1234 + .../p/leveldb-go/testdata/make-db.cc | 116 + .../p/leveldb-go/testdata/make-table.cc | 106 + .../code.google.com/p/rsc/gf256/blog_test.go | 85 + .../code.google.com/p/rsc/gf256/gf256.go | 241 + .../code.google.com/p/rsc/gf256/gf256_test.go | 194 + .../code.google.com/p/rsc/qr/coding/gen.go | 149 + .../code.google.com/p/rsc/qr/coding/qr.go | 815 + .../p/rsc/qr/coding/qr_test.go | 133 + .../code.google.com/p/rsc/qr/png.go | 400 + .../code.google.com/p/rsc/qr/png_test.go | 73 + .../code.google.com/p/rsc/qr/qr.go | 116 + .../code.google.com/p/rsc/qr/web/pic.go | 505 + .../code.google.com/p/rsc/qr/web/play.go | 1118 + .../p/rsc/qr/web/resize/resize.go | 152 + .../code.google.com/p/snappy-go/.hgignore | 29 + .../code.google.com/p/snappy-go/AUTHORS | 11 + .../code.google.com/p/snappy-go/CONTRIBUTORS | 32 + .../code.google.com/p/snappy-go/LICENSE | 27 + .../code.google.com/p/snappy-go/README | 11 + .../p/snappy-go/lib/codereview/codereview.cfg | 1 + .../p/snappy-go/snappy/decode.go | 121 + .../p/snappy-go/snappy/encode.go | 178 + .../p/snappy-go/snappy/snappy.go | 38 + .../p/snappy-go/snappy/snappy_test.go | 117 + .../code.google.com/p/xsrftoken/COPYING | 202 + .../code.google.com/p/xsrftoken/xsrf.go | 94 + .../code.google.com/p/xsrftoken/xsrf_test.go | 92 + .../third_party/fontawesome/LICENSE.txt | 4 + .../third_party/fontawesome/VERSION.txt | 1 + .../fontawesome/css/font-awesome.css | 1338 ++ .../fontawesome/css/font-awesome.min.css | 4 + .../third_party/fontawesome/fileembed.go | 30 + .../fontawesome/fonts/FontAwesome.otf | Bin 0 -> 62856 bytes .../fontawesome/fonts/fontawesome-webfont.eot | Bin 0 -> 38205 bytes .../fontawesome/fonts/fontawesome-webfont.svg | 414 + .../fontawesome/fonts/fontawesome-webfont.ttf | Bin 0 -> 80652 bytes .../fonts/fontawesome-webfont.woff | Bin 0 -> 44432 bytes .../bradfitz/gomemcache/memcache/memcache.go | 579 + .../gomemcache/memcache/memcache_test.go | 164 + .../bradfitz/gomemcache/memcache/selector.go | 84 + .../github.com/bradfitz/latlong/latlong.go | 253 + .../bradfitz/latlong/latlong_test.go | 107 + .../bradfitz/latlong/z_gen_tables.go | 30 + .../bradfitz/runsit/listen/listen.go | 135 + .../github.com/camlistore/lock/COPYING | 202 + .../github.com/camlistore/lock/README.txt | 3 + .../github.com/camlistore/lock/lock.go | 158 + .../camlistore/lock/lock_appengine.go | 32 + .../camlistore/lock/lock_darwin_amd64.go | 80 + .../camlistore/lock/lock_freebsd.go | 79 + .../camlistore/lock/lock_linux_amd64.go | 80 + .../camlistore/lock/lock_linux_arm.go | 81 + .../camlistore/lock/lock_sigzero.go | 26 + .../github.com/camlistore/lock/lock_test.go | 131 + .../github.com/cznic/bufs/Makefile | 30 + .../github.com/cznic/bufs/README.md | 8 + .../third_party/github.com/cznic/bufs/bufs.go | 391 + .../github.com/cznic/bufs/bufs_test.go | 174 + .../github.com/cznic/exp/README.md | 10 + .../github.com/cznic/exp/dbm/LICENSE | 27 + .../github.com/cznic/exp/dbm/Makefile | 23 + .../github.com/cznic/exp/dbm/README.md | 9 + .../github.com/cznic/exp/dbm/all_test.go | 2901 +++ .../github.com/cznic/exp/dbm/array.go | 547 + .../github.com/cznic/exp/dbm/bench | 9 + .../github.com/cznic/exp/dbm/bits.go | 306 + .../cznic/exp/dbm/db_bench/Makefile | 20 + .../github.com/cznic/exp/dbm/db_bench/main.go | 195 + .../cznic/exp/dbm/db_bench/main_test.go | 26 + .../github.com/cznic/exp/dbm/dbm.go | 1151 + .../github.com/cznic/exp/dbm/doc.go | 303 + .../github.com/cznic/exp/dbm/etc.go | 156 + .../github.com/cznic/exp/dbm/file.go | 452 + .../github.com/cznic/exp/dbm/http.go | 121 + .../github.com/cznic/exp/dbm/options.go | 206 + .../github.com/cznic/exp/dbm/slice.go | 290 + .../github.com/cznic/exp/dbm/v0.go | 23 + .../github.com/cznic/exp/lldb/2pc.go | 324 + .../github.com/cznic/exp/lldb/2pc_docs.go | 44 + .../github.com/cznic/exp/lldb/2pc_test.go | 285 + .../github.com/cznic/exp/lldb/LICENSE | 27 + .../github.com/cznic/exp/lldb/Makefile | 34 + .../github.com/cznic/exp/lldb/README.md | 8 + .../github.com/cznic/exp/lldb/all_test.go | 43 + .../github.com/cznic/exp/lldb/btree.go | 2276 ++ .../github.com/cznic/exp/lldb/btree_test.go | 1887 ++ .../cznic/exp/lldb/db_bench/Makefile | 21 + .../cznic/exp/lldb/db_bench/main.go | 237 + .../cznic/exp/lldb/db_bench/main_test.go | 126 + .../github.com/cznic/exp/lldb/errors.go | 170 + .../github.com/cznic/exp/lldb/falloc.go | 1970 ++ .../github.com/cznic/exp/lldb/falloc_test.go | 1833 ++ .../github.com/cznic/exp/lldb/filer.go | 192 + .../github.com/cznic/exp/lldb/filer_test.go | 764 + .../github.com/cznic/exp/lldb/gb.go | 812 + .../github.com/cznic/exp/lldb/gb_test.go | 364 + .../github.com/cznic/exp/lldb/lldb.go | 155 + .../github.com/cznic/exp/lldb/lldb_test.go | 217 + .../github.com/cznic/exp/lldb/memfiler.go | 344 + .../cznic/exp/lldb/memfiler_test.go | 132 + .../github.com/cznic/exp/lldb/osfiler.go | 130 + .../cznic/exp/lldb/simplefilefiler.go | 123 + .../github.com/cznic/exp/lldb/xact.go | 629 + .../github.com/cznic/exp/lldb/xact_test.go | 400 + .../github.com/cznic/fileutil/AUTHORS | 14 + .../github.com/cznic/fileutil/CONTRIBUTORS | 14 + .../github.com/cznic/fileutil/LICENSE | 27 + .../github.com/cznic/fileutil/Makefile | 27 + .../github.com/cznic/fileutil/README | 16 + .../github.com/cznic/fileutil/all_test.go | 39 + .../github.com/cznic/fileutil/falloc/LICENSE | 27 + .../github.com/cznic/fileutil/falloc/README | 5 + .../cznic/fileutil/falloc/all_test.go | 3105 +++ .../github.com/cznic/fileutil/falloc/docs.go | 251 + .../github.com/cznic/fileutil/falloc/error.go | 130 + .../cznic/fileutil/falloc/falloc.go | 676 + .../cznic/fileutil/falloc/test_deps.go | 15 + .../github.com/cznic/fileutil/fileutil.go | 223 + .../github.com/cznic/fileutil/fileutil_arm.go | 25 + .../cznic/fileutil/fileutil_darwin.go | 25 + .../cznic/fileutil/fileutil_freebsd.go | 25 + .../cznic/fileutil/fileutil_linux.go | 96 + .../cznic/fileutil/fileutil_openbsd.go | 25 + .../cznic/fileutil/fileutil_plan9.go | 25 + .../cznic/fileutil/fileutil_solaris.go | 27 + .../cznic/fileutil/fileutil_windows.go | 183 + .../github.com/cznic/fileutil/hdb/LICENSE | 27 + .../github.com/cznic/fileutil/hdb/README | 5 + .../github.com/cznic/fileutil/hdb/all_test.go | 15 + .../github.com/cznic/fileutil/hdb/hdb.go | 153 + .../cznic/fileutil/hdb/test_deps.go | 13 + .../github.com/cznic/fileutil/punch_test.go | 55 + .../github.com/cznic/fileutil/storage/LICENSE | 27 + .../github.com/cznic/fileutil/storage/README | 5 + .../cznic/fileutil/storage/all_test.go | 22 + .../cznic/fileutil/storage/cache.go | 322 + .../cznic/fileutil/storage/cache_test.go | 83 + .../cznic/fileutil/storage/dev_test.go | 18 + .../github.com/cznic/fileutil/storage/file.go | 50 + .../github.com/cznic/fileutil/storage/mem.go | 161 + .../cznic/fileutil/storage/mem_test.go | 15 + .../cznic/fileutil/storage/probe.go | 74 + .../cznic/fileutil/storage/probe_test.go | 86 + .../cznic/fileutil/storage/storage.go | 141 + .../cznic/fileutil/storage/test_deps.go | 13 + .../github.com/cznic/fileutil/test_deps.go | 13 + .../third_party/github.com/cznic/kv/LICENSE | 27 + .../third_party/github.com/cznic/kv/Makefile | 28 + .../third_party/github.com/cznic/kv/README.md | 10 + .../.2196ad2c3cbc669595720f0cfb6f0dd888bc64bc | 0 .../github.com/cznic/kv/_testdata/open.db | Bin 0 -> 144 bytes .../github.com/cznic/kv/all_test.go | 1910 ++ .../third_party/github.com/cznic/kv/doc.go | 82 + .../third_party/github.com/cznic/kv/etc.go | 56 + .../third_party/github.com/cznic/kv/kv.go | 831 + .../third_party/github.com/cznic/kv/lock.go | 58 + .../github.com/cznic/kv/options.go | 253 + .../third_party/github.com/cznic/kv/v0.go | 21 + .../third_party/github.com/cznic/kv/verify.go | 81 + .../github.com/cznic/mathutil/GO-LICENSE | 27 + .../github.com/cznic/mathutil/LICENSE | 27 + .../github.com/cznic/mathutil/README | 10 + .../github.com/cznic/mathutil/all_test.go | 3485 +++ .../github.com/cznic/mathutil/bits.go | 207 + .../github.com/cznic/mathutil/envelope.go | 46 + .../github.com/cznic/mathutil/ff/main.go | 83 + .../github.com/cznic/mathutil/mathutil.go | 829 + .../cznic/mathutil/mersenne/LICENSE | 27 + .../github.com/cznic/mathutil/mersenne/README | 2 + .../cznic/mathutil/mersenne/all_test.go | 938 + .../cznic/mathutil/mersenne/mersenne.go | 288 + .../cznic/mathutil/nist-sts-2-1-1-report | 267 + .../github.com/cznic/mathutil/permute.go | 39 + .../github.com/cznic/mathutil/primes.go | 342 + .../github.com/cznic/mathutil/rat.go | 27 + .../github.com/cznic/mathutil/rnd.go | 383 + .../github.com/cznic/mathutil/tables.go | 6995 ++++++ .../github.com/cznic/mathutil/test_deps.go | 11 + .../github.com/cznic/sortutil/LICENSE | 27 + .../github.com/cznic/sortutil/README | 4 + .../github.com/cznic/sortutil/all_test.go | 360 + .../github.com/cznic/sortutil/sortutil.go | 227 + .../github.com/cznic/zappy/LICENSE | 27 + .../github.com/cznic/zappy/Makefile | 22 + .../github.com/cznic/zappy/README.md | 9 + .../github.com/cznic/zappy/SNAPPY-GO-LICENSE | 27 + .../github.com/cznic/zappy/all_test.go | 378 + .../github.com/cznic/zappy/decode.go | 38 + .../github.com/cznic/zappy/decode_cgo.go | 121 + .../github.com/cznic/zappy/decode_nocgo.go | 89 + .../github.com/cznic/zappy/encode.go | 37 + .../github.com/cznic/zappy/encode_cgo.go | 140 + .../github.com/cznic/zappy/encode_nocgo.go | 92 + .../github.com/cznic/zappy/zappy.go | 241 + .../github.com/davecgh/go-spew/LICENSE | 13 + .../github.com/davecgh/go-spew/README.md | 140 + .../github.com/davecgh/go-spew/cov_report.sh | 22 + .../github.com/davecgh/go-spew/spew/common.go | 344 + .../davecgh/go-spew/spew/common_test.go | 192 + .../github.com/davecgh/go-spew/spew/config.go | 288 + .../github.com/davecgh/go-spew/spew/doc.go | 196 + .../github.com/davecgh/go-spew/spew/dump.go | 474 + .../davecgh/go-spew/spew/dump_test.go | 912 + .../davecgh/go-spew/spew/dumpcgo_test.go | 82 + .../davecgh/go-spew/spew/dumpnocgo_test.go | 26 + .../davecgh/go-spew/spew/example_test.go | 230 + .../github.com/davecgh/go-spew/spew/format.go | 413 + .../davecgh/go-spew/spew/format_test.go | 1483 ++ .../davecgh/go-spew/spew/internal_test.go | 162 + .../github.com/davecgh/go-spew/spew/spew.go | 148 + .../davecgh/go-spew/spew/spew_test.go | 308 + .../davecgh/go-spew/spew/testdata/dumpcgo.go | 81 + .../davecgh/go-spew/test_coverage.txt | 61 + .../github.com/garyburd/go-oauth/.gitignore | 5 + .../garyburd/go-oauth/README.markdown | 20 + .../garyburd/go-oauth/oauth/examples_test.go | 54 + .../garyburd/go-oauth/oauth/oauth.go | 442 + .../garyburd/go-oauth/oauth/oauth_test.go | 172 + .../github.com/go-sql-driver/mysql/AUTHORS | 28 + .../go-sql-driver/mysql/CHANGELOG.md | 40 + .../go-sql-driver/mysql/CONTRIBUTING.md | 40 + .../github.com/go-sql-driver/mysql/LICENSE | 373 + .../github.com/go-sql-driver/mysql/README.md | 312 + .../go-sql-driver/mysql/benchmark_test.go | 208 + .../github.com/go-sql-driver/mysql/buffer.go | 126 + .../go-sql-driver/mysql/connection.go | 261 + .../github.com/go-sql-driver/mysql/const.go | 142 + .../github.com/go-sql-driver/mysql/driver.go | 109 + .../go-sql-driver/mysql/driver_test.go | 1259 ++ .../github.com/go-sql-driver/mysql/errors.go | 105 + .../github.com/go-sql-driver/mysql/infile.go | 152 + .../github.com/go-sql-driver/mysql/packets.go | 1209 + .../github.com/go-sql-driver/mysql/result.go | 22 + .../github.com/go-sql-driver/mysql/rows.go | 86 + .../go-sql-driver/mysql/statement.go | 112 + .../go-sql-driver/mysql/transaction.go | 31 + .../github.com/go-sql-driver/mysql/utils.go | 681 + .../go-sql-driver/mysql/utils_test.go | 124 + .../github.com/golang/glog/LICENSE | 191 + .../third_party/github.com/golang/glog/README | 44 + .../github.com/golang/glog/glog.go | 1033 + .../github.com/golang/glog/glog_file.go | 124 + .../github.com/golang/glog/glog_test.go | 333 + .../github.com/gorilla/websocket/LICENSE | 23 + .../github.com/gorilla/websocket/README.md | 49 + .../github.com/gorilla/websocket/client.go | 69 + .../gorilla/websocket/client_server_test.go | 114 + .../github.com/gorilla/websocket/conn.go | 759 + .../github.com/gorilla/websocket/conn_test.go | 140 + .../github.com/gorilla/websocket/doc.go | 103 + .../github.com/gorilla/websocket/json.go | 49 + .../github.com/gorilla/websocket/json_test.go | 63 + .../github.com/gorilla/websocket/server.go | 128 + .../gorilla/websocket/server_test.go | 33 + .../github.com/gorilla/websocket/util.go | 44 + .../hjfreyer/taglib-go/taglib/.gitignore | 1 + .../hjfreyer/taglib-go/taglib/LICENSE | 191 + .../hjfreyer/taglib-go/taglib/README.md | 40 + .../hjfreyer/taglib-go/taglib/id3/id3v23.go | 308 + .../taglib-go/taglib/id3/id3v23frames.go | 19 + .../hjfreyer/taglib-go/taglib/id3/id3v24.go | 317 + .../taglib-go/taglib/id3/id3v24_test.go | 42 + .../taglib-go/taglib/id3/id3v24frames.go | 19 + .../hjfreyer/taglib-go/taglib/id3/util.go | 117 + .../hjfreyer/taglib-go/taglib/taglib.go | 76 + .../hjfreyer/taglib-go/taglib/taglib_test.go | 55 + .../taglib-go/taglib/testdata/test24.mp3 | Bin 0 -> 184861 bytes .../third_party/github.com/lib/pq/LICENSE.md | 8 + .../third_party/github.com/lib/pq/README.md | 103 + .../third_party/github.com/lib/pq/buf.go | 81 + .../third_party/github.com/lib/pq/conn.go | 684 + .../github.com/lib/pq/conn_test.go | 528 + .../third_party/github.com/lib/pq/encode.go | 122 + .../github.com/lib/pq/encode_test.go | 164 + .../third_party/github.com/lib/pq/error.go | 108 + .../github.com/lib/pq/oid/types.go | 169 + .../third_party/github.com/lib/pq/url.go | 68 + .../third_party/github.com/lib/pq/url_test.go | 54 + .../github.com/lib/pq/user_posix.go | 15 + .../github.com/lib/pq/user_windows.go | 27 + .../github.com/mattn/go-sqlite3/README.mkd | 22 + .../github.com/mattn/go-sqlite3/sqlite3.go | 400 + .../mattn/go-sqlite3/sqlite3_other.go | 9 + .../mattn/go-sqlite3/sqlite3_test.go | 409 + .../third_party/github.com/nf/cr2/buffer.go | 69 + .../github.com/nf/cr2/buffer_test.go | 36 + .../third_party/github.com/nf/cr2/consts.go | 113 + .../third_party/github.com/nf/cr2/reader.go | 339 + .../github.com/nf/cr2/reader_test.go | 172 + .../github.com/nf/cr2/samples_test.go | 240 + .../russross/blackfriday/LICENSE.txt | 29 + .../github.com/russross/blackfriday/README.md | 246 + .../github.com/russross/blackfriday/block.go | 1389 ++ .../russross/blackfriday/block_test.go | 1407 ++ .../github.com/russross/blackfriday/html.go | 948 + .../github.com/russross/blackfriday/inline.go | 1103 + .../russross/blackfriday/inline_test.go | 1016 + .../github.com/russross/blackfriday/latex.go | 332 + .../russross/blackfriday/markdown.go | 919 + .../russross/blackfriday/smartypants.go | 398 + .../github.com/rwcarlsen/goexif/LICENSE | 24 + .../rwcarlsen/goexif/README.camlistore | 9 + .../github.com/rwcarlsen/goexif/README.md | 71 + .../rwcarlsen/goexif/exif/README.md | 4 + .../github.com/rwcarlsen/goexif/exif/exif.go | 619 + .../rwcarlsen/goexif/exif/fields.go | 293 + .../rwcarlsen/goexif/exifstat/main.go | 60 + .../rwcarlsen/goexif/mknote/fields.go | 268 + .../rwcarlsen/goexif/mknote/mknote.go | 70 + .../github.com/rwcarlsen/goexif/tiff/tag.go | 438 + .../github.com/rwcarlsen/goexif/tiff/tiff.go | 153 + .../shurcooL/sanitized_anchor_name/README.md | 25 + .../shurcooL/sanitized_anchor_name/main.go | 29 + .../sanitized_anchor_name/main_test.go | 35 + .../syndtr/goleveldb/leveldb/batch.go | 252 + .../syndtr/goleveldb/leveldb/batch_test.go | 120 + .../syndtr/goleveldb/leveldb/bench2_test.go | 58 + .../syndtr/goleveldb/leveldb/bench_test.go | 464 + .../goleveldb/leveldb/cache/bench2_test.go | 30 + .../syndtr/goleveldb/leveldb/cache/cache.go | 676 + .../goleveldb/leveldb/cache/cache_test.go | 554 + .../syndtr/goleveldb/leveldb/cache/lru.go | 195 + .../syndtr/goleveldb/leveldb/comparer.go | 75 + .../leveldb/comparer/bytes_comparer.go | 51 + .../goleveldb/leveldb/comparer/comparer.go | 57 + .../syndtr/goleveldb/leveldb/corrupt_test.go | 500 + .../github.com/syndtr/goleveldb/leveldb/db.go | 945 + .../syndtr/goleveldb/leveldb/db_compaction.go | 835 + .../syndtr/goleveldb/leveldb/db_iter.go | 350 + .../syndtr/goleveldb/leveldb/db_snapshot.go | 183 + .../syndtr/goleveldb/leveldb/db_state.go | 211 + .../syndtr/goleveldb/leveldb/db_test.go | 2665 +++ .../syndtr/goleveldb/leveldb/db_util.go | 100 + .../syndtr/goleveldb/leveldb/db_write.go | 311 + .../syndtr/goleveldb/leveldb/doc.go | 90 + .../syndtr/goleveldb/leveldb/errors.go | 18 + .../syndtr/goleveldb/leveldb/errors/errors.go | 76 + .../syndtr/goleveldb/leveldb/external_test.go | 58 + .../syndtr/goleveldb/leveldb/filter.go | 31 + .../syndtr/goleveldb/leveldb/filter/bloom.go | 116 + .../goleveldb/leveldb/filter/bloom_test.go | 142 + .../syndtr/goleveldb/leveldb/filter/filter.go | 60 + .../goleveldb/leveldb/iterator/array_iter.go | 184 + .../leveldb/iterator/array_iter_test.go | 30 + .../leveldb/iterator/indexed_iter.go | 242 + .../leveldb/iterator/indexed_iter_test.go | 83 + .../syndtr/goleveldb/leveldb/iterator/iter.go | 131 + .../leveldb/iterator/iter_suite_test.go | 11 + .../goleveldb/leveldb/iterator/merged_iter.go | 304 + .../leveldb/iterator/merged_iter_test.go | 60 + .../goleveldb/leveldb/journal/journal.go | 520 + .../goleveldb/leveldb/journal/journal_test.go | 818 + .../syndtr/goleveldb/leveldb/key.go | 142 + .../syndtr/goleveldb/leveldb/key_test.go | 133 + .../goleveldb/leveldb/leveldb_suite_test.go | 11 + .../goleveldb/leveldb/memdb/bench_test.go | 75 + .../syndtr/goleveldb/leveldb/memdb/memdb.go | 468 + .../leveldb/memdb/memdb_suite_test.go | 11 + .../goleveldb/leveldb/memdb/memdb_test.go | 135 + .../syndtr/goleveldb/leveldb/opt/options.go | 639 + .../syndtr/goleveldb/leveldb/options.go | 92 + .../syndtr/goleveldb/leveldb/session.go | 455 + .../goleveldb/leveldb/session_record.go | 313 + .../goleveldb/leveldb/session_record_test.go | 64 + .../syndtr/goleveldb/leveldb/session_util.go | 249 + .../goleveldb/leveldb/storage/file_storage.go | 534 + .../leveldb/storage/file_storage_plan9.go | 52 + .../leveldb/storage/file_storage_solaris.go | 68 + .../leveldb/storage/file_storage_test.go | 142 + .../leveldb/storage/file_storage_unix.go | 63 + .../leveldb/storage/file_storage_windows.go | 69 + .../goleveldb/leveldb/storage/mem_storage.go | 203 + .../leveldb/storage/mem_storage_test.go | 66 + .../goleveldb/leveldb/storage/storage.go | 157 + .../syndtr/goleveldb/leveldb/storage_test.go | 539 + .../syndtr/goleveldb/leveldb/table.go | 521 + .../goleveldb/leveldb/table/block_test.go | 139 + .../syndtr/goleveldb/leveldb/table/reader.go | 1107 + .../syndtr/goleveldb/leveldb/table/table.go | 177 + .../leveldb/table/table_suite_test.go | 11 + .../goleveldb/leveldb/table/table_test.go | 122 + .../syndtr/goleveldb/leveldb/table/writer.go | 379 + .../syndtr/goleveldb/leveldb/testutil/db.go | 222 + .../goleveldb/leveldb/testutil/ginkgo.go | 21 + .../syndtr/goleveldb/leveldb/testutil/iter.go | 327 + .../syndtr/goleveldb/leveldb/testutil/kv.go | 352 + .../goleveldb/leveldb/testutil/kvtest.go | 187 + .../goleveldb/leveldb/testutil/storage.go | 586 + .../syndtr/goleveldb/leveldb/testutil/util.go | 171 + .../syndtr/goleveldb/leveldb/testutil_test.go | 63 + .../syndtr/goleveldb/leveldb/util.go | 91 + .../syndtr/goleveldb/leveldb/util/buffer.go | 293 + .../goleveldb/leveldb/util/buffer_pool.go | 238 + .../goleveldb/leveldb/util/buffer_test.go | 369 + .../syndtr/goleveldb/leveldb/util/crc32.go | 30 + .../syndtr/goleveldb/leveldb/util/hash.go | 48 + .../syndtr/goleveldb/leveldb/util/pool.go | 21 + .../goleveldb/leveldb/util/pool_legacy.go | 33 + .../syndtr/goleveldb/leveldb/util/range.go | 32 + .../syndtr/goleveldb/leveldb/util/util.go | 73 + .../syndtr/goleveldb/leveldb/version.go | 457 + .../syndtr/gosnappy/snappy/decode.go | 292 + .../syndtr/gosnappy/snappy/encode.go | 258 + .../syndtr/gosnappy/snappy/snappy.go | 68 + .../syndtr/gosnappy/snappy/snappy_test.go | 364 + .../github.com/tgulacsi/picago/README.md | 12 + .../github.com/tgulacsi/picago/atom.go | 98 + .../github.com/tgulacsi/picago/atom_test.go | 326 + .../github.com/tgulacsi/picago/auth.go | 116 + .../github.com/tgulacsi/picago/doc.go | 10 + .../github.com/tgulacsi/picago/get.go | 366 + .../tgulacsi/picago/testdata/album-list.xml | 140 + .../picago/testdata/gallery-with-a-video.xml | 206 + .../camlistore/third_party/glitch/LICENSE | 19 + .../third_party/glitch/fileembed.go | 26 + .../npc_piggy__x1_chew_png_1354829433.png | Bin 0 -> 45880 bytes ...c_piggy__x1_look_screen_png_1354829434.png | Bin 0 -> 34788 bytes .../npc_piggy__x1_rooked1_png_1354829442.png | Bin 0 -> 7150 bytes ...ggy__x1_too_much_nibble_png_1354829441.png | Bin 0 -> 50629 bytes .../npc_piggy__x1_walk_png_1354829432.png | Bin 0 -> 41980 bytes .../camlistore/third_party/go/README | 14 + .../go/pkg/archive/zip/example_test.go | 75 + .../third_party/go/pkg/archive/zip/reader.go | 453 + .../go/pkg/archive/zip/reader_test.go | 533 + .../go/pkg/archive/zip/register.go | 110 + .../third_party/go/pkg/archive/zip/struct.go | 313 + .../zip/testdata/crc32-not-streamed.zip | Bin 0 -> 314 bytes .../go/pkg/archive/zip/testdata/dd.zip | Bin 0 -> 154 bytes .../zip/testdata/go-no-datadesc-sig.zip | Bin 0 -> 330 bytes .../zip/testdata/go-with-datadesc-sig.zip | Bin 0 -> 242 bytes .../archive/zip/testdata/gophercolor16x16.png | Bin 0 -> 785 bytes .../go/pkg/archive/zip/testdata/readme.notzip | Bin 0 -> 1905 bytes .../go/pkg/archive/zip/testdata/readme.zip | Bin 0 -> 1885 bytes .../go/pkg/archive/zip/testdata/symlink.zip | Bin 0 -> 173 bytes .../zip/testdata/test-trailing-junk.zip | Bin 0 -> 1184 bytes .../go/pkg/archive/zip/testdata/test.zip | Bin 0 -> 1170 bytes .../go/pkg/archive/zip/testdata/unix.zip | Bin 0 -> 620 bytes .../go/pkg/archive/zip/testdata/winxp.zip | Bin 0 -> 412 bytes .../go/pkg/archive/zip/testdata/zip64-2.zip | Bin 0 -> 266 bytes .../go/pkg/archive/zip/testdata/zip64.zip | Bin 0 -> 242 bytes .../third_party/go/pkg/archive/zip/writer.go | 357 + .../go/pkg/archive/zip/writer_test.go | 164 + .../go/pkg/archive/zip/zip_test.go | 395 + .../third_party/go/pkg/image/jpeg/dct_test.go | 299 + .../third_party/go/pkg/image/jpeg/fdct.go | 190 + .../third_party/go/pkg/image/jpeg/huffman.go | 244 + .../third_party/go/pkg/image/jpeg/idct.go | 192 + .../third_party/go/pkg/image/jpeg/reader.go | 524 + .../go/pkg/image/jpeg/reader_test.go | 224 + .../third_party/go/pkg/image/jpeg/scan.go | 437 + .../third_party/go/pkg/image/jpeg/writer.go | 614 + .../go/pkg/image/jpeg/writer_test.go | 232 + .../third_party/golang.org/x/image/AUTHORS | 3 + .../golang.org/x/image/CONTRIBUTING.md | 31 + .../golang.org/x/image/CONTRIBUTORS | 3 + .../third_party/golang.org/x/image/LICENSE | 27 + .../third_party/golang.org/x/image/PATENTS | 22 + .../third_party/golang.org/x/image/README | 3 + .../golang.org/x/image/bmp/reader.go | 190 + .../golang.org/x/image/bmp/writer.go | 158 + .../golang.org/x/image/codereview.cfg | 1 + .../golang.org/x/image/draw/draw.go | 79 + .../golang.org/x/image/draw/gen.go | 1341 ++ .../golang.org/x/image/draw/impl.go | 6306 ++++++ .../golang.org/x/image/draw/scale.go | 514 + .../golang.org/x/image/math/f32/f32.go | 37 + .../golang.org/x/image/math/f64/f64.go | 37 + .../golang.org/x/image/riff/riff.go | 180 + .../golang.org/x/image/tiff/buffer.go | 69 + .../golang.org/x/image/tiff/compress.go | 58 + .../golang.org/x/image/tiff/consts.go | 133 + .../golang.org/x/image/tiff/lzw/reader.go | 272 + .../golang.org/x/image/tiff/reader.go | 605 + .../golang.org/x/image/tiff/writer.go | 438 + .../golang.org/x/image/vp8/decode.go | 375 + .../golang.org/x/image/vp8/filter.go | 273 + .../golang.org/x/image/vp8/idct.go | 98 + .../golang.org/x/image/vp8/partition.go | 127 + .../golang.org/x/image/vp8/pred.go | 201 + .../golang.org/x/image/vp8/predfunc.go | 553 + .../golang.org/x/image/vp8/quant.go | 98 + .../golang.org/x/image/vp8/reconstruct.go | 442 + .../golang.org/x/image/vp8/token.go | 381 + .../golang.org/x/image/vp8l/decode.go | 603 + .../golang.org/x/image/vp8l/huffman.go | 245 + .../golang.org/x/image/vp8l/transform.go | 299 + .../golang.org/x/image/webp/decode.go | 272 + .../x/image/webp/nycbcra/nycbcra.go | 186 + .../golang.org/x/net/webdav/file.go | 794 + .../golang.org/x/net/webdav/file_test.go | 1158 + .../third_party/golang.org/x/net/webdav/if.go | 173 + .../golang.org/x/net/webdav/if_test.go | 322 + .../x/net/webdav/litmus_test_server.go | 94 + .../golang.org/x/net/webdav/lock.go | 445 + .../golang.org/x/net/webdav/lock_test.go | 731 + .../golang.org/x/net/webdav/prop.go | 384 + .../golang.org/x/net/webdav/prop_test.go | 618 + .../golang.org/x/net/webdav/webdav.go | 674 + .../golang.org/x/net/webdav/webdav_test.go | 161 + .../golang.org/x/net/webdav/xml.go | 468 + .../golang.org/x/net/webdav/xml_test.go | 908 + .../third_party/labix.org/v2/mgo/LICENSE | 25 + .../third_party/labix.org/v2/mgo/auth.go | 276 + .../third_party/labix.org/v2/mgo/auth_test.go | 778 + .../third_party/labix.org/v2/mgo/bson/LICENSE | 25 + .../third_party/labix.org/v2/mgo/bson/bson.go | 684 + .../labix.org/v2/mgo/bson/bson_test.go | 1452 ++ .../labix.org/v2/mgo/bson/decode.go | 795 + .../labix.org/v2/mgo/bson/encode.go | 462 + .../third_party/labix.org/v2/mgo/cluster.go | 595 + .../labix.org/v2/mgo/cluster_test.go | 1537 ++ .../third_party/labix.org/v2/mgo/doc.go | 30 + .../labix.org/v2/mgo/export_test.go | 32 + .../third_party/labix.org/v2/mgo/gridfs.go | 717 + .../labix.org/v2/mgo/gridfs_test.go | 607 + .../third_party/labix.org/v2/mgo/log.go | 89 + .../third_party/labix.org/v2/mgo/queue.go | 91 + .../labix.org/v2/mgo/queue_test.go | 104 + .../third_party/labix.org/v2/mgo/server.go | 435 + .../third_party/labix.org/v2/mgo/session.go | 3295 +++ .../labix.org/v2/mgo/session_test.go | 3213 +++ .../third_party/labix.org/v2/mgo/socket.go | 655 + .../third_party/labix.org/v2/mgo/stats.go | 147 + .../labix.org/v2/mgo/suite_test.go | 240 + .../camlistore/third_party/less/fileembed.go | 28 + .../camlistore/third_party/less/less.js | 16 + .../third_party/react/JSXTransformer.js | 12569 ++++++++++ .../camlistore/third_party/react/LICENSE.txt | 201 + .../camlistore/third_party/react/README.md | 98 + .../camlistore/third_party/react/VERSION.txt | 1 + .../camlistore/third_party/react/fileembed.go | 30 + .../third_party/react/react-with-addons.js | 18884 ++++++++++++++++ .../react/react-with-addons.min.js | 21 + .../camlistore/third_party/react/react.js | 17228 ++++++++++++++ .../camlistore/third_party/react/react.min.js | 21 + .../camlistore/third_party/update.pl | 113 + .../camlistore/camlistore/website/.gitignore | 3 + .../example-blobserver-config.json | 21 + .../blobserver-example/root/GENERATION.dat | 16 + ...5e60f367cc8156ae48198c496b2b2ebdf5313d.dat | 17 + ...2758fb54521cb6540d256098e7c0f1625b33e3.dat | 9 + ...c1d1cfe92fce5f09d194ba73a0b023102c9b25.dat | 1 + ...c66602c5cff5bb7162de9f6cda88fcd37415ea.dat | 7 + .../camlistore/camlistore/website/camweb.go | 607 + .../camlistore/website/camweb_test.go | 74 + .../camlistore/website/content/code | 45 + .../camlistore/website/content/community | 17 + .../camlistore/website/content/contributors | 31 + .../camlistore/website/content/docs/arch | 6 + .../website/content/docs/client-config | 84 + .../website/content/docs/index.html | 62 + .../website/content/docs/json-signing | 11 + .../camlistore/website/content/docs/overview | 158 + .../website/content/docs/principles | 20 + .../camlistore/website/content/docs/prior-art | 29 + .../camlistore/website/content/docs/protocol | 10 + .../website/content/docs/release/0.1 | 95 + .../website/content/docs/release/0.2 | 55 + .../website/content/docs/release/0.3 | 55 + .../website/content/docs/release/0.4 | 59 + .../website/content/docs/release/0.7 | 87 + .../website/content/docs/release/0.9 | 209 + .../website/content/docs/schema/index.html | 57 + .../website/content/docs/schema/permanode | 146 + .../website/content/docs/server-config | 190 + .../camlistore/website/content/docs/sharing | 106 + .../camlistore/website/content/docs/status | 62 + .../camlistore/website/content/docs/terms | 172 + .../camlistore/website/content/docs/todo | 4 + .../camlistore/website/content/docs/uses | 38 + .../website/content/docs/web-ui-styleguide | 85 + .../camlistore/website/content/download | 48 + .../camlistore/website/content/index.html | 40 + .../camlistore/website/contributors.go | 178 + .../camlistore/camlistore/website/dirtrees.go | 335 + .../camlistore/camlistore/website/email.go | 242 + .../camlistore/camlistore/website/format.go | 360 + .../camlistore/website/gitweb-camli.conf | 35 + .../camlistore/camlistore/website/godoc.go | 426 + .../camlistore/camlistore/website/logging.go | 168 + .../camlistore/camlistore/website/run.pl | 54 + .../camlistore/website/scripts/run-blobserver | 17 + .../camlistore/website/static/all-async.js | 8 + .../camlistore/website/static/all.css | 259 + .../website/static/camli-bar-background.png | Bin 0 -> 1651 bytes .../website/static/camli-header.jpg | Bin 0 -> 63735 bytes .../website/static/camli-header.png | Bin 0 -> 201834 bytes .../camlistore/website/static/favicon.ico | Bin 0 -> 1150 bytes .../camlistore/website/static/godocs.js | 213 + .../camlistore/website/static/index.html | 1 + .../camlistore/website/static/mock.html | 130 + .../camlistore/website/static/piggy.gif | Bin 0 -> 28853 bytes .../camlistore/website/static/robots.txt | 3 + .../camlistore/website/static/ss/8RmuLuw.jpg | Bin 0 -> 177704 bytes .../README.slides | 4 + .../2011-05-07-Camlistore-Sao-Paolo/arch.png | Bin 0 -> 137425 bytes .../blobjects.png | Bin 0 -> 136514 bytes .../diagrams.odp | Bin 0 -> 15790 bytes .../fsbackup.png | Bin 0 -> 26577 bytes .../images/colorbar.png | Bin 0 -> 213 bytes .../index.html | 747 + .../prettify.js | 1391 ++ .../2011-05-07-Camlistore-Sao-Paolo/repl.png | Bin 0 -> 36057 bytes .../2011-05-07-Camlistore-Sao-Paolo/slides.js | 607 + .../styles.css | 600 + .../camlistore/camlistore/website/test.cgi | 30 + .../camlistore/website/tmpl/camlierror.html | 3 + .../camlistore/website/tmpl/contrib.html | 10 + .../camlistore/website/tmpl/error.html | 3 + .../camlistore/website/tmpl/githeader.html | 27 + .../camlistore/website/tmpl/package.html | 212 + .../camlistore/website/tmpl/page.html | 58 + .../coreos/coreos-cloudinit/.gitignore | 4 + .../coreos/coreos-cloudinit/.travis.yml | 12 + .../coreos/coreos-cloudinit/CONTRIBUTING.md | 68 + vendor/github.com/coreos/coreos-cloudinit/DCO | 36 + .../Documentation/cloud-config-deprecated.md | 38 + .../Documentation/cloud-config-locations.md | 26 + .../Documentation/cloud-config-oem.md | 37 + .../Documentation/cloud-config.md | 476 + .../Documentation/config-drive.md | 40 + .../Documentation/debian-interfaces.md | 27 + .../Documentation/vmware-guestinfo.md | 35 + .../coreos-cloudinit/Godeps/Godeps.json | 46 + .../coreos/coreos-cloudinit/Godeps/Readme | 5 + .../coreos/coreos-cloudinit/LICENSE | 202 + .../coreos/coreos-cloudinit/MAINTAINERS | 2 + .../github.com/coreos/coreos-cloudinit/NOTICE | 5 + .../coreos/coreos-cloudinit/README.md | 86 + .../github.com/coreos/coreos-cloudinit/build | 17 + .../coreos/coreos-cloudinit/config/config.go | 2 +- .../config/validate/validate.go | 2 +- .../coreos-cloudinit/coreos-cloudinit.go | 428 + .../coreos-cloudinit/coreos-cloudinit_test.go | 147 + .../github.com/coreos/coreos-cloudinit/cover | 27 + .../datasource/configdrive/configdrive.go | 102 + .../configdrive/configdrive_test.go | 145 + .../coreos-cloudinit/datasource/datasource.go | 38 + .../coreos-cloudinit/datasource/file/file.go | 55 + .../metadata/cloudsigma/server_context.go | 197 + .../cloudsigma/server_context_test.go | 200 + .../metadata/digitalocean/metadata.go | 111 + .../metadata/digitalocean/metadata_test.go | 143 + .../datasource/metadata/ec2/metadata.go | 115 + .../datasource/metadata/ec2/metadata_test.go | 222 + .../datasource/metadata/metadata.go | 71 + .../datasource/metadata/metadata_test.go | 185 + .../datasource/metadata/packet/metadata.go | 106 + .../datasource/metadata/test/test.go | 41 + .../datasource/proc_cmdline/proc_cmdline.go | 110 + .../proc_cmdline/proc_cmdline_test.go | 102 + .../datasource/test/filesystem.go | 57 + .../datasource/test/filesystem_test.go | 115 + .../coreos-cloudinit/datasource/url/url.go | 55 + .../datasource/vmware/vmware.go | 235 + .../datasource/vmware/vmware_test.go | 298 + .../datasource/waagent/waagent.go | 117 + .../datasource/waagent/waagent_test.go | 166 + .../coreos-cloudinit/initialize/config.go | 294 + .../initialize/config_test.go | 299 + .../coreos/coreos-cloudinit/initialize/env.go | 116 + .../coreos-cloudinit/initialize/env_test.go | 148 + .../coreos-cloudinit/initialize/github.go | 32 + .../coreos-cloudinit/initialize/ssh_keys.go | 57 + .../initialize/ssh_keys_test.go | 56 + .../coreos-cloudinit/initialize/user_data.go | 45 + .../initialize/user_data_test.go | 74 + .../coreos-cloudinit/initialize/workspace.go | 66 + .../coreos/coreos-cloudinit/network/debian.go | 63 + .../coreos-cloudinit/network/debian_test.go | 56 + .../coreos-cloudinit/network/digitalocean.go | 169 + .../network/digitalocean_test.go | 481 + .../coreos-cloudinit/network/interface.go | 340 + .../network/interface_test.go | 368 + .../network/is_go15_false_test.go | 5 + .../network/is_go15_true_test.go | 5 + .../coreos/coreos-cloudinit/network/packet.go | 115 + .../coreos/coreos-cloudinit/network/stanza.go | 340 + .../coreos-cloudinit/network/stanza_test.go | 582 + .../coreos/coreos-cloudinit/network/vmware.go | 174 + .../coreos-cloudinit/network/vmware_test.go | 361 + .../coreos-cloudinit/pkg/http_client.go | 155 + .../coreos-cloudinit/pkg/http_client_test.go | 154 + .../coreos/coreos-cloudinit/system/env.go | 52 + .../coreos-cloudinit/system/env_file.go | 114 + .../coreos-cloudinit/system/env_file_test.go | 442 + .../coreos-cloudinit/system/env_test.go | 69 + .../coreos-cloudinit/system/etc_hosts.go | 62 + .../coreos-cloudinit/system/etc_hosts_test.go | 60 + .../coreos/coreos-cloudinit/system/etcd.go | 37 + .../coreos/coreos-cloudinit/system/etcd2.go | 37 + .../coreos-cloudinit/system/etcd_test.go | 79 + .../coreos/coreos-cloudinit/system/file.go | 116 + .../coreos-cloudinit/system/file_test.go | 253 + .../coreos/coreos-cloudinit/system/flannel.go | 44 + .../coreos-cloudinit/system/flannel_test.go | 76 + .../coreos/coreos-cloudinit/system/fleet.go | 38 + .../coreos-cloudinit/system/fleet_test.go | 58 + .../coreos-cloudinit/system/locksmith.go | 37 + .../coreos-cloudinit/system/locksmith_test.go | 58 + .../coreos-cloudinit/system/networkd.go | 95 + .../coreos/coreos-cloudinit/system/oem.go | 46 + .../coreos-cloudinit/system/oem_test.go | 61 + .../coreos/coreos-cloudinit/system/ssh_key.go | 73 + .../coreos/coreos-cloudinit/system/systemd.go | 205 + .../coreos-cloudinit/system/systemd_test.go | 280 + .../coreos/coreos-cloudinit/system/unit.go | 82 + .../coreos-cloudinit/system/unit_test.go | 136 + .../coreos/coreos-cloudinit/system/update.go | 151 + .../coreos-cloudinit/system/update_test.go | 161 + .../coreos/coreos-cloudinit/system/user.go | 114 + .../github.com/coreos/coreos-cloudinit/test | 43 + .../units/90-configdrive.rules | 11 + .../coreos-cloudinit/units/90-ovfenv.rules | 8 + .../units/media-configdrive.mount | 13 + .../units/media-configvirtfs.mount | 18 + .../coreos-cloudinit/units/media-ovfenv.mount | 10 + .../units/system-cloudinit@.service | 11 + .../units/system-config.target | 10 + .../units/user-cloudinit-proc-cmdline.service | 13 + .../units/user-cloudinit@.path | 5 + .../units/user-cloudinit@.service | 13 + .../units/user-config-ovfenv.service | 12 + .../coreos-cloudinit/units/user-config.target | 13 + .../units/user-configdrive.path | 10 + .../units/user-configdrive.service | 22 + .../units/user-configvirtfs.service | 11 + .../coreos => }/go-semver/.travis.yml | 0 vendor/github.com/coreos/go-semver/LICENSE | 202 + .../coreos => }/go-semver/README.md | 0 .../coreos => }/go-semver/example.go | 0 .../github.com/coreos/go-systemd/.travis.yml | 8 + .../coreos/go-systemd/CONTRIBUTING.md | 77 + vendor/github.com/coreos/go-systemd/DCO | 36 + vendor/github.com/coreos/go-systemd/LICENSE | 191 + vendor/github.com/coreos/go-systemd/README.md | 54 + .../coreos/go-systemd/activation/files.go | 52 + .../go-systemd/activation/files_test.go | 82 + .../coreos/go-systemd/activation/listeners.go | 62 + .../go-systemd/activation/listeners_test.go | 86 + .../go-systemd/activation/packetconns.go | 37 + .../go-systemd/activation/packetconns_test.go | 68 + .../coreos/go-systemd/daemon/sdnotify.go | 31 + .../github.com/coreos/go-systemd/dbus/dbus.go | 198 + .../coreos/go-systemd/dbus/dbus_test.go | 77 + .../coreos/go-systemd/dbus/methods.go | 442 + .../coreos/go-systemd/dbus/methods_test.go | 345 + .../coreos/go-systemd/dbus/properties.go | 218 + .../github.com/coreos/go-systemd/dbus/set.go | 47 + .../coreos/go-systemd/dbus/set_test.go | 53 + .../coreos/go-systemd/dbus/subscription.go | 250 + .../go-systemd/dbus/subscription_set.go | 57 + .../go-systemd/dbus/subscription_set_test.go | 82 + .../go-systemd/dbus/subscription_test.go | 105 + .../examples/activation/activation.go | 58 + .../examples/activation/httpserver/README.md | 19 + .../activation/httpserver/hello.service | 11 + .../activation/httpserver/hello.socket | 5 + .../activation/httpserver/httpserver.go | 40 + .../go-systemd/examples/activation/listen.go | 64 + .../go-systemd/examples/activation/udpconn.go | 86 + .../fixtures/enable-disable.service | 5 + .../go-systemd/fixtures/start-stop.service | 5 + .../fixtures/subscribe-events-set.service | 5 + .../fixtures/subscribe-events.service | 5 + .../coreos/go-systemd/login1/dbus.go | 108 + .../coreos/go-systemd/login1/dbus_test.go | 28 + .../coreos/go-systemd/machine1/dbus.go | 81 + .../coreos/go-systemd/machine1/dbus_test.go | 30 + .../coreos/go-systemd/sdjournal/journal.go | 357 + .../go-systemd/sdjournal/journal_test.go | 90 + .../coreos/go-systemd/sdjournal/read.go | 209 + vendor/github.com/coreos/go-systemd/test | 76 + .../coreos/go-systemd/unit/deserialize.go | 276 + .../go-systemd/unit/deserialize_test.go | 381 + .../coreos/go-systemd/unit/end_to_end_test.go | 88 + .../coreos/go-systemd/unit/escape.go | 116 + .../coreos/go-systemd/unit/escape_test.go | 211 + .../coreos/go-systemd/unit/option.go | 54 + .../coreos/go-systemd/unit/option_test.go | 214 + .../coreos/go-systemd/unit/serialize.go | 75 + .../coreos/go-systemd/unit/serialize_test.go | 170 + .../github.com/coreos/go-systemd/util/util.go | 270 + vendor/github.com/coreos/ignition/.gitignore | 3 + vendor/github.com/coreos/ignition/.travis.yml | 12 + .../coreos/ignition/CONTRIBUTING.md | 59 + vendor/github.com/coreos/ignition/DCO | 36 + vendor/github.com/coreos/ignition/LICENSE | 202 + vendor/github.com/coreos/ignition/MAINTAINERS | 2 + vendor/github.com/coreos/ignition/NEWS | 190 + vendor/github.com/coreos/ignition/NOTICE | 5 + vendor/github.com/coreos/ignition/README.md | 15 + vendor/github.com/coreos/ignition/ROADMAP.md | 18 + vendor/github.com/coreos/ignition/build | 20 + .../github.com/alecthomas/units/COPYING | 19 - .../github.com/alecthomas/units/README.md | 11 - .../github.com/alecthomas/units/bytes.go | 83 - .../github.com/alecthomas/units/bytes_test.go | 49 - .../vendor/github.com/alecthomas/units/doc.go | 13 - .../vendor/github.com/alecthomas/units/si.go | 26 - .../github.com/alecthomas/units/util.go | 138 - .../camlistore/pkg/errorutil/highlight.go | 58 - .../v1/vendor/github.com/go-yaml/yaml/LICENSE | 188 - .../github.com/go-yaml/yaml/LICENSE.libyaml | 31 - .../vendor/github.com/go-yaml/yaml/README.md | 128 - .../v1/vendor/github.com/go-yaml/yaml/apic.go | 742 - .../vendor/github.com/go-yaml/yaml/decode.go | 683 - .../github.com/go-yaml/yaml/decode_test.go | 966 - .../github.com/go-yaml/yaml/emitterc.go | 1685 -- .../vendor/github.com/go-yaml/yaml/encode.go | 306 - .../github.com/go-yaml/yaml/encode_test.go | 485 - .../vendor/github.com/go-yaml/yaml/parserc.go | 1096 - .../vendor/github.com/go-yaml/yaml/readerc.go | 391 - .../vendor/github.com/go-yaml/yaml/resolve.go | 203 - .../github.com/go-yaml/yaml/scannerc.go | 2710 --- .../vendor/github.com/go-yaml/yaml/sorter.go | 104 - .../github.com/go-yaml/yaml/suite_test.go | 12 - .../vendor/github.com/go-yaml/yaml/writerc.go | 89 - .../v1/vendor/github.com/go-yaml/yaml/yaml.go | 344 - .../vendor/github.com/go-yaml/yaml/yamlh.go | 716 - .../github.com/go-yaml/yaml/yamlprivateh.go | 173 - .../github.com/alecthomas/units/COPYING | 19 - .../github.com/alecthomas/units/README.md | 11 - .../github.com/alecthomas/units/bytes.go | 83 - .../github.com/alecthomas/units/bytes_test.go | 49 - .../vendor/github.com/alecthomas/units/doc.go | 13 - .../vendor/github.com/alecthomas/units/si.go | 26 - .../github.com/alecthomas/units/util.go | 138 - .../coreos/go-semver/semver/semver.go | 257 - .../coreos/go-semver/semver/semver_test.go | 330 - .../vendor/github.com/go-yaml/yaml/LICENSE | 188 - .../github.com/go-yaml/yaml/LICENSE.libyaml | 31 - .../vendor/github.com/go-yaml/yaml/README.md | 128 - .../vendor/github.com/go-yaml/yaml/apic.go | 742 - .../vendor/github.com/go-yaml/yaml/decode.go | 683 - .../github.com/go-yaml/yaml/decode_test.go | 966 - .../github.com/go-yaml/yaml/emitterc.go | 1685 -- .../vendor/github.com/go-yaml/yaml/encode.go | 306 - .../github.com/go-yaml/yaml/encode_test.go | 485 - .../vendor/github.com/go-yaml/yaml/parserc.go | 1096 - .../vendor/github.com/go-yaml/yaml/readerc.go | 391 - .../vendor/github.com/go-yaml/yaml/resolve.go | 203 - .../github.com/go-yaml/yaml/scannerc.go | 2710 --- .../vendor/github.com/go-yaml/yaml/sorter.go | 104 - .../github.com/go-yaml/yaml/suite_test.go | 12 - .../vendor/github.com/go-yaml/yaml/writerc.go | 89 - .../vendor/github.com/go-yaml/yaml/yaml.go | 344 - .../vendor/github.com/go-yaml/yaml/yamlh.go | 716 - .../github.com/go-yaml/yaml/yamlprivateh.go | 173 - .../vincent-petithory/dataurl/LICENSE | 20 - .../vincent-petithory/dataurl/README.md | 81 - .../dataurl/cmd/dataurl/main.go | 142 - .../vincent-petithory/dataurl/dataurl.go | 280 - .../vincent-petithory/dataurl/dataurl_test.go | 587 - .../vincent-petithory/dataurl/doc.go | 28 - .../vincent-petithory/dataurl/lex.go | 521 - .../vincent-petithory/dataurl/rfc2396.go | 130 - .../vincent-petithory/dataurl/rfc2396_test.go | 69 - .../vincent-petithory/dataurl/wercker.yml | 1 - .../vendor/go4.org/errorutil/highlight.go | 58 - .../coreos/ignition/doc/configuration.md | 87 + .../coreos/ignition/doc/examples.md | 220 + .../coreos/ignition/doc/getting-started.md | 39 + .../coreos/ignition/doc/migrating-configs.md | 148 + .../ignition/doc/supported-platforms.md | 17 + .../coreos/ignition/internal/exec/engine.go | 184 + .../ignition/internal/exec/engine_test.go | 98 + .../internal/exec/stages/disks/disks.go | 260 + .../internal/exec/stages/files/files.go | 324 + .../internal/exec/stages/files/files_test.go | 85 + .../ignition/internal/exec/stages/name.go | 37 + .../ignition/internal/exec/stages/stages.go | 51 + .../ignition/internal/exec/util/file.go | 180 + .../ignition/internal/exec/util/passwd.go | 173 + .../sort.go => internal/exec/util/path.go} | 25 +- .../ignition/internal/exec/util/unit.go | 75 + .../ignition/internal/exec/util/user_lookup.c | 139 + .../internal/exec/util/user_lookup.go | 50 + .../ignition/internal/exec/util/user_lookup.h | 23 + .../internal/exec/util/user_lookup_test.go | 97 + .../ignition/internal/exec/util/util.go | 32 + .../coreos/ignition/internal/log/log.go | 180 + .../coreos/ignition/internal/log/stdout.go | 31 + .../coreos/ignition/internal/main.go | 98 + .../coreos/ignition/internal/oem/name.go | 35 + .../coreos/ignition/internal/oem/oem.go | 170 + .../internal/providers/azure/azure.go | 139 + .../internal/providers/cmdline/cmdline.go | 208 + .../ignition/internal/providers/ec2/ec2.go | 70 + .../ignition/internal/providers/gce/gce.go | 74 + .../ignition/internal/providers/noop/noop.go | 55 + .../ignition/internal/providers/providers.go | 43 + .../internal/providers/util/backoff.go | 30 + .../ignition/internal/providers/util/wait.go | 67 + .../internal/providers/util/wait_test.go | 75 + .../internal/providers/vmware/vmware.go | 93 + .../internal/providers/vmware/vmware_amd64.go | 54 + .../providers/vmware/vmware_unsupported.go | 34 + .../ignition/internal/registry/registry.go | 58 + .../internal/registry/registry_test.go | 154 + .../coreos/ignition/internal/sgdisk/sgdisk.go | 87 + .../ignition/internal/systemd/systemd.go | 50 + .../coreos/ignition/internal/util/http.go | 130 + .../internal/util/tools/prerelease_check.go | 33 + .../ignition/internal/util/verification.go | 57 + .../internal/util/verification_test.go | 84 + .../coreos/ignition/internal/vendor.manifest | 9 + .../ignition/internal/version/version.go | 24 + .../github.com/coreos/ignition/tag_release.sh | 29 + vendor/github.com/coreos/ignition/test | 34 + vendor/github.com/coreos/pkg/.gitignore | 27 + vendor/github.com/coreos/pkg/CONTRIBUTING.md | 71 + vendor/github.com/coreos/pkg/DCO | 36 + vendor/github.com/coreos/pkg/LICENSE | 202 + vendor/github.com/coreos/pkg/MAINTAINERS | 1 + vendor/github.com/coreos/pkg/NOTICE | 5 + vendor/github.com/coreos/pkg/README.md | 3 + vendor/github.com/coreos/pkg/build | 3 + .../github.com/coreos/pkg/cryptoutil/aes.go | 94 + .../coreos/pkg/cryptoutil/aes_test.go | 93 + vendor/github.com/coreos/pkg/health/README.md | 11 + vendor/github.com/coreos/pkg/health/health.go | 127 + .../coreos/pkg/health/health_test.go | 198 + .../github.com/coreos/pkg/httputil/README.md | 13 + .../github.com/coreos/pkg/httputil/cookie.go | 21 + .../coreos/pkg/httputil/cookie_test.go | 51 + vendor/github.com/coreos/pkg/httputil/json.go | 27 + .../coreos/pkg/httputil/json_test.go | 56 + .../coreos/pkg/multierror/multierror.go | 32 + .../coreos/pkg/multierror/multierror_test.go | 59 + vendor/github.com/coreos/pkg/netutil/proxy.go | 55 + vendor/github.com/coreos/pkg/netutil/url.go | 17 + .../github.com/coreos/pkg/netutil/url_test.go | 86 + vendor/github.com/coreos/pkg/test | 56 + .../github.com/coreos/pkg/timeutil/backoff.go | 15 + .../coreos/pkg/timeutil/backoff_test.go | 52 + vendor/github.com/coreos/pkg/yamlutil/yaml.go | 55 + .../coreos/pkg/yamlutil/yaml_test.go | 80 + .../src/github.com/coreos => }/yaml/LICENSE | 0 .../coreos => }/yaml/LICENSE.libyaml | 0 .../src/github.com/coreos => }/yaml/README.md | 0 .../src/github.com/coreos => }/yaml/apic.go | 0 .../src/github.com/coreos => }/yaml/decode.go | 0 .../coreos => }/yaml/decode_test.go | 0 .../github.com/coreos => }/yaml/emitterc.go | 0 .../src/github.com/coreos => }/yaml/encode.go | 0 .../coreos => }/yaml/encode_test.go | 0 .../github.com/coreos => }/yaml/parserc.go | 0 .../github.com/coreos => }/yaml/readerc.go | 0 .../github.com/coreos => }/yaml/resolve.go | 0 .../github.com/coreos => }/yaml/scannerc.go | 0 .../src/github.com/coreos => }/yaml/sorter.go | 0 .../github.com/coreos => }/yaml/suite_test.go | 0 .../github.com/coreos => }/yaml/writerc.go | 0 .../src/github.com/coreos => }/yaml/yaml.go | 0 .../src/github.com/coreos => }/yaml/yamlh.go | 0 .../coreos => }/yaml/yamlprivateh.go | 0 vendor/github.com/davecgh/go-spew/.gitignore | 22 + vendor/github.com/davecgh/go-spew/.travis.yml | 11 + vendor/github.com/davecgh/go-spew/LICENSE | 13 + vendor/github.com/davecgh/go-spew/README.md | 194 + .../github.com/davecgh/go-spew/cov_report.sh | 22 + .../davecgh/go-spew/test_coverage.txt | 61 + vendor/github.com/golang/protobuf/.gitignore | 15 + vendor/github.com/golang/protobuf/AUTHORS | 3 + .../github.com/golang/protobuf/CONTRIBUTORS | 3 + vendor/github.com/golang/protobuf/LICENSE | 31 + .../github.com/golang/protobuf/Make.protobuf | 40 + vendor/github.com/golang/protobuf/Makefile | 54 + vendor/github.com/golang/protobuf/README.md | 199 + .../golang/protobuf/jsonpb/jsonpb.go | 722 + .../golang/protobuf/jsonpb/jsonpb_test.go | 509 + .../jsonpb/jsonpb_test_proto/Makefile | 33 + .../jsonpb_test_proto/more_test_objects.pb.go | 159 + .../jsonpb_test_proto/more_test_objects.proto | 53 + .../jsonpb_test_proto/test_objects.pb.go | 740 + .../jsonpb_test_proto/test_objects.proto | 132 + .../golang/protobuf/protoc-gen-go/Makefile | 33 + .../protoc-gen-go/descriptor/Makefile | 39 + .../protoc-gen-go/descriptor/descriptor.pb.go | 2006 ++ .../golang/protobuf/protoc-gen-go/doc.go | 51 + .../protobuf/protoc-gen-go/generator/Makefile | 40 + .../protoc-gen-go/generator/generator.go | 2781 +++ .../protoc-gen-go/generator/name_test.go | 85 + .../protobuf/protoc-gen-go/grpc/grpc.go | 456 + .../protobuf/protoc-gen-go/link_grpc.go | 34 + .../golang/protobuf/protoc-gen-go/main.go | 98 + .../protobuf/protoc-gen-go/plugin/Makefile | 45 + .../protoc-gen-go/plugin/plugin.pb.go | 225 + .../protoc-gen-go/plugin/plugin.pb.golden | 83 + .../protobuf/protoc-gen-go/testdata/Makefile | 72 + .../testdata/extension_base.proto | 46 + .../testdata/extension_extra.proto | 38 + .../protoc-gen-go/testdata/extension_test.go | 210 + .../testdata/extension_user.proto | 100 + .../protoc-gen-go/testdata/grpc.proto | 59 + .../protoc-gen-go/testdata/imp.pb.go.golden | 113 + .../protobuf/protoc-gen-go/testdata/imp.proto | 70 + .../protoc-gen-go/testdata/imp2.proto | 43 + .../protoc-gen-go/testdata/imp3.proto | 38 + .../protoc-gen-go/testdata/main_test.go | 46 + .../protoc-gen-go/testdata/multi/multi1.proto | 44 + .../protoc-gen-go/testdata/multi/multi2.proto | 46 + .../protoc-gen-go/testdata/multi/multi3.proto | 43 + .../protoc-gen-go/testdata/my_test/test.pb.go | 882 + .../testdata/my_test/test.pb.go.golden | 882 + .../protoc-gen-go/testdata/my_test/test.proto | 156 + .../protoc-gen-go/testdata/proto3.proto | 52 + .../github.com/golang/protobuf/ptypes/any.go | 136 + .../golang/protobuf/ptypes/any/any.pb.go | 111 + .../golang/protobuf/ptypes/any/any.proto | 100 + .../golang/protobuf/ptypes/any_test.go | 113 + .../github.com/golang/protobuf/ptypes/doc.go | 35 + .../golang/protobuf/ptypes/duration.go | 102 + .../protobuf/ptypes/duration/duration.pb.go | 107 + .../protobuf/ptypes/duration/duration.proto | 97 + .../golang/protobuf/ptypes/duration_test.go | 121 + .../golang/protobuf/ptypes/empty/empty.pb.go | 63 + .../golang/protobuf/ptypes/empty/empty.proto | 53 + .../golang/protobuf/ptypes/regen.sh | 72 + .../protobuf/ptypes/struct/struct.pb.go | 376 + .../protobuf/ptypes/struct/struct.proto | 96 + .../golang/protobuf/ptypes/timestamp.go | 125 + .../protobuf/ptypes/timestamp/timestamp.pb.go | 120 + .../protobuf/ptypes/timestamp/timestamp.proto | 111 + .../golang/protobuf/ptypes/timestamp_test.go | 138 + .../protobuf/ptypes/wrappers/wrappers.pb.go | 194 + .../protobuf/ptypes/wrappers/wrappers.proto | 119 + .../inconshreveable/mousetrap/LICENSE | 13 + .../inconshreveable/mousetrap/README.md | 23 + .../inconshreveable/mousetrap/trap_others.go | 15 + .../inconshreveable/mousetrap/trap_windows.go | 98 + .../mousetrap/trap_windows_1.4.go | 46 + .../github.com/pmezard/go-difflib/.travis.yml | 5 + vendor/github.com/pmezard/go-difflib/LICENSE | 27 + .../github.com/pmezard/go-difflib/README.md | 50 + vendor/github.com/spf13/pflag/verify/all.sh | 0 vendor/github.com/spf13/pflag/verify/gofmt.sh | 0 .../github.com/spf13/pflag/verify/golint.sh | 0 vendor/github.com/stretchr/testify/.gitignore | 24 + .../github.com/stretchr/testify/.travis.yml | 15 + .../stretchr/testify/Godeps/Godeps.json | 21 + .../github.com/stretchr/testify/Godeps/Readme | 5 + .../github.com/stretchr/testify/LICENCE.txt | 22 + vendor/github.com/stretchr/testify/LICENSE | 22 + vendor/github.com/stretchr/testify/README.md | 332 + .../stretchr/testify/_codegen/main.go | 287 + vendor/github.com/stretchr/testify/doc.go | 22 + .../github.com/stretchr/testify/http/doc.go | 2 + .../testify/http/test_response_writer.go | 49 + .../testify/http/test_round_tripper.go | 17 + .../github.com/stretchr/testify/mock/doc.go | 44 + .../github.com/stretchr/testify/mock/mock.go | 683 + .../stretchr/testify/mock/mock_test.go | 1068 + .../stretchr/testify/package_test.go | 12 + .../stretchr/testify/require/doc.go | 28 + .../testify/require/forward_requirements.go | 16 + .../require/forward_requirements_test.go | 385 + .../stretchr/testify/require/require.go | 464 + .../stretchr/testify/require/require.go.tmpl | 6 + .../testify/require/require_forward.go | 388 + .../testify/require/require_forward.go.tmpl | 4 + .../stretchr/testify/require/requirements.go | 9 + .../testify/require/requirements_test.go | 369 + .../github.com/stretchr/testify/suite/doc.go | 65 + .../stretchr/testify/suite/interfaces.go | 34 + .../stretchr/testify/suite/suite.go | 115 + .../stretchr/testify/suite/suite_test.go | 239 + vendor/go4.org/.gitignore | 24 + vendor/go4.org/.travis.yml | 10 + vendor/go4.org/AUTHORS | 8 + vendor/go4.org/LICENSE | 202 + vendor/go4.org/README.md | 83 + vendor/go4.org/bytereplacer/bytereplacer.go | 286 + .../go4.org/bytereplacer/bytereplacer_test.go | 423 + .../go4.org/cloud/cloudlaunch/cloudlaunch.go | 445 + .../go4.org/cloud/google/gceutil/gceutil.go | 110 + .../go4.org/cloud/google/gcsutil/storage.go | 180 + vendor/go4.org/ctxutil/ctxutil.go | 44 + vendor/go4.org/errorutil/highlight.go | 2 +- vendor/go4.org/fault/fault.go | 59 + vendor/go4.org/jsonconfig/eval.go | 321 + vendor/go4.org/jsonconfig/jsonconfig.go | 297 + vendor/go4.org/jsonconfig/jsonconfig_test.go | 114 + .../go4.org/jsonconfig/testdata/boolenv.json | 11 + .../go4.org/jsonconfig/testdata/include1.json | 3 + .../jsonconfig/testdata/include1bis.json | 3 + .../go4.org/jsonconfig/testdata/include2.json | 3 + .../jsonconfig/testdata/listexpand.json | 4 + vendor/go4.org/jsonconfig/testdata/loop1.json | 3 + vendor/go4.org/jsonconfig/testdata/loop2.json | 3 + vendor/go4.org/legal/legal.go | 32 + vendor/go4.org/legal/legal_test.go | 29 + vendor/go4.org/lock/.gitignore | 1 + vendor/go4.org/lock/lock.go | 186 + vendor/go4.org/lock/lock_appengine.go | 32 + vendor/go4.org/lock/lock_darwin_amd64.go | 67 + vendor/go4.org/lock/lock_freebsd.go | 66 + vendor/go4.org/lock/lock_linux_amd64.go | 67 + vendor/go4.org/lock/lock_linux_arm.go | 68 + vendor/go4.org/lock/lock_plan9.go | 41 + vendor/go4.org/lock/lock_sigzero.go | 26 + vendor/go4.org/lock/lock_test.go | 222 + vendor/go4.org/net/throttle/throttle.go | 137 + vendor/go4.org/oauthutil/oauth.go | 121 + vendor/go4.org/osutil/exec_plan9.go | 35 + vendor/go4.org/osutil/exec_procfs.go | 42 + vendor/go4.org/osutil/exec_solaris_amd64.go | 71 + vendor/go4.org/osutil/exec_sysctl.go | 63 + vendor/go4.org/osutil/exec_test.go | 94 + vendor/go4.org/osutil/exec_windows.go | 64 + vendor/go4.org/osutil/osutil.go | 32 + vendor/go4.org/readerutil/fakeseeker.go | 70 + vendor/go4.org/readerutil/fakeseeker_test.go | 55 + vendor/go4.org/readerutil/multireaderat.go | 91 + .../go4.org/readerutil/multireaderat_test.go | 48 + vendor/go4.org/readerutil/readerutil.go | 84 + vendor/go4.org/readerutil/readerutil_test.go | 38 + vendor/go4.org/strutil/intern.go | 39 + vendor/go4.org/strutil/strconv.go | 117 + vendor/go4.org/strutil/strutil.go | 200 + vendor/go4.org/strutil/strutil_test.go | 230 + vendor/go4.org/syncutil/gate.go | 42 + vendor/go4.org/syncutil/group.go | 64 + vendor/go4.org/syncutil/once.go | 60 + vendor/go4.org/syncutil/once_test.go | 57 + vendor/go4.org/syncutil/sem.go | 64 + vendor/go4.org/syncutil/sem_test.go | 33 + .../syncutil/singleflight/singleflight.go | 64 + .../singleflight/singleflight_test.go | 85 + .../go4.org/syncutil/syncdebug/syncdebug.go | 198 + .../syncutil/syncdebug/syncdebug_test.go | 30 + vendor/go4.org/syncutil/syncutil.go | 18 + vendor/go4.org/types/types.go | 147 + vendor/go4.org/types/types_test.go | 103 + vendor/go4.org/wkfs/gcs/gcs.go | 196 + vendor/go4.org/wkfs/gcs/gcs_test.go | 84 + vendor/go4.org/wkfs/wkfs.go | 132 + vendor/go4.org/writerutil/writerutil.go | 105 + vendor/go4.org/writerutil/writerutil_test.go | 73 + vendor/golang.org/x/crypto/.gitattributes | 10 + vendor/golang.org/x/crypto/.gitignore | 2 + vendor/golang.org/x/crypto/AUTHORS | 3 + vendor/golang.org/x/crypto/CONTRIBUTING.md | 31 + vendor/golang.org/x/crypto/CONTRIBUTORS | 3 + vendor/golang.org/x/crypto/LICENSE | 27 + vendor/golang.org/x/crypto/PATENTS | 22 + vendor/golang.org/x/crypto/README | 3 + vendor/golang.org/x/crypto/bcrypt/base64.go | 35 + vendor/golang.org/x/crypto/bcrypt/bcrypt.go | 294 + .../golang.org/x/crypto/bcrypt/bcrypt_test.go | 226 + vendor/golang.org/x/crypto/blowfish/block.go | 159 + .../x/crypto/blowfish/blowfish_test.go | 274 + vendor/golang.org/x/crypto/blowfish/cipher.go | 91 + vendor/golang.org/x/crypto/blowfish/const.go | 199 + vendor/golang.org/x/crypto/bn256/bn256.go | 404 + .../golang.org/x/crypto/bn256/bn256_test.go | 304 + vendor/golang.org/x/crypto/bn256/constants.go | 44 + vendor/golang.org/x/crypto/bn256/curve.go | 278 + .../golang.org/x/crypto/bn256/example_test.go | 43 + vendor/golang.org/x/crypto/bn256/gfp12.go | 200 + vendor/golang.org/x/crypto/bn256/gfp2.go | 219 + vendor/golang.org/x/crypto/bn256/gfp6.go | 296 + vendor/golang.org/x/crypto/bn256/optate.go | 395 + vendor/golang.org/x/crypto/bn256/twist.go | 249 + vendor/golang.org/x/crypto/cast5/cast5.go | 2 +- vendor/golang.org/x/crypto/codereview.cfg | 1 + .../x/crypto/curve25519/const_amd64.s | 20 + .../x/crypto/curve25519/cswap_amd64.s | 88 + .../x/crypto/curve25519/curve25519.go | 841 + .../x/crypto/curve25519/curve25519_test.go | 29 + vendor/golang.org/x/crypto/curve25519/doc.go | 23 + .../x/crypto/curve25519/freeze_amd64.s | 94 + .../x/crypto/curve25519/ladderstep_amd64.s | 1398 ++ .../x/crypto/curve25519/mont25519_amd64.go | 240 + .../x/crypto/curve25519/mul_amd64.s | 191 + .../x/crypto/curve25519/square_amd64.s | 153 + .../golang.org/x/crypto/hkdf/example_test.go | 61 + vendor/golang.org/x/crypto/hkdf/hkdf.go | 75 + vendor/golang.org/x/crypto/hkdf/hkdf_test.go | 370 + vendor/golang.org/x/crypto/md4/md4.go | 118 + vendor/golang.org/x/crypto/md4/md4_test.go | 71 + vendor/golang.org/x/crypto/md4/md4block.go | 89 + vendor/golang.org/x/crypto/nacl/box/box.go | 85 + .../golang.org/x/crypto/nacl/box/box_test.go | 78 + .../x/crypto/nacl/secretbox/secretbox.go | 149 + .../x/crypto/nacl/secretbox/secretbox_test.go | 91 + vendor/golang.org/x/crypto/ocsp/ocsp.go | 673 + vendor/golang.org/x/crypto/ocsp/ocsp_test.go | 584 + .../x/crypto/openpgp/armor/armor.go | 2 +- .../x/crypto/openpgp/clearsign/clearsign.go | 2 +- .../x/crypto/openpgp/elgamal/elgamal.go | 2 +- .../x/crypto/openpgp/errors/errors.go | 2 +- .../x/crypto/openpgp/packet/packet.go | 2 +- vendor/golang.org/x/crypto/openpgp/read.go | 2 +- vendor/golang.org/x/crypto/openpgp/s2k/s2k.go | 2 +- .../x/crypto/otr/libotr_test_helper.c | 197 + vendor/golang.org/x/crypto/otr/otr.go | 1408 ++ vendor/golang.org/x/crypto/otr/otr_test.go | 470 + vendor/golang.org/x/crypto/otr/smp.go | 572 + vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go | 77 + .../golang.org/x/crypto/pbkdf2/pbkdf2_test.go | 157 + .../golang.org/x/crypto/pkcs12/bmp-string.go | 50 + .../x/crypto/pkcs12/bmp-string_test.go | 63 + vendor/golang.org/x/crypto/pkcs12/crypto.go | 131 + .../golang.org/x/crypto/pkcs12/crypto_test.go | 125 + vendor/golang.org/x/crypto/pkcs12/errors.go | 23 + .../crypto/pkcs12/internal/rc2/bench_test.go | 27 + .../x/crypto/pkcs12/internal/rc2/rc2.go | 274 + .../x/crypto/pkcs12/internal/rc2/rc2_test.go | 93 + vendor/golang.org/x/crypto/pkcs12/mac.go | 45 + vendor/golang.org/x/crypto/pkcs12/mac_test.go | 42 + vendor/golang.org/x/crypto/pkcs12/pbkdf.go | 170 + .../golang.org/x/crypto/pkcs12/pbkdf_test.go | 34 + vendor/golang.org/x/crypto/pkcs12/pkcs12.go | 342 + .../golang.org/x/crypto/pkcs12/pkcs12_test.go | 138 + vendor/golang.org/x/crypto/pkcs12/safebags.go | 57 + .../x/crypto/poly1305/const_amd64.s | 45 + .../golang.org/x/crypto/poly1305/poly1305.go | 32 + .../x/crypto/poly1305/poly1305_amd64.s | 497 + .../x/crypto/poly1305/poly1305_arm.s | 379 + .../x/crypto/poly1305/poly1305_test.go | 86 + .../golang.org/x/crypto/poly1305/sum_amd64.go | 24 + .../golang.org/x/crypto/poly1305/sum_arm.go | 24 + .../golang.org/x/crypto/poly1305/sum_ref.go | 1531 ++ .../x/crypto/ripemd160/ripemd160.go | 120 + .../x/crypto/ripemd160/ripemd160_test.go | 64 + .../x/crypto/ripemd160/ripemd160block.go | 161 + .../x/crypto/salsa20/salsa/hsalsa20.go | 144 + .../x/crypto/salsa20/salsa/salsa2020_amd64.s | 902 + .../x/crypto/salsa20/salsa/salsa208.go | 199 + .../x/crypto/salsa20/salsa/salsa20_amd64.go | 23 + .../x/crypto/salsa20/salsa/salsa20_ref.go | 234 + .../x/crypto/salsa20/salsa/salsa_test.go | 35 + vendor/golang.org/x/crypto/salsa20/salsa20.go | 54 + .../x/crypto/salsa20/salsa20_test.go | 139 + vendor/golang.org/x/crypto/scrypt/scrypt.go | 243 + .../golang.org/x/crypto/scrypt/scrypt_test.go | 160 + vendor/golang.org/x/crypto/sha3/doc.go | 66 + vendor/golang.org/x/crypto/sha3/hashes.go | 65 + vendor/golang.org/x/crypto/sha3/keccakf.go | 410 + vendor/golang.org/x/crypto/sha3/register.go | 18 + vendor/golang.org/x/crypto/sha3/sha3.go | 193 + vendor/golang.org/x/crypto/sha3/sha3_test.go | 306 + vendor/golang.org/x/crypto/sha3/shake.go | 60 + .../sha3/testdata/keccakKats.json.deflate | Bin 0 -> 521342 bytes vendor/golang.org/x/crypto/sha3/xor.go | 16 + .../golang.org/x/crypto/sha3/xor_generic.go | 28 + .../golang.org/x/crypto/sha3/xor_unaligned.go | 58 + .../golang.org/x/crypto/ssh/agent/client.go | 616 + .../x/crypto/ssh/agent/client_test.go | 287 + .../x/crypto/ssh/agent/example_test.go | 40 + .../golang.org/x/crypto/ssh/agent/forward.go | 103 + .../golang.org/x/crypto/ssh/agent/keyring.go | 184 + .../x/crypto/ssh/agent/keyring_test.go | 78 + .../golang.org/x/crypto/ssh/agent/server.go | 209 + .../x/crypto/ssh/agent/server_test.go | 77 + .../x/crypto/ssh/agent/testdata_test.go | 64 + .../golang.org/x/crypto/ssh/benchmark_test.go | 122 + vendor/golang.org/x/crypto/ssh/buffer.go | 98 + vendor/golang.org/x/crypto/ssh/buffer_test.go | 87 + vendor/golang.org/x/crypto/ssh/certs.go | 501 + vendor/golang.org/x/crypto/ssh/certs_test.go | 216 + vendor/golang.org/x/crypto/ssh/channel.go | 631 + vendor/golang.org/x/crypto/ssh/cipher.go | 552 + vendor/golang.org/x/crypto/ssh/cipher_test.go | 127 + vendor/golang.org/x/crypto/ssh/client.go | 213 + vendor/golang.org/x/crypto/ssh/client_auth.go | 441 + .../x/crypto/ssh/client_auth_test.go | 393 + vendor/golang.org/x/crypto/ssh/client_test.go | 39 + vendor/golang.org/x/crypto/ssh/common.go | 354 + vendor/golang.org/x/crypto/ssh/connection.go | 144 + vendor/golang.org/x/crypto/ssh/doc.go | 18 + .../golang.org/x/crypto/ssh/example_test.go | 211 + vendor/golang.org/x/crypto/ssh/handshake.go | 412 + .../golang.org/x/crypto/ssh/handshake_test.go | 415 + vendor/golang.org/x/crypto/ssh/kex.go | 526 + vendor/golang.org/x/crypto/ssh/kex_test.go | 50 + vendor/golang.org/x/crypto/ssh/keys.go | 720 + vendor/golang.org/x/crypto/ssh/keys_test.go | 437 + vendor/golang.org/x/crypto/ssh/mac.go | 57 + .../golang.org/x/crypto/ssh/mempipe_test.go | 110 + vendor/golang.org/x/crypto/ssh/messages.go | 725 + .../golang.org/x/crypto/ssh/messages_test.go | 254 + vendor/golang.org/x/crypto/ssh/mux.go | 356 + vendor/golang.org/x/crypto/ssh/mux_test.go | 525 + vendor/golang.org/x/crypto/ssh/server.go | 495 + vendor/golang.org/x/crypto/ssh/session.go | 605 + .../golang.org/x/crypto/ssh/session_test.go | 774 + vendor/golang.org/x/crypto/ssh/tcpip.go | 407 + vendor/golang.org/x/crypto/ssh/tcpip_test.go | 20 + .../x/crypto/ssh/terminal/terminal.go | 892 + .../x/crypto/ssh/terminal/terminal_test.go | 269 + .../golang.org/x/crypto/ssh/terminal/util.go | 128 + .../x/crypto/ssh/terminal/util_bsd.go | 12 + .../x/crypto/ssh/terminal/util_linux.go | 11 + .../x/crypto/ssh/terminal/util_windows.go | 174 + .../x/crypto/ssh/test/agent_unix_test.go | 59 + .../golang.org/x/crypto/ssh/test/cert_test.go | 47 + vendor/golang.org/x/crypto/ssh/test/doc.go | 7 + .../x/crypto/ssh/test/forward_unix_test.go | 160 + .../x/crypto/ssh/test/session_test.go | 340 + .../x/crypto/ssh/test/tcpip_test.go | 46 + .../x/crypto/ssh/test/test_unix_test.go | 261 + .../x/crypto/ssh/test/testdata_test.go | 64 + .../golang.org/x/crypto/ssh/testdata/doc.go | 8 + .../golang.org/x/crypto/ssh/testdata/keys.go | 43 + .../golang.org/x/crypto/ssh/testdata_test.go | 63 + vendor/golang.org/x/crypto/ssh/transport.go | 332 + .../golang.org/x/crypto/ssh/transport_test.go | 109 + vendor/golang.org/x/crypto/tea/cipher.go | 109 + vendor/golang.org/x/crypto/tea/tea_test.go | 93 + vendor/golang.org/x/crypto/twofish/twofish.go | 342 + .../x/crypto/twofish/twofish_test.go | 129 + vendor/golang.org/x/crypto/xtea/block.go | 66 + vendor/golang.org/x/crypto/xtea/cipher.go | 82 + vendor/golang.org/x/crypto/xtea/xtea_test.go | 229 + vendor/golang.org/x/crypto/xts/xts.go | 138 + vendor/golang.org/x/crypto/xts/xts_test.go | 85 + vendor/golang.org/x/net/.gitattributes | 10 + vendor/golang.org/x/net/.gitignore | 2 + vendor/golang.org/x/net/AUTHORS | 3 + vendor/golang.org/x/net/CONTRIBUTING.md | 31 + vendor/golang.org/x/net/CONTRIBUTORS | 3 + vendor/golang.org/x/net/LICENSE | 27 + vendor/golang.org/x/net/PATENTS | 22 + vendor/golang.org/x/net/README | 3 + vendor/golang.org/x/net/bpf/asm.go | 41 + vendor/golang.org/x/net/bpf/constants.go | 215 + vendor/golang.org/x/net/bpf/doc.go | 81 + vendor/golang.org/x/net/bpf/instructions.go | 434 + .../golang.org/x/net/bpf/instructions_test.go | 184 + .../x/net/bpf/testdata/all_instructions.bpf | 1 + .../x/net/bpf/testdata/all_instructions.txt | 79 + vendor/golang.org/x/net/codereview.cfg | 1 + vendor/golang.org/x/net/context/context.go | 2 +- .../x/net/context/ctxhttp/ctxhttp.go | 2 +- vendor/golang.org/x/net/dict/dict.go | 210 + vendor/golang.org/x/net/html/atom/atom.go | 78 + .../golang.org/x/net/html/atom/atom_test.go | 109 + vendor/golang.org/x/net/html/atom/gen.go | 648 + vendor/golang.org/x/net/html/atom/table.go | 713 + .../golang.org/x/net/html/atom/table_test.go | 351 + .../golang.org/x/net/html/charset/charset.go | 257 + .../x/net/html/charset/charset_test.go | 237 + .../html/charset/testdata/HTTP-charset.html | 48 + .../charset/testdata/HTTP-vs-UTF-8-BOM.html | 48 + .../testdata/HTTP-vs-meta-charset.html | 49 + .../testdata/HTTP-vs-meta-content.html | 49 + .../testdata/No-encoding-declaration.html | 47 + .../x/net/html/charset/testdata/README | 9 + .../html/charset/testdata/UTF-16BE-BOM.html | Bin 0 -> 2670 bytes .../html/charset/testdata/UTF-16LE-BOM.html | Bin 0 -> 2682 bytes .../testdata/UTF-8-BOM-vs-meta-charset.html | 49 + .../testdata/UTF-8-BOM-vs-meta-content.html | 48 + .../testdata/meta-charset-attribute.html | 48 + .../testdata/meta-content-attribute.html | 48 + vendor/golang.org/x/net/html/const.go | 102 + vendor/golang.org/x/net/html/doc.go | 106 + vendor/golang.org/x/net/html/doctype.go | 156 + vendor/golang.org/x/net/html/entity.go | 2253 ++ vendor/golang.org/x/net/html/entity_test.go | 29 + vendor/golang.org/x/net/html/escape.go | 258 + vendor/golang.org/x/net/html/escape_test.go | 97 + vendor/golang.org/x/net/html/example_test.go | 40 + vendor/golang.org/x/net/html/foreign.go | 226 + vendor/golang.org/x/net/html/node.go | 193 + vendor/golang.org/x/net/html/node_test.go | 146 + vendor/golang.org/x/net/html/parse.go | 2094 ++ vendor/golang.org/x/net/html/parse_test.go | 388 + vendor/golang.org/x/net/html/render.go | 271 + vendor/golang.org/x/net/html/render_test.go | 156 + .../golang.org/x/net/html/testdata/go1.html | 2237 ++ .../x/net/html/testdata/webkit/README | 28 + .../x/net/html/testdata/webkit/adoption01.dat | 194 + .../x/net/html/testdata/webkit/adoption02.dat | 31 + .../x/net/html/testdata/webkit/comments01.dat | 135 + .../x/net/html/testdata/webkit/doctype01.dat | 370 + .../x/net/html/testdata/webkit/entities01.dat | 603 + .../x/net/html/testdata/webkit/entities02.dat | 249 + .../html/testdata/webkit/html5test-com.dat | 246 + .../x/net/html/testdata/webkit/inbody01.dat | 43 + .../x/net/html/testdata/webkit/isindex.dat | 40 + ...pending-spec-changes-plain-text-unsafe.dat | Bin 0 -> 115 bytes .../testdata/webkit/pending-spec-changes.dat | 52 + .../testdata/webkit/plain-text-unsafe.dat | Bin 0 -> 4166 bytes .../net/html/testdata/webkit/scriptdata01.dat | 308 + .../testdata/webkit/scripted/adoption01.dat | 15 + .../testdata/webkit/scripted/webkit01.dat | 28 + .../x/net/html/testdata/webkit/tables01.dat | 212 + .../x/net/html/testdata/webkit/tests1.dat | 1952 ++ .../x/net/html/testdata/webkit/tests10.dat | 799 + .../x/net/html/testdata/webkit/tests11.dat | 482 + .../x/net/html/testdata/webkit/tests12.dat | 62 + .../x/net/html/testdata/webkit/tests14.dat | 74 + .../x/net/html/testdata/webkit/tests15.dat | 208 + .../x/net/html/testdata/webkit/tests16.dat | 2299 ++ .../x/net/html/testdata/webkit/tests17.dat | 153 + .../x/net/html/testdata/webkit/tests18.dat | 269 + .../x/net/html/testdata/webkit/tests19.dat | 1237 + .../x/net/html/testdata/webkit/tests2.dat | 763 + .../x/net/html/testdata/webkit/tests20.dat | 455 + .../x/net/html/testdata/webkit/tests21.dat | 221 + .../x/net/html/testdata/webkit/tests22.dat | 157 + .../x/net/html/testdata/webkit/tests23.dat | 155 + .../x/net/html/testdata/webkit/tests24.dat | 79 + .../x/net/html/testdata/webkit/tests25.dat | 219 + .../x/net/html/testdata/webkit/tests26.dat | 313 + .../x/net/html/testdata/webkit/tests3.dat | 305 + .../x/net/html/testdata/webkit/tests4.dat | 59 + .../x/net/html/testdata/webkit/tests5.dat | 191 + .../x/net/html/testdata/webkit/tests6.dat | 663 + .../x/net/html/testdata/webkit/tests7.dat | 390 + .../x/net/html/testdata/webkit/tests8.dat | 148 + .../x/net/html/testdata/webkit/tests9.dat | 457 + .../testdata/webkit/tests_innerHTML_1.dat | 741 + .../x/net/html/testdata/webkit/tricky01.dat | 261 + .../x/net/html/testdata/webkit/webkit01.dat | 610 + .../x/net/html/testdata/webkit/webkit02.dat | 159 + vendor/golang.org/x/net/html/token.go | 1219 + vendor/golang.org/x/net/html/token_test.go | 748 + vendor/golang.org/x/net/icmp/dstunreach.go | 41 + vendor/golang.org/x/net/icmp/echo.go | 45 + vendor/golang.org/x/net/icmp/endpoint.go | 113 + vendor/golang.org/x/net/icmp/example_test.go | 63 + vendor/golang.org/x/net/icmp/extension.go | 89 + .../golang.org/x/net/icmp/extension_test.go | 259 + vendor/golang.org/x/net/icmp/helper.go | 27 + vendor/golang.org/x/net/icmp/helper_posix.go | 75 + vendor/golang.org/x/net/icmp/interface.go | 236 + vendor/golang.org/x/net/icmp/ipv4.go | 56 + vendor/golang.org/x/net/icmp/ipv4_test.go | 71 + vendor/golang.org/x/net/icmp/ipv6.go | 23 + vendor/golang.org/x/net/icmp/listen_posix.go | 98 + vendor/golang.org/x/net/icmp/listen_stub.go | 33 + vendor/golang.org/x/net/icmp/message.go | 150 + vendor/golang.org/x/net/icmp/message_test.go | 134 + vendor/golang.org/x/net/icmp/messagebody.go | 41 + vendor/golang.org/x/net/icmp/mpls.go | 77 + vendor/golang.org/x/net/icmp/multipart.go | 109 + .../golang.org/x/net/icmp/multipart_test.go | 442 + vendor/golang.org/x/net/icmp/packettoobig.go | 43 + vendor/golang.org/x/net/icmp/paramprob.go | 63 + vendor/golang.org/x/net/icmp/ping_test.go | 166 + vendor/golang.org/x/net/icmp/sys_freebsd.go | 11 + vendor/golang.org/x/net/icmp/timeexceeded.go | 39 + vendor/golang.org/x/net/idna/idna.go | 68 + vendor/golang.org/x/net/idna/idna_test.go | 43 + vendor/golang.org/x/net/idna/punycode.go | 200 + vendor/golang.org/x/net/idna/punycode_test.go | 198 + .../golang.org/x/net/internal/iana/const.go | 180 + vendor/golang.org/x/net/internal/iana/gen.go | 293 + .../x/net/internal/nettest/error_posix.go | 31 + .../x/net/internal/nettest/error_stub.go | 11 + .../x/net/internal/nettest/interface.go | 94 + .../x/net/internal/nettest/rlimit.go | 11 + .../x/net/internal/nettest/rlimit_stub.go | 9 + .../x/net/internal/nettest/rlimit_unix.go | 17 + .../x/net/internal/nettest/rlimit_windows.go | 7 + .../x/net/internal/nettest/stack.go | 36 + .../x/net/internal/nettest/stack_stub.go | 18 + .../x/net/internal/nettest/stack_unix.go | 22 + .../x/net/internal/nettest/stack_windows.go | 32 + .../x/net/internal/timeseries/timeseries.go | 2 +- vendor/golang.org/x/net/ipv4/control.go | 70 + vendor/golang.org/x/net/ipv4/control_bsd.go | 40 + .../golang.org/x/net/ipv4/control_pktinfo.go | 37 + vendor/golang.org/x/net/ipv4/control_stub.go | 23 + vendor/golang.org/x/net/ipv4/control_unix.go | 164 + .../golang.org/x/net/ipv4/control_windows.go | 27 + vendor/golang.org/x/net/ipv4/defs_darwin.go | 77 + .../golang.org/x/net/ipv4/defs_dragonfly.go | 38 + vendor/golang.org/x/net/ipv4/defs_freebsd.go | 75 + vendor/golang.org/x/net/ipv4/defs_linux.go | 111 + vendor/golang.org/x/net/ipv4/defs_netbsd.go | 37 + vendor/golang.org/x/net/ipv4/defs_openbsd.go | 37 + vendor/golang.org/x/net/ipv4/defs_solaris.go | 57 + .../golang.org/x/net/ipv4/dgramopt_posix.go | 251 + vendor/golang.org/x/net/ipv4/dgramopt_stub.go | 106 + vendor/golang.org/x/net/ipv4/doc.go | 242 + vendor/golang.org/x/net/ipv4/endpoint.go | 187 + vendor/golang.org/x/net/ipv4/example_test.go | 224 + vendor/golang.org/x/net/ipv4/gen.go | 208 + .../golang.org/x/net/ipv4/genericopt_posix.go | 59 + .../golang.org/x/net/ipv4/genericopt_stub.go | 29 + vendor/golang.org/x/net/ipv4/header.go | 132 + vendor/golang.org/x/net/ipv4/header_test.go | 119 + vendor/golang.org/x/net/ipv4/helper.go | 59 + vendor/golang.org/x/net/ipv4/helper_stub.go | 23 + vendor/golang.org/x/net/ipv4/helper_unix.go | 50 + .../golang.org/x/net/ipv4/helper_windows.go | 49 + vendor/golang.org/x/net/ipv4/iana.go | 34 + vendor/golang.org/x/net/ipv4/icmp.go | 57 + vendor/golang.org/x/net/ipv4/icmp_linux.go | 25 + vendor/golang.org/x/net/ipv4/icmp_stub.go | 25 + vendor/golang.org/x/net/ipv4/icmp_test.go | 95 + .../x/net/ipv4/mocktransponder_test.go | 21 + .../golang.org/x/net/ipv4/multicast_test.go | 330 + .../x/net/ipv4/multicastlistener_test.go | 249 + .../x/net/ipv4/multicastsockopt_test.go | 195 + vendor/golang.org/x/net/ipv4/packet.go | 97 + vendor/golang.org/x/net/ipv4/payload.go | 15 + vendor/golang.org/x/net/ipv4/payload_cmsg.go | 81 + .../golang.org/x/net/ipv4/payload_nocmsg.go | 42 + .../golang.org/x/net/ipv4/readwrite_test.go | 174 + vendor/golang.org/x/net/ipv4/sockopt.go | 46 + .../golang.org/x/net/ipv4/sockopt_asmreq.go | 83 + .../x/net/ipv4/sockopt_asmreq_stub.go | 21 + .../x/net/ipv4/sockopt_asmreq_unix.go | 46 + .../x/net/ipv4/sockopt_asmreq_windows.go | 45 + .../x/net/ipv4/sockopt_asmreqn_stub.go | 17 + .../x/net/ipv4/sockopt_asmreqn_unix.go | 42 + .../x/net/ipv4/sockopt_ssmreq_stub.go | 17 + .../x/net/ipv4/sockopt_ssmreq_unix.go | 61 + vendor/golang.org/x/net/ipv4/sockopt_stub.go | 11 + vendor/golang.org/x/net/ipv4/sockopt_unix.go | 122 + .../golang.org/x/net/ipv4/sockopt_windows.go | 68 + vendor/golang.org/x/net/ipv4/sys_bsd.go | 34 + vendor/golang.org/x/net/ipv4/sys_darwin.go | 96 + vendor/golang.org/x/net/ipv4/sys_freebsd.go | 73 + vendor/golang.org/x/net/ipv4/sys_linux.go | 55 + vendor/golang.org/x/net/ipv4/sys_openbsd.go | 32 + vendor/golang.org/x/net/ipv4/sys_stub.go | 13 + vendor/golang.org/x/net/ipv4/sys_windows.go | 61 + .../x/net/ipv4/syscall_linux_386.go | 31 + vendor/golang.org/x/net/ipv4/syscall_unix.go | 26 + .../golang.org/x/net/ipv4/thunk_linux_386.s | 8 + vendor/golang.org/x/net/ipv4/unicast_test.go | 246 + .../x/net/ipv4/unicastsockopt_test.go | 139 + vendor/golang.org/x/net/ipv4/zsys_darwin.go | 99 + .../golang.org/x/net/ipv4/zsys_dragonfly.go | 33 + .../golang.org/x/net/ipv4/zsys_freebsd_386.go | 93 + .../x/net/ipv4/zsys_freebsd_amd64.go | 95 + .../golang.org/x/net/ipv4/zsys_freebsd_arm.go | 95 + .../golang.org/x/net/ipv4/zsys_linux_386.go | 130 + .../golang.org/x/net/ipv4/zsys_linux_amd64.go | 132 + .../golang.org/x/net/ipv4/zsys_linux_arm.go | 130 + .../golang.org/x/net/ipv4/zsys_linux_arm64.go | 134 + .../x/net/ipv4/zsys_linux_mips64.go | 134 + .../x/net/ipv4/zsys_linux_mips64le.go | 134 + .../golang.org/x/net/ipv4/zsys_linux_ppc64.go | 134 + .../x/net/ipv4/zsys_linux_ppc64le.go | 134 + vendor/golang.org/x/net/ipv4/zsys_netbsd.go | 30 + vendor/golang.org/x/net/ipv4/zsys_openbsd.go | 30 + vendor/golang.org/x/net/ipv4/zsys_solaris.go | 60 + vendor/golang.org/x/net/ipv6/control.go | 85 + .../x/net/ipv6/control_rfc2292_unix.go | 55 + .../x/net/ipv6/control_rfc3542_unix.go | 99 + vendor/golang.org/x/net/ipv6/control_stub.go | 23 + vendor/golang.org/x/net/ipv6/control_unix.go | 166 + .../golang.org/x/net/ipv6/control_windows.go | 27 + vendor/golang.org/x/net/ipv6/defs_darwin.go | 112 + .../golang.org/x/net/ipv6/defs_dragonfly.go | 84 + vendor/golang.org/x/net/ipv6/defs_freebsd.go | 105 + vendor/golang.org/x/net/ipv6/defs_linux.go | 136 + vendor/golang.org/x/net/ipv6/defs_netbsd.go | 80 + vendor/golang.org/x/net/ipv6/defs_openbsd.go | 89 + vendor/golang.org/x/net/ipv6/defs_solaris.go | 96 + .../golang.org/x/net/ipv6/dgramopt_posix.go | 288 + vendor/golang.org/x/net/ipv6/dgramopt_stub.go | 119 + vendor/golang.org/x/net/ipv6/doc.go | 240 + vendor/golang.org/x/net/ipv6/endpoint.go | 123 + vendor/golang.org/x/net/ipv6/example_test.go | 216 + vendor/golang.org/x/net/ipv6/gen.go | 208 + .../golang.org/x/net/ipv6/genericopt_posix.go | 60 + .../golang.org/x/net/ipv6/genericopt_stub.go | 30 + vendor/golang.org/x/net/ipv6/header.go | 55 + vendor/golang.org/x/net/ipv6/header_test.go | 55 + vendor/golang.org/x/net/ipv6/helper.go | 53 + vendor/golang.org/x/net/ipv6/helper_stub.go | 19 + vendor/golang.org/x/net/ipv6/helper_unix.go | 46 + .../golang.org/x/net/ipv6/helper_windows.go | 45 + vendor/golang.org/x/net/ipv6/iana.go | 82 + vendor/golang.org/x/net/ipv6/icmp.go | 57 + vendor/golang.org/x/net/ipv6/icmp_bsd.go | 29 + vendor/golang.org/x/net/ipv6/icmp_linux.go | 27 + vendor/golang.org/x/net/ipv6/icmp_solaris.go | 24 + vendor/golang.org/x/net/ipv6/icmp_stub.go | 23 + vendor/golang.org/x/net/ipv6/icmp_test.go | 96 + vendor/golang.org/x/net/ipv6/icmp_windows.go | 26 + .../x/net/ipv6/mocktransponder_test.go | 32 + .../golang.org/x/net/ipv6/multicast_test.go | 260 + .../x/net/ipv6/multicastlistener_test.go | 246 + .../x/net/ipv6/multicastsockopt_test.go | 157 + vendor/golang.org/x/net/ipv6/payload.go | 15 + vendor/golang.org/x/net/ipv6/payload_cmsg.go | 70 + .../golang.org/x/net/ipv6/payload_nocmsg.go | 41 + .../golang.org/x/net/ipv6/readwrite_test.go | 189 + vendor/golang.org/x/net/ipv6/sockopt.go | 46 + .../x/net/ipv6/sockopt_asmreq_unix.go | 22 + .../x/net/ipv6/sockopt_asmreq_windows.go | 21 + .../x/net/ipv6/sockopt_ssmreq_stub.go | 17 + .../x/net/ipv6/sockopt_ssmreq_unix.go | 59 + vendor/golang.org/x/net/ipv6/sockopt_stub.go | 13 + vendor/golang.org/x/net/ipv6/sockopt_test.go | 133 + vendor/golang.org/x/net/ipv6/sockopt_unix.go | 122 + .../golang.org/x/net/ipv6/sockopt_windows.go | 86 + vendor/golang.org/x/net/ipv6/sys_bsd.go | 56 + vendor/golang.org/x/net/ipv6/sys_darwin.go | 133 + vendor/golang.org/x/net/ipv6/sys_freebsd.go | 91 + vendor/golang.org/x/net/ipv6/sys_linux.go | 72 + vendor/golang.org/x/net/ipv6/sys_stub.go | 13 + vendor/golang.org/x/net/ipv6/sys_windows.go | 63 + .../x/net/ipv6/syscall_linux_386.go | 31 + vendor/golang.org/x/net/ipv6/syscall_unix.go | 26 + .../golang.org/x/net/ipv6/thunk_linux_386.s | 8 + vendor/golang.org/x/net/ipv6/unicast_test.go | 182 + .../x/net/ipv6/unicastsockopt_test.go | 111 + vendor/golang.org/x/net/ipv6/zsys_darwin.go | 131 + .../golang.org/x/net/ipv6/zsys_dragonfly.go | 90 + .../golang.org/x/net/ipv6/zsys_freebsd_386.go | 122 + .../x/net/ipv6/zsys_freebsd_amd64.go | 124 + .../golang.org/x/net/ipv6/zsys_freebsd_arm.go | 124 + .../golang.org/x/net/ipv6/zsys_linux_386.go | 152 + .../golang.org/x/net/ipv6/zsys_linux_amd64.go | 154 + .../golang.org/x/net/ipv6/zsys_linux_arm.go | 152 + .../golang.org/x/net/ipv6/zsys_linux_arm64.go | 156 + .../x/net/ipv6/zsys_linux_mips64.go | 156 + .../x/net/ipv6/zsys_linux_mips64le.go | 156 + .../golang.org/x/net/ipv6/zsys_linux_ppc64.go | 156 + .../x/net/ipv6/zsys_linux_ppc64le.go | 156 + vendor/golang.org/x/net/ipv6/zsys_netbsd.go | 84 + vendor/golang.org/x/net/ipv6/zsys_openbsd.go | 93 + vendor/golang.org/x/net/ipv6/zsys_solaris.go | 105 + vendor/golang.org/x/net/netutil/listen.go | 48 + .../golang.org/x/net/netutil/listen_test.go | 101 + vendor/golang.org/x/net/proxy/direct.go | 18 + vendor/golang.org/x/net/proxy/per_host.go | 140 + .../golang.org/x/net/proxy/per_host_test.go | 55 + vendor/golang.org/x/net/proxy/proxy.go | 94 + vendor/golang.org/x/net/proxy/proxy_test.go | 142 + vendor/golang.org/x/net/proxy/socks5.go | 210 + vendor/golang.org/x/net/publicsuffix/gen.go | 663 + vendor/golang.org/x/net/publicsuffix/list.go | 133 + .../x/net/publicsuffix/list_test.go | 416 + vendor/golang.org/x/net/publicsuffix/table.go | 8786 +++++++ .../x/net/publicsuffix/table_test.go | 15751 +++++++++++++ vendor/golang.org/x/net/trace/trace.go | 2 +- vendor/golang.org/x/net/webdav/file.go | 794 + vendor/golang.org/x/net/webdav/file_test.go | 1166 + vendor/golang.org/x/net/webdav/if.go | 173 + vendor/golang.org/x/net/webdav/if_test.go | 322 + .../x/net/webdav/internal/xml/README | 11 + .../x/net/webdav/internal/xml/atom_test.go | 56 + .../x/net/webdav/internal/xml/example_test.go | 151 + .../x/net/webdav/internal/xml/marshal.go | 1223 + .../x/net/webdav/internal/xml/marshal_test.go | 1939 ++ .../x/net/webdav/internal/xml/read.go | 692 + .../x/net/webdav/internal/xml/read_test.go | 744 + .../x/net/webdav/internal/xml/typeinfo.go | 371 + .../x/net/webdav/internal/xml/xml.go | 1998 ++ .../x/net/webdav/internal/xml/xml_test.go | 752 + .../x/net/webdav/litmus_test_server.go | 94 + vendor/golang.org/x/net/webdav/lock.go | 445 + vendor/golang.org/x/net/webdav/lock_test.go | 731 + vendor/golang.org/x/net/webdav/prop.go | 388 + vendor/golang.org/x/net/webdav/prop_test.go | 606 + vendor/golang.org/x/net/webdav/webdav.go | 686 + vendor/golang.org/x/net/webdav/webdav_test.go | 242 + vendor/golang.org/x/net/webdav/xml.go | 519 + vendor/golang.org/x/net/webdav/xml_test.go | 906 + vendor/golang.org/x/net/websocket/client.go | 113 + .../x/net/websocket/exampledial_test.go | 31 + .../x/net/websocket/examplehandler_test.go | 26 + vendor/golang.org/x/net/websocket/hybi.go | 586 + .../golang.org/x/net/websocket/hybi_test.go | 608 + vendor/golang.org/x/net/websocket/server.go | 113 + .../golang.org/x/net/websocket/websocket.go | 411 + .../x/net/websocket/websocket_test.go | 587 + vendor/golang.org/x/net/xsrftoken/xsrf.go | 88 + .../golang.org/x/net/xsrftoken/xsrf_test.go | 83 + vendor/google.golang.org/grpc/codegen.sh | 0 vendor/google.golang.org/grpc/codes/codes.go | 2 +- vendor/google.golang.org/grpc/coverage.sh | 0 .../grpc/credentials/credentials.go | 2 +- vendor/google.golang.org/grpc/doc.go | 2 +- .../google.golang.org/grpc/grpclog/logger.go | 2 +- .../grpc/interop/grpc_testing/test.pb.go | 0 .../grpc/metadata/metadata.go | 2 +- .../grpc/transport/transport.go | 2 +- 3185 files changed, 633008 insertions(+), 25383 deletions(-) create mode 100644 glide.lock create mode 100644 glide.yaml create mode 100644 vendor/github.com/camlistore/camlistore/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/.hackfests/2010-12-01.txt create mode 100644 vendor/github.com/camlistore/camlistore/.hackfests/2012-11-03.txt create mode 100644 vendor/github.com/camlistore/camlistore/.hackfests/2012-12-23.txt create mode 100644 vendor/github.com/camlistore/camlistore/.hackfests/2013-01-20.txt create mode 100644 vendor/github.com/camlistore/camlistore/.hackfests/2013-12-27.txt create mode 100644 vendor/github.com/camlistore/camlistore/.header create mode 100644 vendor/github.com/camlistore/camlistore/AUTHORS create mode 100644 vendor/github.com/camlistore/camlistore/BUILDING create mode 100644 vendor/github.com/camlistore/camlistore/CONTRIBUTORS rename vendor/github.com/{coreos/ignition/config/vendor/github.com/coreos/go-semver/LICENSE => camlistore/camlistore/COPYING} (100%) create mode 100644 vendor/github.com/camlistore/camlistore/Dockerfile create mode 100644 vendor/github.com/camlistore/camlistore/HACKING create mode 100644 vendor/github.com/camlistore/camlistore/Makefile create mode 100644 vendor/github.com/camlistore/camlistore/README create mode 100644 vendor/github.com/camlistore/camlistore/TESTS create mode 100644 vendor/github.com/camlistore/camlistore/TODO create mode 100644 vendor/github.com/camlistore/camlistore/app/hello/main.go create mode 100644 vendor/github.com/camlistore/camlistore/app/publisher/fileembed.go create mode 100644 vendor/github.com/camlistore/camlistore/app/publisher/gallery.html create mode 100644 vendor/github.com/camlistore/camlistore/app/publisher/main.go create mode 100644 vendor/github.com/camlistore/camlistore/app/publisher/pics.css create mode 100644 vendor/github.com/camlistore/camlistore/app/publisher/publish_test.go create mode 100644 vendor/github.com/camlistore/camlistore/app/publisher/zip.go create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/.classpath create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/.project create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/.settings/org.eclipse.jdt.core.prefs create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/.settings/org.eclipse.jdt.ui.prefs create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/AndroidManifest.xml create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/Makefile create mode 100755 vendor/github.com/camlistore/camlistore/clients/android/build-in-docker.pl create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/build.properties create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/build.xml create mode 100755 vendor/github.com/camlistore/camlistore/clients/android/check-environment.pl create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/default.properties create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/devenv/Dockerfile create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/local.properties.TEMPLATE create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/project.properties create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/res/drawable-hdpi/icon.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/res/drawable-mdpi/icon.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/res/drawable-xhdpi/icon.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/res/drawable/icon_file.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/res/drawable/icon_folder.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/res/layout/main.xml create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/res/values/strings.xml create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/res/xml/preferences.xml create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/com/google/zxing/integration/android/IntentIntegrator.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/com/google/zxing/integration/android/IntentResult.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/CamliActivity.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/CamliFileObserver.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/DummyNullCallback.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/HostPort.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/IStatusCallback.aidl create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/IUploadService.aidl create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/OnAlarmReceiver.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/OnBootReceiver.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/Preferences.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/QRPreference.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/QueuedFile.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/SettingsActivity.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/UploadApplication.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/UploadService.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/UploadThread.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/Util.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/src/org/camlistore/WifiPowerReceiver.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/.classpath create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/.project create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/.settings/org.eclipse.jdt.core.prefs create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/AndroidManifest.xml create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/build.properties create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/build.xml create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/default.properties create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/project.properties create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/res/drawable-hdpi/icon.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/res/drawable-ldpi/icon.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/res/drawable-mdpi/icon.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/res/layout/main.xml create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/res/values/strings.xml create mode 100644 vendor/github.com/camlistore/camlistore/clients/android/test/src/org/camlistore/CamliActivityTest.java create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/Crypto.js create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/SHA1.js create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/background.html create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/base64.js create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/chrome_ex_oauth.html create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/chrome_ex_oauth.js create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/chrome_ex_oauthsimple.js create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/icon128.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/icon19.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/icon48.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-bg_diagonals-thick_18_b81900_40x40.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-bg_diagonals-thick_20_666666_40x40.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-bg_flat_10_000000_40x100.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-bg_glass_100_f6f6f6_1x400.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-bg_glass_100_fdf5ce_1x400.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-bg_glass_65_ffffff_1x400.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-bg_gloss-wave_35_f6a828_500x100.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-bg_highlight-soft_100_eeeeee_1x100.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-bg_highlight-soft_75_ffe45c_1x100.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-icons_222222_256x240.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-icons_228ef1_256x240.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-icons_ef8c08_256x240.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-icons_ffd27a_256x240.png create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/images/ui-icons_ffffff_256x240.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/jquery-1.4.2.min.js create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/jquery-ui-1.8.5.custom.css create mode 100755 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/jquery-ui-1.8.5.custom.min.js create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/json2.js create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/manifest.json create mode 100644 vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/options.html create mode 100755 vendor/github.com/camlistore/camlistore/clients/curl/example.sh create mode 100644 vendor/github.com/camlistore/camlistore/clients/curl/test_data.txt create mode 100755 vendor/github.com/camlistore/camlistore/clients/curl/upload-file.pl create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/Podfile create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/Podfile.lock create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/Readme.md create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup.xcodeproj/project.pbxproj create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup.xcworkspace/contents.xcworkspacedata create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup.xcworkspace/xcshareddata/photobackup.xccheckout create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Base.lproj/Main_iPad.storyboard create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Base.lproj/Main_iPhone.storyboard create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/LaunchImage.launchimage/Contents.json create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/LaunchImage.launchimage/startup-r4.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/LaunchImage.launchimage/startup-small.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAAppDelegate.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAAppDelegate.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliClient.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliClient.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliFile.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliFile.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUploadOperation.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUploadOperation.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUtil.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUtil.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAViewController.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAViewController.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/SettingsViewController.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/SettingsViewController.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadStatusCell.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadStatusCell.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadTaskCell.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadTaskCell.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/en.lproj/InfoPlist.strings create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/main.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/photobackup-Info.plist create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/photobackup-Prefix.pch create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackupTests/en.lproj/InfoPlist.strings create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackupTests/photobackupTests-Info.plist create mode 100644 vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackupTests/photobackupTests.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/js/README create mode 100644 vendor/github.com/camlistore/camlistore/clients/js/camel.jpg create mode 100644 vendor/github.com/camlistore/camlistore/clients/js/client.js create mode 100644 vendor/github.com/camlistore/camlistore/clients/js/index.html create mode 100644 vendor/github.com/camlistore/camlistore/clients/js/style.css create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/BUILDING create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore.xcodeproj/project.pbxproj create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/AppDelegate.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/AppDelegate.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Base.lproj/MainMenu.xib create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Camlicon.icns create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Camlistore-Info.plist create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Camlistore-Prefix.pch create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Credits.html create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/FUSEManager.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/FUSEManager.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/LoginItemManager.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/LoginItemManager.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/TimeTravelWindowController.h create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/TimeTravelWindowController.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/TimeTravelWindowController.xib create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/en.lproj/InfoPlist.strings create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/main.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/make-dmg.sh create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/menuicon-selected.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/menuicon-selected@2x.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/menuicon.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/menuicon@2x.png create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/CamlistoreTests/CamlistoreTests-Info.plist create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/CamlistoreTests/CamlistoreTests.m create mode 100644 vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/CamlistoreTests/en.lproj/InfoPlist.strings create mode 100755 vendor/github.com/camlistore/camlistore/clients/python/camliclient.py create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camdeploy/camdeploy.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camdeploy/gce.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camget/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camget/camget.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camget/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camget/graph.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/cammount/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/cmd/cammount/cammount.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/cammount/cammount_other.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/cammount/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/androidx.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/attr.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/blobs.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/cache.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/camput.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/camput_test.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/delete.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/discard.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/files.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/init.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/kvcache.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/logging.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/permanode.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/rawobj.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/remove.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/share.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/stat_darwin.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/stat_linux.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camput/uploader.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/camtool.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/claims.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/dbinit.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/debug.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/describe.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/disco.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/dp_idx_rebuild.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/dumpconfig.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/env.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/exif.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/googinit.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/index.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/list.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/makestatic.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/mime.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/packblobs.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/search.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/searchdoc.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/splits.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/sqlite_cond.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/sync.go create mode 100644 vendor/github.com/camlistore/camlistore/cmd/camtool/sync_test.go create mode 100644 vendor/github.com/camlistore/camlistore/config/dev-blobserver-config.json create mode 100644 vendor/github.com/camlistore/camlistore/config/dev-client-dir-demo/client-config.json create mode 100644 vendor/github.com/camlistore/camlistore/config/dev-client-dir/client-config.json create mode 100644 vendor/github.com/camlistore/camlistore/config/dev-indexer-config.json create mode 100644 vendor/github.com/camlistore/camlistore/config/dev-server-config.json create mode 100644 vendor/github.com/camlistore/camlistore/depcheck/depcheck.go create mode 100644 vendor/github.com/camlistore/camlistore/depcheck/min_go_version.go create mode 100755 vendor/github.com/camlistore/camlistore/dev/camfix.pl create mode 100644 vendor/github.com/camlistore/camlistore/dev/config-dir-local/client-config.json create mode 100644 vendor/github.com/camlistore/camlistore/dev/demo.sh create mode 100755 vendor/github.com/camlistore/camlistore/dev/dev-db create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/appengine.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/camget.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/cammount.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/camput.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/camtool.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/devcam.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/env.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/exec.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/hook.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/review.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/server.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/devcam/test.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/envvardoc/envvardoc.go create mode 100644 vendor/github.com/camlistore/camlistore/dev/local.sh create mode 100755 vendor/github.com/camlistore/camlistore/dev/make-release create mode 100755 vendor/github.com/camlistore/camlistore/dev/push create mode 100644 vendor/github.com/camlistore/camlistore/dev/update_closure_compiler.go create mode 100644 vendor/github.com/camlistore/camlistore/doc/app-environment.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/blog-notes.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/environment-vars.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/example-blobs/README.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-648357ea2ee9dd7031d0ff786840e6deac8b7a6a.dat create mode 100644 vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-648357ea2ee9dd7031d0ff786840e6deac8b7a6a.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-c4da9d771661563a27704b91b67989e7ea1e50b8.dat create mode 100644 vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-c4da9d771661563a27704b91b67989e7ea1e50b8.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/json-signing/example/public-key.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-after.camli create mode 100644 vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-before-J.camli create mode 100644 vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-before.camli create mode 100644 vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-before.camli.detachsig create mode 100644 vendor/github.com/camlistore/camlistore/doc/json-signing/example/some-notes.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/json-signing/example/some-notes.txt.camli create mode 100644 vendor/github.com/camlistore/camlistore/doc/json-signing/example/test-keyring.gpg create mode 100644 vendor/github.com/camlistore/camlistore/doc/json-signing/example/test-secring.gpg create mode 100644 vendor/github.com/camlistore/camlistore/doc/json-signing/json-signing.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/overview.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/protocol/blob-enumerate-protocol.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/protocol/blob-get-protocol.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/protocol/blob-stat-protocol.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/protocol/blob-upload-protocol.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/protocol/blob-upload-resume.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/protocol/discovery.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/publishing/README create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/blob-magic.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/bytes.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/claims/TODO create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/claims/attributes.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/claims/delete.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/claims/share.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/files/directory.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/files/fifo.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/files/file-common.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/files/file.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/files/inode.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/files/socket.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/files/symlink.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/objects/keep.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/objects/permanode.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/schema/objects/static-set.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/search-ui.txt create mode 100644 vendor/github.com/camlistore/camlistore/doc/terminology.txt create mode 100644 vendor/github.com/camlistore/camlistore/internal/chanworker/chanworker.go create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/camli/__init__.py create mode 100755 vendor/github.com/camlistore/camlistore/lib/python/camli/op.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/camli/schema.py create mode 100755 vendor/github.com/camlistore/camlistore/lib/python/camli/schema_test.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/__init__.py create mode 100755 vendor/github.com/camlistore/camlistore/lib/python/fusepy/context.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/fuse.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/fuse24.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/fuse3.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/fusell.py create mode 100755 vendor/github.com/camlistore/camlistore/lib/python/fusepy/loopback.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/.project create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/.pydevproject create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/README.txt create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/ctypeslib.zip create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/fuse_ctypes.h create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse/__init__.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse/interface.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse/operations.py create mode 100755 vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse_example.py create mode 100755 vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/setup.py create mode 100755 vendor/github.com/camlistore/camlistore/lib/python/fusepy/memory.py create mode 100755 vendor/github.com/camlistore/camlistore/lib/python/fusepy/memory3.py create mode 100755 vendor/github.com/camlistore/camlistore/lib/python/fusepy/memoryll.py create mode 100755 vendor/github.com/camlistore/camlistore/lib/python/fusepy/sftp.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/setup.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/simplejson/__init__.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/simplejson/decoder.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/simplejson/encoder.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/simplejson/ordered_dict.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/simplejson/scanner.py create mode 100644 vendor/github.com/camlistore/camlistore/lib/python/simplejson/tool.py create mode 100644 vendor/github.com/camlistore/camlistore/make.go create mode 100644 vendor/github.com/camlistore/camlistore/misc/buildbot/README create mode 100644 vendor/github.com/camlistore/camlistore/misc/buildbot/builder/builder.go create mode 100644 vendor/github.com/camlistore/camlistore/misc/buildbot/builder/builder_test.go create mode 100644 vendor/github.com/camlistore/camlistore/misc/buildbot/master/bot_test.go create mode 100644 vendor/github.com/camlistore/camlistore/misc/buildbot/master/master.go create mode 100755 vendor/github.com/camlistore/camlistore/misc/commit-msg.githook create mode 100755 vendor/github.com/camlistore/camlistore/misc/copyrightifity create mode 100644 vendor/github.com/camlistore/camlistore/misc/devlib.pl create mode 100644 vendor/github.com/camlistore/camlistore/misc/docker/djpeg-static/Dockerfile create mode 100644 vendor/github.com/camlistore/camlistore/misc/docker/djpeg-static/Makefile create mode 100644 vendor/github.com/camlistore/camlistore/misc/docker/dock.go create mode 100644 vendor/github.com/camlistore/camlistore/misc/docker/go/Dockerfile create mode 100644 vendor/github.com/camlistore/camlistore/misc/docker/mysql/Dockerfile create mode 100644 vendor/github.com/camlistore/camlistore/misc/docker/mysql/Makefile create mode 100755 vendor/github.com/camlistore/camlistore/misc/docker/mysql/run-mysqld create mode 100644 vendor/github.com/camlistore/camlistore/misc/docker/release/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/misc/docker/release/build-binaries.go create mode 100644 vendor/github.com/camlistore/camlistore/misc/docker/server/Dockerfile create mode 100644 vendor/github.com/camlistore/camlistore/misc/docker/server/build-camlistore-server.go create mode 100755 vendor/github.com/camlistore/camlistore/misc/gitversion create mode 100644 vendor/github.com/camlistore/camlistore/misc/old-devscripts/README create mode 100755 vendor/github.com/camlistore/camlistore/misc/old-devscripts/dev-camwebdav create mode 100755 vendor/github.com/camlistore/camlistore/misc/old-devscripts/dev-indexer create mode 100755 vendor/github.com/camlistore/camlistore/misc/old-devscripts/dev-synctoindexer create mode 100644 vendor/github.com/camlistore/camlistore/misc/release-history-tags create mode 100755 vendor/github.com/camlistore/camlistore/misc/review create mode 100644 vendor/github.com/camlistore/camlistore/misc/testfile create mode 100644 vendor/github.com/camlistore/camlistore/old/README create mode 100644 vendor/github.com/camlistore/camlistore/old/camwebdav/main.go create mode 100644 vendor/github.com/camlistore/camlistore/old/camwebdav/response.go create mode 100644 vendor/github.com/camlistore/camlistore/old/camwebdav/xml.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/pkg/app/app.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/app/app_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/auth/auth.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/auth/auth_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blob/blob.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blob/chanpeek.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blob/fetcher.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blob/ref.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blob/ref_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/archiver/archiver.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/archiver/archiver_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/blobhub.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/blobhub_appengine.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/blobhub_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/blobpacked/blobpacked.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/blobpacked/blobpacked_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/blobpacked/stream.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/blobpacked/stream_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/blobpacked/subfetch.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/blobpacked/subfetch_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/blobpacked/wholefetch.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/cond/cond.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/cond/cond_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/dir/dir.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/diskpacked/dele.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/diskpacked/diskpacked.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/diskpacked/diskpacked_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/diskpacked/punch_linux.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/diskpacked/reindex.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/diskpacked/reindex_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/diskpacked/stream_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/diskpacked/testdata/pack-00000.blobs create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/diskpacked/testdata/pack-00001.blobs create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/encrypt/encrypt.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/encrypt/encrypt_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/enumerate.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/gethandler/get.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/gethandler/get_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/google/cloudstorage/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/google/cloudstorage/cloudstorage_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/google/cloudstorage/storage.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/google/drive/drive.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/google/drive/drive_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/google/drive/enumerate.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/google/drive/fetch.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/google/drive/receive.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/google/drive/remove.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/google/drive/service/service.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/google/drive/stat.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/handlers/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/handlers/enumerate.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/handlers/enumerate_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/handlers/get.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/handlers/remove.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/handlers/stat.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/handlers/upload.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/interface.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/local/generation.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/localdisk/enumerate.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/localdisk/enumerate_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/localdisk/generation.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/localdisk/localdisk.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/localdisk/localdisk_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/localdisk/path.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/localdisk/path_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/localdisk/receive.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/localdisk/receive_posix.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/localdisk/receive_windows.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/localdisk/stat.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/localdisk/upgrade32.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/memory/mem.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/memory/mem_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/mergedenum.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/mergedenum_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/mongo/enumerate.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/mongo/fetch.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/mongo/mongo.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/mongo/mongo_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/mongo/receive.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/mongo/remove.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/mongo/stat.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/multistream.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/multistream_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/namespace/ns.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/namespace/ns_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/noimpl.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/protocol/protocol.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/protocol/protocol_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/proxycache/proxycache.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/receive.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/receive_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/registry.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/remote/remote.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/replica/replica.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/replica/replica_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/s3/enumerate.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/s3/fetch.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/s3/receive.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/s3/remove.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/s3/s3.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/s3/s3_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/s3/stat.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/shard/shard.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/stats/statreceiver.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/storagetest/storagetest.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/sync.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/blobserver/sync_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/buildinfo/buildinfo.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/buildinfo/buildinfo_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/buildinfo/testinglinked.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/cacher/cacher.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/camerrors/errors.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/android/androidx.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/android/androidx_fake.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/android/androidx_real.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/client.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/config.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/config_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/enumerate.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/get.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/ignored_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/remove.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/stat_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/stats.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/transport_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/client/upload.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/cmdmain/cmdmain.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/cmdmain/cmdmain_go12.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/constants/constants.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/constants/google/google.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/context/context.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/conv/conv.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/conv/conv_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/deploy/gce/cloud-config.yaml create mode 100644 vendor/github.com/camlistore/camlistore/pkg/deploy/gce/debug/main.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/deploy/gce/deploy.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/deploy/gce/handler.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/deploy/gce/notes.txt create mode 100644 vendor/github.com/camlistore/camlistore/pkg/env/env.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fault/fault.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fileembed/fileembed.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fileembed/genfileembed/genfileembed.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/at.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/debug.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/fs.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/fs_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/mut.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/mut_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/recent.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/ro.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/root.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/roots.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/time.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/time_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/util.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/xattr.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/fs/z_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/gc/gc.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/gc/gc_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/geocode/geocode.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/geocode/geocode_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/googlestorage/README create mode 100644 vendor/github.com/camlistore/camlistore/pkg/googlestorage/googlestorage.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/googlestorage/googlestorage_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum create mode 100644 vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-1 create mode 100644 vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-2 create mode 100644 vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-3 create mode 100644 vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-4 create mode 100644 vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-get create mode 100644 vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-stat create mode 100644 vendor/github.com/camlistore/camlistore/pkg/hashutil/hashutil.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/httputil/auth.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/httputil/auth_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/httputil/certs.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/httputil/certs_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/httputil/faketransport.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/httputil/httputil.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/httputil/httputil_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/httputil/transport.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/bench_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/benchfastjpeg_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/fastjpeg/fastjpeg.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/fastjpeg/fastjpeg_test.go create mode 100755 vendor/github.com/camlistore/camlistore/pkg/images/fastjpeg/testdata/djpeg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/images.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/images_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/resize/bench_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/resize/resize.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/resize/resize_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/resize/testdata/test-resample-128x128-64x64.png create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/resize/testdata/test-resample-768x576-128x96.png create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/resize/testdata/test.png create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f1-exif.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f1-s.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f1.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f2-exif.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f2.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f3-exif.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f3.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f4-exif.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f4.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f5-exif.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f5.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f6-exif.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f6.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f7-exif.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f7.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f8-exif.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/images/testdata/f8.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/README create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/allimporters/importers.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/attrs.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/dummy/dummy.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/feed/atom/atom.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/feed/feed.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/feed/parse.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/feed/rdf/rdf.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/feed/rss/rss.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/flickr/README create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/flickr/flickr.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/flickr/flickr_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/flickr/testdata.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/README create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/api.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/foursquare.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/foursquare_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/testdata.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/testdata/users-me-res.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/html.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/importer.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/importer_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/noop.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/oauth.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/picasa/README create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/picasa/oa2_importers.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/picasa/picasa.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/picasa/picasa_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/picasa/testdata.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/picasa/testdata/users-me-res.xml create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/pinboard/pinboard.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/pinboard/pinboard_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/pinboard/testdata/batchresponse.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/twitter/README create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/twitter/testdata.go create mode 100755 vendor/github.com/camlistore/camlistore/pkg/importer/twitter/testdata/verify_credentials-res.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/twitter/twitter.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/importer/twitter/twitter_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/corpus.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/corpus_bench_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/corpus_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/enumstat.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/export_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/index.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/index_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/indextest/testdata/0s.mp3 create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/indextest/testdata/dude-exif.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/indextest/testdata/dude.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/indextest/tests.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/interface.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/keys.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/keys_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/kvfile_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/memindex.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/mongo_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/mysql_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/postgres_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/receive.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/reversetime.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/sniff.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/sqlindex/sqlindex.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/sqlite/sqlite.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/sqlite/sqlite_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/index/util.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonconfig/eval.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonconfig/jsonconfig.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonconfig/jsonconfig_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/boolenv.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/include1.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/include2.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/listexpand.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/loop1.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/loop2.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/jsonsign_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/keys.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/sign.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/sign_appengine.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/sign_normal.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/signhandler/sig.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/password-foo-keyring.gpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/password-foo-secring.gpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/test-keyring.gpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/test-keyring2.gpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/test-secring.gpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/test-secring2.gpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/jsonsign/verify.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/kvutil/kvutil.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/leak/leak.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/leak/leak_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/legal/legal.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/legal/legal_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/legal/legalprint/legalprint.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/lru/cache.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/lru/cache_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/magic.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/magic_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.tar create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.tar.gz create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.tar.xz create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.tbz2 create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.zip create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/magic.pdf create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.bmp create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.gif create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.ico create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.png create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.psd create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.tiff create mode 100644 vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.xcf create mode 100644 vendor/github.com/camlistore/camlistore/pkg/media/audio.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/media/audio_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/media/testdata/128_cbr.mp3 create mode 100644 vendor/github.com/camlistore/camlistore/pkg/media/testdata/id3v1.mp3 create mode 100644 vendor/github.com/camlistore/camlistore/pkg/media/testdata/xing_header.mp3 create mode 100644 vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/auth.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/auth_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/client.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/client_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/misc/closure/genclosuredeps/genclosuredeps.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/misc/closure/gendeps.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/misc/closure/gendeps_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/misc/closure/jstest/jstest.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/misc/gpgagent/gpgagent.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/misc/gpgagent/gpgagent_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/misc/pinentry/pinentry.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/netutil/ident.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/netutil/ident_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/netutil/netutil.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/netutil/netutil_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/oauthutil/oauth.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/cpu.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/cpu_freebsd.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/cpu_linux.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/findproc_appengine.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/findproc_normal.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/gce/gce.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/mem.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/mem_unix.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/openurl.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/osutil.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/paths.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/paths_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/restart_freebsd.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/restart_stub.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/restart_unix.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/restart_windows.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_appengine.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_posix.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_solaris.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_windows.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/pools/pools.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/publish/types.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/readerutil/countingreader.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/readerutil/opener.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/readerutil/opener_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/readerutil/readersize.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/readerutil/readersize_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/rollsum/rollsum.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/rollsum/rollsum_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/blob.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/dirreader.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/fileread_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/filereader.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/filewriter.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/filewriter_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/lookup.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/nodeattr/nodeattr.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/schema.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/schema_darwin.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/schema_linux.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/schema_posix.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/schema_public_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/schema_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/sign.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/sign_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/testdata/coffee-sf.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/schema/testdata/gocon-tokyo.jpg create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/describe.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/describe_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/export_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/expr.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/expr_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/handler.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/handler_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/lexer.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/lexer_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/match_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/predicate.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/predicate_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/query.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/query_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/search.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/search/websocket.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/app/app.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/app/app_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/cgo_probe.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/download.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/favicon.ico create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/fileembed.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/filetree.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/help.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/image.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/root.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/root_appengine.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/root_normal.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/share.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/share_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/status.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/sync.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/thumbcache.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/ui.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/uploadhelper.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/wizard-html.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/server/wizard.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/devmode.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/env.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/export_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/genconfig.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/genconfig_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/serverinit.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/serverinit_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurl-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurl.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurlbad.err create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurlbad.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_googlecloud-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_googlecloud.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_localdisk-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_localdisk.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/default-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/default.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/diskpacked-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/diskpacked.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/flickr-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/flickr.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/gen_client_config.in create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/gen_client_config.out create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk_subdir-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk_subdir.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_queues_on_db-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_queues_on_db.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account_subdir-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account_subdir.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/justblobs-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/justblobs.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/leveldb-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/leveldb.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/listenbase-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/listenbase.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mem-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mem.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memindex-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memindex.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memory_storage-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memory_storage.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mongo-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mongo.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/multipublish-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/multipublish.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/noindex.err create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/noindex.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_alt_host-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_alt_host.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_google_nolocaldisk.err create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_google_nolocaldisk.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk_mysql-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk_mysql.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/sqlite-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/sqlite.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/thumbcache_on_db-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/thumbcache_on_db.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/tls-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/tls.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_blog-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_blog.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_gallery-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_gallery.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_sourceroot-want.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_sourceroot.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/singleflight/singleflight.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/singleflight/singleflight_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/buffer/buffer.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/buffer/buffer_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/kv.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/kvfile/kvfile.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/kvfile/kvfile_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/kvtest/kvtest.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/leveldb/leveldb.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/leveldb/leveldb_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/mem.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/mem_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/mongo/mongokv.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/mongo/mongokv_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/cloudsql.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/dbschema.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/mysqlkv.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/mysqlkv_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/postgres/dbschema.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/postgres/postgreskv.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/postgres/postgreskv_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/dbschema.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/sqlite_cond.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/sqlitekv.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/sqlitekv_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/sqlkv/sqlkv.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/sorted/sqlkv/sqlkv_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/strutil/intern.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/strutil/strconv.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/strutil/strutil.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/strutil/strutil_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/syncutil/gate.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/syncutil/group.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/syncutil/lock.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/syncutil/once.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/syncutil/once_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/syncutil/sem.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/syncutil/sem_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/syncutil/syncutil_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/asserts/asserts.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/blob.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/diff.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/dockertest/docker.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/fakeindex.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/fetcher.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/fetcher_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/integration/camget_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/integration/camlistore_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/integration/camput_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/integration/diskpacked_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/integration/integration.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/integration/non-utf8_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/integration/share_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/integration/z_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/loader.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/test_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/testdata/bench-diskpacked-server-config.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/testdata/bench-localdisk-server-config.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/testdata/server-config.json create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/testdep.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/wait.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/test/world.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/throttle/throttle.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/atomics.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/camtypes/camtypes.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/camtypes/discovery.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/camtypes/errors.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/camtypes/search.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/camtypes/search_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/camtypes/sign.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/camtypes/statustype.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/clientconfig/config.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/example_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/fakeseeker.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/fakeseeker_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/serverconfig/config.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/types.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/types/types_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/handler.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/handler_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/service.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/service_test.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/testdata/small.webm create mode 100644 vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/thumbnailer.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/webserver/envpipe_unix.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/webserver/envpipe_windows.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/webserver/webserver.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/wkfs/gcs/gcs.go create mode 100644 vendor/github.com/camlistore/camlistore/pkg/wkfs/wkfs.go create mode 100644 vendor/github.com/camlistore/camlistore/server/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/server/appengine/README create mode 100644 vendor/github.com/camlistore/camlistore/server/appengine/app.yaml create mode 100644 vendor/github.com/camlistore/camlistore/server/appengine/build_test.go create mode 100644 vendor/github.com/camlistore/camlistore/server/appengine/camli/aeindex.go create mode 100644 vendor/github.com/camlistore/camlistore/server/appengine/camli/common.go create mode 100644 vendor/github.com/camlistore/camlistore/server/appengine/camli/contextpool.go create mode 100644 vendor/github.com/camlistore/camlistore/server/appengine/camli/main.go create mode 100644 vendor/github.com/camlistore/camlistore/server/appengine/camli/ownerauth.go create mode 100644 vendor/github.com/camlistore/camlistore/server/appengine/camli/storage.go create mode 100644 vendor/github.com/camlistore/camlistore/server/appengine/config.json create mode 120000 vendor/github.com/camlistore/camlistore/server/appengine/test-secring.gpg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/README create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/camlistored.go create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/run_test.go create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/setup.go create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/TODO create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/animation_loop.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_detail.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_container.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_container_react.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_container_test.html create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_demo_content.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_foursquare.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_foursquare_content.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_generic_content.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_image_content.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_progress_test.html create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_react.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_twitter.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_twitter_content.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_video.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_video_content.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_test.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blobref.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/blog.html create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/cache_buster_iframe.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/checkmark2.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/checkmark2_blue.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/circled_plus.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/clear.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/close.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/closure-toolbar-bg.png create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/closure/closure.go create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/date_utils.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/debug.html create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/debug_console.html create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/debug_console.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/detail.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/detail.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/dialog.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/dialog.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/directory_detail.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/down.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/file.png create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/fileembed.go create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/fileembed_appengine.go create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/fileembed_normal.go create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/filetree.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/filetree.html create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/filetree.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/folder.png create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/foursquare-logo.png create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/hash_worker.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/header.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/header.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/icon_16716.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/icon_27307.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/image_detail.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/index.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/index.html create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/index.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/js-notes.txt create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/magnifying_glass.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/math.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/mobile.html create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/mobile_setup.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/mobile_setup.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/navigator.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/navigator_test.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/new_permanode.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/node.png create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/object.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode.html create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_detail.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_detail.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_utils.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_utils_test.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/prefix-free.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/property_sheet.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/property_sheet.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/pyramid_throbber.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/pyramid_throbber.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/react_util.js create mode 100755 vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe-no-wheel.svg create mode 100755 vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe-wheel.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe1-16.png create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe1-32.png create mode 100755 vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe1.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/search_session.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/search_session_test.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/server_connection.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/server_type.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/sidebar.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/sidebar.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/sigdebug.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/spinner.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/spinner.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/spinner_test.html create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/sprited_animation.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/sprited_image.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/style.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/tags_control.css create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/tags_control.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/target.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/thumber.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/thumber_test.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/trash.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/twitter-logo.png create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/ui_test.go create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/up.svg create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/worker_message_router.js create mode 100644 vendor/github.com/camlistore/camlistore/server/camlistored/ui/wsdebug.html create mode 100644 vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/README create mode 100644 vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/app.yaml create mode 100644 vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/config.py create mode 100644 vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/index.yaml create mode 100644 vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/main.py create mode 100644 vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/static/style.css create mode 100644 vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/test_data.txt create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/camsigd.go create mode 100755 vendor/github.com/camlistore/camlistore/server/sigserver/client.pl create mode 100755 vendor/github.com/camlistore/camlistore/server/sigserver/run.sh create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/sign.go create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/spec.txt create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/test/00-start.t create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/test/10-sign.t create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/test/CamsigdTest.pm create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/test/doc.tmp create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/test/pubkey-blobs/sha1-82e6f3494f698aa498d5906349c0aa0a183d89a6.camli create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/test/sig.tmp create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/test/test-keyring.gpg create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/test/test-keyring2.gpg create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/test/test-secring.gpg create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/test/test-secring2.gpg create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/test/test.json create mode 100644 vendor/github.com/camlistore/camlistore/server/sigserver/verify.go create mode 100755 vendor/github.com/camlistore/camlistore/server/tester/bs-test.pl create mode 100644 vendor/github.com/camlistore/camlistore/third_party/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/.gitattributes create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/debug.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux-error-init.seq create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux-error-init.seq.png create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux.seq create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux.seq.png create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-osx-error-init.seq create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-osx-error-init.seq.png create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-osx.seq create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-osx.seq.png create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-sequence.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/writing-docs.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/error_darwin.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/error_std.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/bench/bench_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/bench/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/debug.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/mounted.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/mountinfo.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/mountinfo_darwin.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/mountinfo_linux.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/record/buffer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/record/record.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/record/wait.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/testfs.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/serve.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/serve_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/tree.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_darwin.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_linux.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_std.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuseutil/fuseutil.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/hellofs/hello.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/mount_darwin.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/mount_linux.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_darwin.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_darwin_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_linux.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/doc.go create mode 100755 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/generate create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/msync.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/msync_386.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/msync_amd64.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/syscallx.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/syscallx_std.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/xattr_darwin.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/xattr_darwin_386.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/xattr_darwin_amd64.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/unmount.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/unmount_linux.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/unmount_std.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/AUTHORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/aria.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/attributes.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/datatables.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/roles.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/array/array.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/asserts/asserts.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/async/nexttick.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/async/run.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/async/throttle.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/base.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/bootstrap/nodejs.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/bootstrap/webworkers.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/crypt/crypt.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/crypt/hash.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/crypt/sha1.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/css/common.css create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/css/toolbar.css create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/debug/debug.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/debug/entrypointregistry.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/debug/error.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/debug/errorhandler.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/debug/logbuffer.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/debug/logger.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/debug/logrecord.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/debug/tracer.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/deps.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/disposable/disposable.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/disposable/idisposable.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/dom/browserfeature.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/dom/classes.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/dom/classlist.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/dom/dom.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/dom/nodetype.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/dom/tagname.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/dom/vendor.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/browserevent.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/browserfeature.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/event.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/eventhandler.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/eventid.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/events.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/eventtarget.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/eventtype.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/filedrophandler.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/keycodes.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/keyhandler.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/listenable.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/listener.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/events/listenermap.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/format/format.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/functions/functions.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/i18n/graphemebreak.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/iter/iter.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/json/json.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/labs/promise/promise.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/labs/promise/thenable.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/log/log.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/math/box.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/math/coordinate.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/math/math.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/math/rect.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/math/size.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/net/errorcode.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/net/eventtype.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/net/httpstatus.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/net/wrapperxmlhttpfactory.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/net/xhrio.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/net/xhrlike.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/net/xmlhttp.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/net/xmlhttpfactory.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/object/object.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/reflect/reflect.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/string/string.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/structs/collection.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/structs/inversionmap.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/structs/map.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/structs/set.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/structs/simplepool.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/structs/structs.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/style/style.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/testing/watchers.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/timer/timer.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/ui/component.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/ui/control.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/ui/controlcontent.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/ui/controlrenderer.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/ui/decorate.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/ui/idgenerator.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/ui/registry.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/uri/uri.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/uri/utils.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/useragent/useragent.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/closure/updatelibrary.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/big5.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/charset.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/charset_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/codepage.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/cp932.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/example_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/file.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/iconv/iconv.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/iconv/iconv_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/iconv/list_query.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/iconv/list_static.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/local.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/utf16.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/charset/utf8.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_big5.dat.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_charsets.json.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_cp932.dat.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_ibm437.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_ibm850.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_ibm866.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_iso-8859-1.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_iso-8859-10.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_iso-8859-15.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_iso-8859-2.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_iso-8859-3.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_iso-8859-4.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_iso-8859-5.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_iso-8859-6.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_iso-8859-7.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_iso-8859-8.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_iso-8859-9.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_jisx0201kana.dat.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_koi8-r.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_windows-1250.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_windows-1251.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/data_windows-1252.cp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go-charset/data/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/AUTHORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/CONTRIBUTORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/bcrypt/base64.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/bcrypt/bcrypt.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/bcrypt/bcrypt_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/blowfish/block.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/blowfish/blowfish_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/blowfish/cipher.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/blowfish/const.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/cast5/cast5.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/cast5/cast5_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/codereview.cfg create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/md4/md4.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/md4/md4_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/md4/md4block.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ocsp/ocsp.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ocsp/ocsp_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/armor/armor.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/armor/armor_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/armor/encode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/canonical_text.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/canonical_text_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/clearsign/clearsign.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/clearsign/clearsign_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/elgamal/elgamal.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/elgamal/elgamal_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/errors/errors.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/keys.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/keys_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/compressed.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/compressed_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/config.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/encrypted_key.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/encrypted_key_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/literal.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/ocfb.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/ocfb_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/one_pass_signature.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/opaque.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/opaque_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/packet.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/packet_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/private_key.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/private_key_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/public_key.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/public_key_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/public_key_v3.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/public_key_v3_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/reader.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/signature.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/signature_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/signature_v3.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/signature_v3_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/symmetric_key_encrypted.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/symmetric_key_encrypted_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/symmetrically_encrypted.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/symmetrically_encrypted_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/userattribute.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/userattribute_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/userid.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/packet/userid_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/read.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/read_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/s2k/s2k.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/s2k/s2k_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/write.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/openpgp/write_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/pbkdf2/pbkdf2.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/pbkdf2/pbkdf2_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ripemd160/ripemd160.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ripemd160/ripemd160_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ripemd160/ripemd160block.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/scrypt/scrypt.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/scrypt/scrypt_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/channel.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/cipher.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/cipher_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/client.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/client_auth.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/client_auth_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/client_func_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/common.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/common_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/messages.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/messages_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/server.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/server_terminal.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/session.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/session_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/tcpip.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/tcpip_func_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/transport.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/ssh/transport_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/twofish/twofish.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/twofish/twofish_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/xtea/block.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/xtea/cipher.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.crypto/xtea/xtea_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/atom/atom.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/atom/atom_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/atom/gen.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/atom/table.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/atom/table_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/const.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/doctype.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/entity.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/entity_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/escape.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/escape_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/example_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/foreign.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/node.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/node_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/parse.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/parse_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/render.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/render_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/token.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/token_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/.hgtags create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/AUTHORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/CONTRIBUTORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/PATENTS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/lib/codereview/codereview.cfg create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/oauth/oauth.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/oauth/oauth_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/.hgignore create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/AUTHORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/CONTRIBUTORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/crc/crc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/comparer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/comparer_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/db.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/options.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/leveldb.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/memdb/memdb.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/memdb/memdb_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/record/record.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/record/record_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/reader.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/table.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/table_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/writer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/lib/codereview/codereview.cfg create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/000003.log create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/CURRENT create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/LOCK create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/LOG create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/MANIFEST-000002 create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/000003.log create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/CURRENT create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/LOCK create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/LOG create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/MANIFEST-000002 create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/000005.sst create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/000006.log create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/CURRENT create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/LOCK create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/LOG create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/LOG.old create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/MANIFEST-000004 create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-4/000005.sst create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-4/000006.log create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-4/CURRENT create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-4/LOCK create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-4/LOG create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-4/LOG.old create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-4/MANIFEST-000004 create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/h.no-compression.sst create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/h.sst create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/h.txt create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/hamlet-act-1.txt create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/make-db.cc create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/make-table.cc create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/gf256/blog_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/gf256/gf256.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/gf256/gf256_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/coding/gen.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/coding/qr.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/coding/qr_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/png.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/png_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/qr.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/web/pic.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/web/play.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/web/resize/resize.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/.hgignore create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/AUTHORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/CONTRIBUTORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/lib/codereview/codereview.cfg create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/decode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/encode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/snappy.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/snappy_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/xsrftoken/COPYING create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/xsrftoken/xsrf.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/xsrftoken/xsrf_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/fontawesome/LICENSE.txt create mode 100644 vendor/github.com/camlistore/camlistore/third_party/fontawesome/VERSION.txt create mode 100644 vendor/github.com/camlistore/camlistore/third_party/fontawesome/css/font-awesome.css create mode 100644 vendor/github.com/camlistore/camlistore/third_party/fontawesome/css/font-awesome.min.css create mode 100644 vendor/github.com/camlistore/camlistore/third_party/fontawesome/fileembed.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/fontawesome/fonts/FontAwesome.otf create mode 100755 vendor/github.com/camlistore/camlistore/third_party/fontawesome/fonts/fontawesome-webfont.eot create mode 100755 vendor/github.com/camlistore/camlistore/third_party/fontawesome/fonts/fontawesome-webfont.svg create mode 100755 vendor/github.com/camlistore/camlistore/third_party/fontawesome/fonts/fontawesome-webfont.ttf create mode 100755 vendor/github.com/camlistore/camlistore/third_party/fontawesome/fonts/fontawesome-webfont.woff create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/bradfitz/gomemcache/memcache/memcache.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/bradfitz/gomemcache/memcache/memcache_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/bradfitz/gomemcache/memcache/selector.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/bradfitz/latlong/latlong.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/bradfitz/latlong/latlong_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/bradfitz/latlong/z_gen_tables.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/bradfitz/runsit/listen/listen.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/camlistore/lock/COPYING create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/camlistore/lock/README.txt create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/camlistore/lock/lock.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/camlistore/lock/lock_appengine.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/camlistore/lock/lock_darwin_amd64.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/camlistore/lock/lock_freebsd.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/camlistore/lock/lock_linux_amd64.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/camlistore/lock/lock_linux_arm.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/camlistore/lock/lock_sigzero.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/camlistore/lock/lock_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/bufs/Makefile create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/bufs/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/bufs/bufs.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/bufs/bufs_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/Makefile create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/all_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/array.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/bench create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/bits.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/db_bench/Makefile create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/db_bench/main.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/db_bench/main_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/dbm.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/etc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/file.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/http.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/options.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/slice.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/dbm/v0.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/2pc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/2pc_docs.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/2pc_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/Makefile create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/all_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/btree.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/btree_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/db_bench/Makefile create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/db_bench/main.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/db_bench/main_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/errors.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/falloc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/falloc_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/filer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/filer_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/gb.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/gb_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/lldb.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/lldb_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/memfiler.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/memfiler_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/osfiler.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/simplefilefiler.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/xact.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/exp/lldb/xact_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/AUTHORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/CONTRIBUTORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/Makefile create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/all_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/falloc/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/falloc/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/falloc/all_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/falloc/docs.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/falloc/error.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/falloc/falloc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/falloc/test_deps.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/fileutil.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/fileutil_arm.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/fileutil_darwin.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/fileutil_freebsd.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/fileutil_linux.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/fileutil_openbsd.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/fileutil_plan9.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/fileutil_solaris.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/fileutil_windows.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/hdb/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/hdb/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/hdb/all_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/hdb/hdb.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/hdb/test_deps.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/punch_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/all_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/cache.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/cache_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/dev_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/file.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/mem.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/mem_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/probe.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/probe_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/storage.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/storage/test_deps.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/fileutil/test_deps.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/Makefile create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/_testdata/.2196ad2c3cbc669595720f0cfb6f0dd888bc64bc create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/_testdata/open.db create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/all_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/etc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/kv.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/lock.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/options.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/v0.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/kv/verify.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/GO-LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/all_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/bits.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/envelope.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/ff/main.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/mathutil.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/mersenne/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/mersenne/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/mersenne/all_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/mersenne/mersenne.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/nist-sts-2-1-1-report create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/permute.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/primes.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/rat.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/rnd.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/tables.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/mathutil/test_deps.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/sortutil/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/sortutil/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/sortutil/all_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/sortutil/sortutil.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/zappy/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/zappy/Makefile create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/zappy/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/zappy/SNAPPY-GO-LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/zappy/all_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/zappy/decode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/zappy/decode_cgo.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/zappy/decode_nocgo.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/zappy/encode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/zappy/encode_cgo.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/zappy/encode_nocgo.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/cznic/zappy/zappy.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/cov_report.sh create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/common.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/common_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/config.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/dump.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/dump_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/dumpcgo_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/dumpnocgo_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/example_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/format.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/format_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/internal_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/spew.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/spew_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/spew/testdata/dumpcgo.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/davecgh/go-spew/test_coverage.txt create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/garyburd/go-oauth/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/garyburd/go-oauth/README.markdown create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/garyburd/go-oauth/oauth/examples_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/garyburd/go-oauth/oauth/oauth.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/garyburd/go-oauth/oauth/oauth_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/AUTHORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/CHANGELOG.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/CONTRIBUTING.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/benchmark_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/buffer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/connection.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/const.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/driver.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/driver_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/errors.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/infile.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/packets.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/result.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/rows.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/statement.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/transaction.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/utils.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/go-sql-driver/mysql/utils_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/golang/glog/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/golang/glog/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/golang/glog/glog.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/golang/glog/glog_file.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/golang/glog/glog_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/gorilla/websocket/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/gorilla/websocket/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/gorilla/websocket/client.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/gorilla/websocket/client_server_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/gorilla/websocket/conn.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/gorilla/websocket/conn_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/gorilla/websocket/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/gorilla/websocket/json.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/gorilla/websocket/json_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/gorilla/websocket/server.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/gorilla/websocket/server_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/gorilla/websocket/util.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/hjfreyer/taglib-go/taglib/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/hjfreyer/taglib-go/taglib/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/hjfreyer/taglib-go/taglib/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/hjfreyer/taglib-go/taglib/id3/id3v23.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/hjfreyer/taglib-go/taglib/id3/id3v23frames.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/hjfreyer/taglib-go/taglib/id3/id3v24.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/hjfreyer/taglib-go/taglib/id3/id3v24_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/hjfreyer/taglib-go/taglib/id3/id3v24frames.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/hjfreyer/taglib-go/taglib/id3/util.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/hjfreyer/taglib-go/taglib/taglib.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/hjfreyer/taglib-go/taglib/taglib_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/hjfreyer/taglib-go/taglib/testdata/test24.mp3 create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/LICENSE.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/buf.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/conn.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/conn_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/encode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/encode_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/error.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/oid/types.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/url.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/url_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/user_posix.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/lib/pq/user_windows.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/mattn/go-sqlite3/README.mkd create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/mattn/go-sqlite3/sqlite3.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/mattn/go-sqlite3/sqlite3_other.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/mattn/go-sqlite3/sqlite3_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/nf/cr2/buffer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/nf/cr2/buffer_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/nf/cr2/consts.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/nf/cr2/reader.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/nf/cr2/reader_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/nf/cr2/samples_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/russross/blackfriday/LICENSE.txt create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/russross/blackfriday/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/russross/blackfriday/block.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/russross/blackfriday/block_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/russross/blackfriday/html.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/russross/blackfriday/inline.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/russross/blackfriday/inline_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/russross/blackfriday/latex.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/russross/blackfriday/markdown.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/russross/blackfriday/smartypants.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/rwcarlsen/goexif/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/rwcarlsen/goexif/README.camlistore create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/rwcarlsen/goexif/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/rwcarlsen/goexif/exif/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/rwcarlsen/goexif/exif/exif.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/rwcarlsen/goexif/exif/fields.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/rwcarlsen/goexif/exifstat/main.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/rwcarlsen/goexif/mknote/fields.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/rwcarlsen/goexif/mknote/mknote.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/rwcarlsen/goexif/tiff/tag.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/rwcarlsen/goexif/tiff/tiff.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/shurcooL/sanitized_anchor_name/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/shurcooL/sanitized_anchor_name/main.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/shurcooL/sanitized_anchor_name/main_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/batch.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/batch_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/bench2_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/bench_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/cache/bench2_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/cache/cache.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/cache/cache_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/cache/lru.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/comparer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/comparer/bytes_comparer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/comparer/comparer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/corrupt_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/db.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/db_compaction.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/db_iter.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/db_snapshot.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/db_state.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/db_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/db_util.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/db_write.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/errors.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/errors/errors.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/external_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/filter.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/filter/bloom.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/filter/bloom_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/filter/filter.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/iterator/array_iter.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/iterator/array_iter_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/iterator/indexed_iter.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/iterator/indexed_iter_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/iterator/iter.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/iterator/iter_suite_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/iterator/merged_iter.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/iterator/merged_iter_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/journal/journal.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/journal/journal_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/key.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/key_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/leveldb_suite_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/memdb/bench_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/memdb/memdb.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/memdb/memdb_suite_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/memdb/memdb_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/opt/options.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/options.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/session.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/session_record.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/session_record_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/session_util.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/storage/file_storage.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/storage/file_storage_plan9.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/storage/file_storage_solaris.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/storage/file_storage_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/storage/file_storage_unix.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/storage/file_storage_windows.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/storage/mem_storage.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/storage/mem_storage_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/storage/storage.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/storage_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/table.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/table/block_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/table/reader.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/table/table.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/table/table_suite_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/table/table_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/table/writer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/testutil/db.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/testutil/ginkgo.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/testutil/iter.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/testutil/kv.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/testutil/kvtest.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/testutil/storage.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/testutil/util.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/testutil_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/util.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/util/buffer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/util/buffer_pool.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/util/buffer_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/util/crc32.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/util/hash.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/util/pool.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/util/pool_legacy.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/util/range.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/util/util.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/goleveldb/leveldb/version.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/gosnappy/snappy/decode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/gosnappy/snappy/encode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/gosnappy/snappy/snappy.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/syndtr/gosnappy/snappy/snappy_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/tgulacsi/picago/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/tgulacsi/picago/atom.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/tgulacsi/picago/atom_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/tgulacsi/picago/auth.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/tgulacsi/picago/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/tgulacsi/picago/get.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/tgulacsi/picago/testdata/album-list.xml create mode 100644 vendor/github.com/camlistore/camlistore/third_party/github.com/tgulacsi/picago/testdata/gallery-with-a-video.xml create mode 100644 vendor/github.com/camlistore/camlistore/third_party/glitch/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/glitch/fileembed.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/glitch/npc_piggy__x1_chew_png_1354829433.png create mode 100644 vendor/github.com/camlistore/camlistore/third_party/glitch/npc_piggy__x1_look_screen_png_1354829434.png create mode 100644 vendor/github.com/camlistore/camlistore/third_party/glitch/npc_piggy__x1_rooked1_png_1354829442.png create mode 100644 vendor/github.com/camlistore/camlistore/third_party/glitch/npc_piggy__x1_too_much_nibble_png_1354829441.png create mode 100644 vendor/github.com/camlistore/camlistore/third_party/glitch/npc_piggy__x1_walk_png_1354829432.png create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/example_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/reader.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/reader_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/register.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/struct.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/crc32-not-streamed.zip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/dd.zip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/go-no-datadesc-sig.zip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/go-with-datadesc-sig.zip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/gophercolor16x16.png create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/readme.notzip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/readme.zip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/symlink.zip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/test-trailing-junk.zip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/test.zip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/unix.zip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/winxp.zip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/zip64-2.zip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/testdata/zip64.zip create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/writer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/writer_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/archive/zip/zip_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/image/jpeg/dct_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/image/jpeg/fdct.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/image/jpeg/huffman.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/image/jpeg/idct.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/image/jpeg/reader.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/image/jpeg/reader_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/image/jpeg/scan.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/image/jpeg/writer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/go/pkg/image/jpeg/writer_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/AUTHORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/CONTRIBUTING.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/CONTRIBUTORS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/PATENTS create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/README create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/bmp/reader.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/bmp/writer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/codereview.cfg create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/draw/draw.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/draw/gen.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/draw/impl.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/draw/scale.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/math/f32/f32.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/math/f64/f64.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/riff/riff.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/tiff/buffer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/tiff/compress.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/tiff/consts.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/tiff/lzw/reader.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/tiff/reader.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/tiff/writer.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/vp8/decode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/vp8/filter.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/vp8/idct.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/vp8/partition.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/vp8/pred.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/vp8/predfunc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/vp8/quant.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/vp8/reconstruct.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/vp8/token.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/vp8l/decode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/vp8l/huffman.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/vp8l/transform.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/webp/decode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/image/webp/nycbcra/nycbcra.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/file.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/file_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/if.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/if_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/litmus_test_server.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/lock.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/lock_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/prop.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/prop_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/webdav.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/webdav_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/xml.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/golang.org/x/net/webdav/xml_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/auth.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/auth_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/bson/LICENSE create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/bson/bson.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/bson/bson_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/bson/decode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/bson/encode.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/cluster.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/cluster_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/doc.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/export_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/gridfs.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/gridfs_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/log.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/queue.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/queue_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/server.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/session.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/session_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/socket.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/stats.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/labix.org/v2/mgo/suite_test.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/less/fileembed.go create mode 100644 vendor/github.com/camlistore/camlistore/third_party/less/less.js create mode 100755 vendor/github.com/camlistore/camlistore/third_party/react/JSXTransformer.js create mode 100644 vendor/github.com/camlistore/camlistore/third_party/react/LICENSE.txt create mode 100644 vendor/github.com/camlistore/camlistore/third_party/react/README.md create mode 100644 vendor/github.com/camlistore/camlistore/third_party/react/VERSION.txt create mode 100644 vendor/github.com/camlistore/camlistore/third_party/react/fileembed.go create mode 100755 vendor/github.com/camlistore/camlistore/third_party/react/react-with-addons.js create mode 100755 vendor/github.com/camlistore/camlistore/third_party/react/react-with-addons.min.js create mode 100755 vendor/github.com/camlistore/camlistore/third_party/react/react.js create mode 100755 vendor/github.com/camlistore/camlistore/third_party/react/react.min.js create mode 100755 vendor/github.com/camlistore/camlistore/third_party/update.pl create mode 100644 vendor/github.com/camlistore/camlistore/website/.gitignore create mode 100644 vendor/github.com/camlistore/camlistore/website/blobserver-example/example-blobserver-config.json create mode 100644 vendor/github.com/camlistore/camlistore/website/blobserver-example/root/GENERATION.dat create mode 100644 vendor/github.com/camlistore/camlistore/website/blobserver-example/root/sha1/0e/5e/sha1-0e5e60f367cc8156ae48198c496b2b2ebdf5313d.dat create mode 100644 vendor/github.com/camlistore/camlistore/website/blobserver-example/root/sha1/10/27/sha1-102758fb54521cb6540d256098e7c0f1625b33e3.dat create mode 100644 vendor/github.com/camlistore/camlistore/website/blobserver-example/root/sha1/3d/c1/sha1-3dc1d1cfe92fce5f09d194ba73a0b023102c9b25.dat create mode 100644 vendor/github.com/camlistore/camlistore/website/blobserver-example/root/sha1/91/c6/sha1-91c66602c5cff5bb7162de9f6cda88fcd37415ea.dat create mode 100644 vendor/github.com/camlistore/camlistore/website/camweb.go create mode 100644 vendor/github.com/camlistore/camlistore/website/camweb_test.go create mode 100644 vendor/github.com/camlistore/camlistore/website/content/code create mode 100644 vendor/github.com/camlistore/camlistore/website/content/community create mode 100644 vendor/github.com/camlistore/camlistore/website/content/contributors create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/arch create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/client-config create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/index.html create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/json-signing create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/overview create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/principles create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/prior-art create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/protocol create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/release/0.1 create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/release/0.2 create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/release/0.3 create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/release/0.4 create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/release/0.7 create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/release/0.9 create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/schema/index.html create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/schema/permanode create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/server-config create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/sharing create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/status create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/terms create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/todo create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/uses create mode 100644 vendor/github.com/camlistore/camlistore/website/content/docs/web-ui-styleguide create mode 100644 vendor/github.com/camlistore/camlistore/website/content/download create mode 100644 vendor/github.com/camlistore/camlistore/website/content/index.html create mode 100644 vendor/github.com/camlistore/camlistore/website/contributors.go create mode 100644 vendor/github.com/camlistore/camlistore/website/dirtrees.go create mode 100644 vendor/github.com/camlistore/camlistore/website/email.go create mode 100644 vendor/github.com/camlistore/camlistore/website/format.go create mode 100644 vendor/github.com/camlistore/camlistore/website/gitweb-camli.conf create mode 100644 vendor/github.com/camlistore/camlistore/website/godoc.go create mode 100644 vendor/github.com/camlistore/camlistore/website/logging.go create mode 100755 vendor/github.com/camlistore/camlistore/website/run.pl create mode 100755 vendor/github.com/camlistore/camlistore/website/scripts/run-blobserver create mode 100644 vendor/github.com/camlistore/camlistore/website/static/all-async.js create mode 100644 vendor/github.com/camlistore/camlistore/website/static/all.css create mode 100644 vendor/github.com/camlistore/camlistore/website/static/camli-bar-background.png create mode 100644 vendor/github.com/camlistore/camlistore/website/static/camli-header.jpg create mode 100644 vendor/github.com/camlistore/camlistore/website/static/camli-header.png create mode 100644 vendor/github.com/camlistore/camlistore/website/static/favicon.ico create mode 100644 vendor/github.com/camlistore/camlistore/website/static/godocs.js create mode 100644 vendor/github.com/camlistore/camlistore/website/static/index.html create mode 100644 vendor/github.com/camlistore/camlistore/website/static/mock.html create mode 100644 vendor/github.com/camlistore/camlistore/website/static/piggy.gif create mode 100644 vendor/github.com/camlistore/camlistore/website/static/robots.txt create mode 100644 vendor/github.com/camlistore/camlistore/website/static/ss/8RmuLuw.jpg create mode 100644 vendor/github.com/camlistore/camlistore/website/talks/2011-05-07-Camlistore-Sao-Paolo/README.slides create mode 100644 vendor/github.com/camlistore/camlistore/website/talks/2011-05-07-Camlistore-Sao-Paolo/arch.png create mode 100644 vendor/github.com/camlistore/camlistore/website/talks/2011-05-07-Camlistore-Sao-Paolo/blobjects.png create mode 100644 vendor/github.com/camlistore/camlistore/website/talks/2011-05-07-Camlistore-Sao-Paolo/diagrams.odp create mode 100644 vendor/github.com/camlistore/camlistore/website/talks/2011-05-07-Camlistore-Sao-Paolo/fsbackup.png create mode 100755 vendor/github.com/camlistore/camlistore/website/talks/2011-05-07-Camlistore-Sao-Paolo/images/colorbar.png create mode 100755 vendor/github.com/camlistore/camlistore/website/talks/2011-05-07-Camlistore-Sao-Paolo/index.html create mode 100644 vendor/github.com/camlistore/camlistore/website/talks/2011-05-07-Camlistore-Sao-Paolo/prettify.js create mode 100644 vendor/github.com/camlistore/camlistore/website/talks/2011-05-07-Camlistore-Sao-Paolo/repl.png create mode 100644 vendor/github.com/camlistore/camlistore/website/talks/2011-05-07-Camlistore-Sao-Paolo/slides.js create mode 100755 vendor/github.com/camlistore/camlistore/website/talks/2011-05-07-Camlistore-Sao-Paolo/styles.css create mode 100755 vendor/github.com/camlistore/camlistore/website/test.cgi create mode 100644 vendor/github.com/camlistore/camlistore/website/tmpl/camlierror.html create mode 100644 vendor/github.com/camlistore/camlistore/website/tmpl/contrib.html create mode 100644 vendor/github.com/camlistore/camlistore/website/tmpl/error.html create mode 100644 vendor/github.com/camlistore/camlistore/website/tmpl/githeader.html create mode 100644 vendor/github.com/camlistore/camlistore/website/tmpl/package.html create mode 100644 vendor/github.com/camlistore/camlistore/website/tmpl/page.html create mode 100644 vendor/github.com/coreos/coreos-cloudinit/.gitignore create mode 100644 vendor/github.com/coreos/coreos-cloudinit/.travis.yml create mode 100644 vendor/github.com/coreos/coreos-cloudinit/CONTRIBUTING.md create mode 100644 vendor/github.com/coreos/coreos-cloudinit/DCO create mode 100644 vendor/github.com/coreos/coreos-cloudinit/Documentation/cloud-config-deprecated.md create mode 100644 vendor/github.com/coreos/coreos-cloudinit/Documentation/cloud-config-locations.md create mode 100644 vendor/github.com/coreos/coreos-cloudinit/Documentation/cloud-config-oem.md create mode 100644 vendor/github.com/coreos/coreos-cloudinit/Documentation/cloud-config.md create mode 100644 vendor/github.com/coreos/coreos-cloudinit/Documentation/config-drive.md create mode 100644 vendor/github.com/coreos/coreos-cloudinit/Documentation/debian-interfaces.md create mode 100644 vendor/github.com/coreos/coreos-cloudinit/Documentation/vmware-guestinfo.md create mode 100644 vendor/github.com/coreos/coreos-cloudinit/Godeps/Godeps.json create mode 100644 vendor/github.com/coreos/coreos-cloudinit/Godeps/Readme create mode 100644 vendor/github.com/coreos/coreos-cloudinit/LICENSE create mode 100644 vendor/github.com/coreos/coreos-cloudinit/MAINTAINERS create mode 100644 vendor/github.com/coreos/coreos-cloudinit/NOTICE create mode 100644 vendor/github.com/coreos/coreos-cloudinit/README.md create mode 100755 vendor/github.com/coreos/coreos-cloudinit/build create mode 100644 vendor/github.com/coreos/coreos-cloudinit/coreos-cloudinit.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/coreos-cloudinit_test.go create mode 100755 vendor/github.com/coreos/coreos-cloudinit/cover create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/configdrive/configdrive.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/configdrive/configdrive_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/datasource.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/file/file.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/metadata/cloudsigma/server_context.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/metadata/cloudsigma/server_context_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/metadata/digitalocean/metadata.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/metadata/digitalocean/metadata_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/metadata/ec2/metadata.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/metadata/ec2/metadata_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/metadata/metadata.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/metadata/metadata_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/metadata/packet/metadata.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/metadata/test/test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/proc_cmdline/proc_cmdline.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/proc_cmdline/proc_cmdline_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/test/filesystem.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/test/filesystem_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/url/url.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/vmware/vmware.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/vmware/vmware_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/waagent/waagent.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/datasource/waagent/waagent_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/initialize/config.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/initialize/config_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/initialize/env.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/initialize/env_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/initialize/github.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/initialize/ssh_keys.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/initialize/ssh_keys_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/initialize/user_data.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/initialize/user_data_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/initialize/workspace.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/debian.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/debian_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/digitalocean.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/digitalocean_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/interface.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/interface_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/is_go15_false_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/is_go15_true_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/packet.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/stanza.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/stanza_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/vmware.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/network/vmware_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/pkg/http_client.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/pkg/http_client_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/env.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/env_file.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/env_file_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/env_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/etc_hosts.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/etc_hosts_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/etcd.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/etcd2.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/etcd_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/file.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/file_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/flannel.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/flannel_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/fleet.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/fleet_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/locksmith.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/locksmith_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/networkd.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/oem.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/oem_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/ssh_key.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/systemd.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/systemd_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/unit.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/unit_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/update.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/update_test.go create mode 100644 vendor/github.com/coreos/coreos-cloudinit/system/user.go create mode 100755 vendor/github.com/coreos/coreos-cloudinit/test create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/90-configdrive.rules create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/90-ovfenv.rules create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/media-configdrive.mount create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/media-configvirtfs.mount create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/media-ovfenv.mount create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/system-cloudinit@.service create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/system-config.target create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/user-cloudinit-proc-cmdline.service create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/user-cloudinit@.path create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/user-cloudinit@.service create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/user-config-ovfenv.service create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/user-config.target create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/user-configdrive.path create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/user-configdrive.service create mode 100644 vendor/github.com/coreos/coreos-cloudinit/units/user-configvirtfs.service rename vendor/github.com/coreos/{ignition/config/vendor/github.com/coreos => }/go-semver/.travis.yml (100%) create mode 100644 vendor/github.com/coreos/go-semver/LICENSE rename vendor/github.com/coreos/{ignition/config/vendor/github.com/coreos => }/go-semver/README.md (100%) rename vendor/github.com/coreos/{ignition/config/vendor/github.com/coreos => }/go-semver/example.go (100%) create mode 100644 vendor/github.com/coreos/go-systemd/.travis.yml create mode 100644 vendor/github.com/coreos/go-systemd/CONTRIBUTING.md create mode 100644 vendor/github.com/coreos/go-systemd/DCO create mode 100644 vendor/github.com/coreos/go-systemd/LICENSE create mode 100644 vendor/github.com/coreos/go-systemd/README.md create mode 100644 vendor/github.com/coreos/go-systemd/activation/files.go create mode 100644 vendor/github.com/coreos/go-systemd/activation/files_test.go create mode 100644 vendor/github.com/coreos/go-systemd/activation/listeners.go create mode 100644 vendor/github.com/coreos/go-systemd/activation/listeners_test.go create mode 100644 vendor/github.com/coreos/go-systemd/activation/packetconns.go create mode 100644 vendor/github.com/coreos/go-systemd/activation/packetconns_test.go create mode 100644 vendor/github.com/coreos/go-systemd/daemon/sdnotify.go create mode 100644 vendor/github.com/coreos/go-systemd/dbus/dbus.go create mode 100644 vendor/github.com/coreos/go-systemd/dbus/dbus_test.go create mode 100644 vendor/github.com/coreos/go-systemd/dbus/methods.go create mode 100644 vendor/github.com/coreos/go-systemd/dbus/methods_test.go create mode 100644 vendor/github.com/coreos/go-systemd/dbus/properties.go create mode 100644 vendor/github.com/coreos/go-systemd/dbus/set.go create mode 100644 vendor/github.com/coreos/go-systemd/dbus/set_test.go create mode 100644 vendor/github.com/coreos/go-systemd/dbus/subscription.go create mode 100644 vendor/github.com/coreos/go-systemd/dbus/subscription_set.go create mode 100644 vendor/github.com/coreos/go-systemd/dbus/subscription_set_test.go create mode 100644 vendor/github.com/coreos/go-systemd/dbus/subscription_test.go create mode 100644 vendor/github.com/coreos/go-systemd/examples/activation/activation.go create mode 100644 vendor/github.com/coreos/go-systemd/examples/activation/httpserver/README.md create mode 100644 vendor/github.com/coreos/go-systemd/examples/activation/httpserver/hello.service create mode 100644 vendor/github.com/coreos/go-systemd/examples/activation/httpserver/hello.socket create mode 100644 vendor/github.com/coreos/go-systemd/examples/activation/httpserver/httpserver.go create mode 100644 vendor/github.com/coreos/go-systemd/examples/activation/listen.go create mode 100644 vendor/github.com/coreos/go-systemd/examples/activation/udpconn.go create mode 100644 vendor/github.com/coreos/go-systemd/fixtures/enable-disable.service create mode 100644 vendor/github.com/coreos/go-systemd/fixtures/start-stop.service create mode 100644 vendor/github.com/coreos/go-systemd/fixtures/subscribe-events-set.service create mode 100644 vendor/github.com/coreos/go-systemd/fixtures/subscribe-events.service create mode 100644 vendor/github.com/coreos/go-systemd/login1/dbus.go create mode 100644 vendor/github.com/coreos/go-systemd/login1/dbus_test.go create mode 100644 vendor/github.com/coreos/go-systemd/machine1/dbus.go create mode 100644 vendor/github.com/coreos/go-systemd/machine1/dbus_test.go create mode 100644 vendor/github.com/coreos/go-systemd/sdjournal/journal.go create mode 100644 vendor/github.com/coreos/go-systemd/sdjournal/journal_test.go create mode 100644 vendor/github.com/coreos/go-systemd/sdjournal/read.go create mode 100755 vendor/github.com/coreos/go-systemd/test create mode 100644 vendor/github.com/coreos/go-systemd/unit/deserialize.go create mode 100644 vendor/github.com/coreos/go-systemd/unit/deserialize_test.go create mode 100644 vendor/github.com/coreos/go-systemd/unit/end_to_end_test.go create mode 100644 vendor/github.com/coreos/go-systemd/unit/escape.go create mode 100644 vendor/github.com/coreos/go-systemd/unit/escape_test.go create mode 100644 vendor/github.com/coreos/go-systemd/unit/option.go create mode 100644 vendor/github.com/coreos/go-systemd/unit/option_test.go create mode 100644 vendor/github.com/coreos/go-systemd/unit/serialize.go create mode 100644 vendor/github.com/coreos/go-systemd/unit/serialize_test.go create mode 100644 vendor/github.com/coreos/go-systemd/util/util.go create mode 100644 vendor/github.com/coreos/ignition/.gitignore create mode 100644 vendor/github.com/coreos/ignition/.travis.yml create mode 100644 vendor/github.com/coreos/ignition/CONTRIBUTING.md create mode 100644 vendor/github.com/coreos/ignition/DCO create mode 100644 vendor/github.com/coreos/ignition/LICENSE create mode 100644 vendor/github.com/coreos/ignition/MAINTAINERS create mode 100644 vendor/github.com/coreos/ignition/NEWS create mode 100644 vendor/github.com/coreos/ignition/NOTICE create mode 100644 vendor/github.com/coreos/ignition/README.md create mode 100644 vendor/github.com/coreos/ignition/ROADMAP.md create mode 100755 vendor/github.com/coreos/ignition/build delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/alecthomas/units/COPYING delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/alecthomas/units/README.md delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/alecthomas/units/bytes.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/alecthomas/units/bytes_test.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/alecthomas/units/doc.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/alecthomas/units/si.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/alecthomas/units/util.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/camlistore/camlistore/pkg/errorutil/highlight.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/LICENSE delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/LICENSE.libyaml delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/README.md delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/apic.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/decode.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/decode_test.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/emitterc.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/encode.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/encode_test.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/parserc.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/readerc.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/resolve.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/scannerc.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/sorter.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/suite_test.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/writerc.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/yaml.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/yamlh.go delete mode 100644 vendor/github.com/coreos/ignition/config/v1/vendor/github.com/go-yaml/yaml/yamlprivateh.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/alecthomas/units/COPYING delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/alecthomas/units/README.md delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/alecthomas/units/bytes.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/alecthomas/units/bytes_test.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/alecthomas/units/doc.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/alecthomas/units/si.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/alecthomas/units/util.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/coreos/go-semver/semver/semver.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/coreos/go-semver/semver/semver_test.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/LICENSE delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/LICENSE.libyaml delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/README.md delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/apic.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/decode.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/decode_test.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/emitterc.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/encode.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/encode_test.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/parserc.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/readerc.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/resolve.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/scannerc.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/sorter.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/suite_test.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/writerc.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/yaml.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/yamlh.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/go-yaml/yaml/yamlprivateh.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/vincent-petithory/dataurl/LICENSE delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/vincent-petithory/dataurl/README.md delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/vincent-petithory/dataurl/cmd/dataurl/main.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/vincent-petithory/dataurl/dataurl.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/vincent-petithory/dataurl/dataurl_test.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/vincent-petithory/dataurl/doc.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/vincent-petithory/dataurl/lex.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/vincent-petithory/dataurl/rfc2396.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/vincent-petithory/dataurl/rfc2396_test.go delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/github.com/vincent-petithory/dataurl/wercker.yml delete mode 100644 vendor/github.com/coreos/ignition/config/vendor/go4.org/errorutil/highlight.go create mode 100644 vendor/github.com/coreos/ignition/doc/configuration.md create mode 100644 vendor/github.com/coreos/ignition/doc/examples.md create mode 100644 vendor/github.com/coreos/ignition/doc/getting-started.md create mode 100644 vendor/github.com/coreos/ignition/doc/migrating-configs.md create mode 100644 vendor/github.com/coreos/ignition/doc/supported-platforms.md create mode 100644 vendor/github.com/coreos/ignition/internal/exec/engine.go create mode 100644 vendor/github.com/coreos/ignition/internal/exec/engine_test.go create mode 100644 vendor/github.com/coreos/ignition/internal/exec/stages/disks/disks.go create mode 100644 vendor/github.com/coreos/ignition/internal/exec/stages/files/files.go create mode 100644 vendor/github.com/coreos/ignition/internal/exec/stages/files/files_test.go create mode 100644 vendor/github.com/coreos/ignition/internal/exec/stages/name.go create mode 100644 vendor/github.com/coreos/ignition/internal/exec/stages/stages.go create mode 100644 vendor/github.com/coreos/ignition/internal/exec/util/file.go create mode 100644 vendor/github.com/coreos/ignition/internal/exec/util/passwd.go rename vendor/github.com/coreos/ignition/{config/vendor/github.com/coreos/go-semver/semver/sort.go => internal/exec/util/path.go} (61%) create mode 100644 vendor/github.com/coreos/ignition/internal/exec/util/unit.go create mode 100644 vendor/github.com/coreos/ignition/internal/exec/util/user_lookup.c create mode 100644 vendor/github.com/coreos/ignition/internal/exec/util/user_lookup.go create mode 100644 vendor/github.com/coreos/ignition/internal/exec/util/user_lookup.h create mode 100644 vendor/github.com/coreos/ignition/internal/exec/util/user_lookup_test.go create mode 100644 vendor/github.com/coreos/ignition/internal/exec/util/util.go create mode 100644 vendor/github.com/coreos/ignition/internal/log/log.go create mode 100644 vendor/github.com/coreos/ignition/internal/log/stdout.go create mode 100644 vendor/github.com/coreos/ignition/internal/main.go create mode 100644 vendor/github.com/coreos/ignition/internal/oem/name.go create mode 100644 vendor/github.com/coreos/ignition/internal/oem/oem.go create mode 100644 vendor/github.com/coreos/ignition/internal/providers/azure/azure.go create mode 100644 vendor/github.com/coreos/ignition/internal/providers/cmdline/cmdline.go create mode 100644 vendor/github.com/coreos/ignition/internal/providers/ec2/ec2.go create mode 100644 vendor/github.com/coreos/ignition/internal/providers/gce/gce.go create mode 100644 vendor/github.com/coreos/ignition/internal/providers/noop/noop.go create mode 100644 vendor/github.com/coreos/ignition/internal/providers/providers.go create mode 100644 vendor/github.com/coreos/ignition/internal/providers/util/backoff.go create mode 100644 vendor/github.com/coreos/ignition/internal/providers/util/wait.go create mode 100644 vendor/github.com/coreos/ignition/internal/providers/util/wait_test.go create mode 100644 vendor/github.com/coreos/ignition/internal/providers/vmware/vmware.go create mode 100644 vendor/github.com/coreos/ignition/internal/providers/vmware/vmware_amd64.go create mode 100644 vendor/github.com/coreos/ignition/internal/providers/vmware/vmware_unsupported.go create mode 100644 vendor/github.com/coreos/ignition/internal/registry/registry.go create mode 100644 vendor/github.com/coreos/ignition/internal/registry/registry_test.go create mode 100644 vendor/github.com/coreos/ignition/internal/sgdisk/sgdisk.go create mode 100644 vendor/github.com/coreos/ignition/internal/systemd/systemd.go create mode 100644 vendor/github.com/coreos/ignition/internal/util/http.go create mode 100644 vendor/github.com/coreos/ignition/internal/util/tools/prerelease_check.go create mode 100644 vendor/github.com/coreos/ignition/internal/util/verification.go create mode 100644 vendor/github.com/coreos/ignition/internal/util/verification_test.go create mode 100644 vendor/github.com/coreos/ignition/internal/vendor.manifest create mode 100644 vendor/github.com/coreos/ignition/internal/version/version.go create mode 100755 vendor/github.com/coreos/ignition/tag_release.sh create mode 100755 vendor/github.com/coreos/ignition/test create mode 100644 vendor/github.com/coreos/pkg/.gitignore create mode 100644 vendor/github.com/coreos/pkg/CONTRIBUTING.md create mode 100644 vendor/github.com/coreos/pkg/DCO create mode 100644 vendor/github.com/coreos/pkg/LICENSE create mode 100644 vendor/github.com/coreos/pkg/MAINTAINERS create mode 100644 vendor/github.com/coreos/pkg/NOTICE create mode 100644 vendor/github.com/coreos/pkg/README.md create mode 100755 vendor/github.com/coreos/pkg/build create mode 100644 vendor/github.com/coreos/pkg/cryptoutil/aes.go create mode 100644 vendor/github.com/coreos/pkg/cryptoutil/aes_test.go create mode 100644 vendor/github.com/coreos/pkg/health/README.md create mode 100644 vendor/github.com/coreos/pkg/health/health.go create mode 100644 vendor/github.com/coreos/pkg/health/health_test.go create mode 100644 vendor/github.com/coreos/pkg/httputil/README.md create mode 100644 vendor/github.com/coreos/pkg/httputil/cookie.go create mode 100644 vendor/github.com/coreos/pkg/httputil/cookie_test.go create mode 100644 vendor/github.com/coreos/pkg/httputil/json.go create mode 100644 vendor/github.com/coreos/pkg/httputil/json_test.go create mode 100644 vendor/github.com/coreos/pkg/multierror/multierror.go create mode 100644 vendor/github.com/coreos/pkg/multierror/multierror_test.go create mode 100644 vendor/github.com/coreos/pkg/netutil/proxy.go create mode 100644 vendor/github.com/coreos/pkg/netutil/url.go create mode 100644 vendor/github.com/coreos/pkg/netutil/url_test.go create mode 100755 vendor/github.com/coreos/pkg/test create mode 100644 vendor/github.com/coreos/pkg/timeutil/backoff.go create mode 100644 vendor/github.com/coreos/pkg/timeutil/backoff_test.go create mode 100644 vendor/github.com/coreos/pkg/yamlutil/yaml.go create mode 100644 vendor/github.com/coreos/pkg/yamlutil/yaml_test.go rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/LICENSE (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/LICENSE.libyaml (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/README.md (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/apic.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/decode.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/decode_test.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/emitterc.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/encode.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/encode_test.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/parserc.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/readerc.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/resolve.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/scannerc.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/sorter.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/suite_test.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/writerc.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/yaml.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/yamlh.go (100%) rename vendor/github.com/coreos/{coreos-cloudinit/Godeps/_workspace/src/github.com/coreos => }/yaml/yamlprivateh.go (100%) create mode 100644 vendor/github.com/davecgh/go-spew/.gitignore create mode 100644 vendor/github.com/davecgh/go-spew/.travis.yml create mode 100644 vendor/github.com/davecgh/go-spew/LICENSE create mode 100644 vendor/github.com/davecgh/go-spew/README.md create mode 100644 vendor/github.com/davecgh/go-spew/cov_report.sh create mode 100644 vendor/github.com/davecgh/go-spew/test_coverage.txt create mode 100644 vendor/github.com/golang/protobuf/.gitignore create mode 100644 vendor/github.com/golang/protobuf/AUTHORS create mode 100644 vendor/github.com/golang/protobuf/CONTRIBUTORS create mode 100644 vendor/github.com/golang/protobuf/LICENSE create mode 100644 vendor/github.com/golang/protobuf/Make.protobuf create mode 100644 vendor/github.com/golang/protobuf/Makefile create mode 100644 vendor/github.com/golang/protobuf/README.md create mode 100644 vendor/github.com/golang/protobuf/jsonpb/jsonpb.go create mode 100644 vendor/github.com/golang/protobuf/jsonpb/jsonpb_test.go create mode 100644 vendor/github.com/golang/protobuf/jsonpb/jsonpb_test_proto/Makefile create mode 100644 vendor/github.com/golang/protobuf/jsonpb/jsonpb_test_proto/more_test_objects.pb.go create mode 100644 vendor/github.com/golang/protobuf/jsonpb/jsonpb_test_proto/more_test_objects.proto create mode 100644 vendor/github.com/golang/protobuf/jsonpb/jsonpb_test_proto/test_objects.pb.go create mode 100644 vendor/github.com/golang/protobuf/jsonpb/jsonpb_test_proto/test_objects.proto create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/Makefile create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/descriptor/Makefile create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/descriptor/descriptor.pb.go create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/doc.go create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/generator/Makefile create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/generator/generator.go create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/generator/name_test.go create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/grpc/grpc.go create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/link_grpc.go create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/main.go create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/plugin/Makefile create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/plugin/plugin.pb.go create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/plugin/plugin.pb.golden create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/Makefile create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/extension_base.proto create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/extension_extra.proto create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/extension_test.go create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/extension_user.proto create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/grpc.proto create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/imp.pb.go.golden create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/imp.proto create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/imp2.proto create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/imp3.proto create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/main_test.go create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/multi/multi1.proto create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/multi/multi2.proto create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/multi/multi3.proto create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/my_test/test.pb.go create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/my_test/test.pb.go.golden create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/my_test/test.proto create mode 100644 vendor/github.com/golang/protobuf/protoc-gen-go/testdata/proto3.proto create mode 100644 vendor/github.com/golang/protobuf/ptypes/any.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/any/any.pb.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/any/any.proto create mode 100644 vendor/github.com/golang/protobuf/ptypes/any_test.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/doc.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/duration.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/duration/duration.pb.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/duration/duration.proto create mode 100644 vendor/github.com/golang/protobuf/ptypes/duration_test.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/empty/empty.pb.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/empty/empty.proto create mode 100755 vendor/github.com/golang/protobuf/ptypes/regen.sh create mode 100644 vendor/github.com/golang/protobuf/ptypes/struct/struct.pb.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/struct/struct.proto create mode 100644 vendor/github.com/golang/protobuf/ptypes/timestamp.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/timestamp/timestamp.pb.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/timestamp/timestamp.proto create mode 100644 vendor/github.com/golang/protobuf/ptypes/timestamp_test.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/wrappers/wrappers.pb.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/wrappers/wrappers.proto create mode 100644 vendor/github.com/inconshreveable/mousetrap/LICENSE create mode 100644 vendor/github.com/inconshreveable/mousetrap/README.md create mode 100644 vendor/github.com/inconshreveable/mousetrap/trap_others.go create mode 100644 vendor/github.com/inconshreveable/mousetrap/trap_windows.go create mode 100644 vendor/github.com/inconshreveable/mousetrap/trap_windows_1.4.go create mode 100644 vendor/github.com/pmezard/go-difflib/.travis.yml create mode 100644 vendor/github.com/pmezard/go-difflib/LICENSE create mode 100644 vendor/github.com/pmezard/go-difflib/README.md mode change 100644 => 100755 vendor/github.com/spf13/pflag/verify/all.sh mode change 100644 => 100755 vendor/github.com/spf13/pflag/verify/gofmt.sh mode change 100644 => 100755 vendor/github.com/spf13/pflag/verify/golint.sh create mode 100644 vendor/github.com/stretchr/testify/.gitignore create mode 100644 vendor/github.com/stretchr/testify/.travis.yml create mode 100644 vendor/github.com/stretchr/testify/Godeps/Godeps.json create mode 100644 vendor/github.com/stretchr/testify/Godeps/Readme create mode 100644 vendor/github.com/stretchr/testify/LICENCE.txt create mode 100644 vendor/github.com/stretchr/testify/LICENSE create mode 100644 vendor/github.com/stretchr/testify/README.md create mode 100644 vendor/github.com/stretchr/testify/_codegen/main.go create mode 100644 vendor/github.com/stretchr/testify/doc.go create mode 100644 vendor/github.com/stretchr/testify/http/doc.go create mode 100644 vendor/github.com/stretchr/testify/http/test_response_writer.go create mode 100644 vendor/github.com/stretchr/testify/http/test_round_tripper.go create mode 100644 vendor/github.com/stretchr/testify/mock/doc.go create mode 100644 vendor/github.com/stretchr/testify/mock/mock.go create mode 100644 vendor/github.com/stretchr/testify/mock/mock_test.go create mode 100644 vendor/github.com/stretchr/testify/package_test.go create mode 100644 vendor/github.com/stretchr/testify/require/doc.go create mode 100644 vendor/github.com/stretchr/testify/require/forward_requirements.go create mode 100644 vendor/github.com/stretchr/testify/require/forward_requirements_test.go create mode 100644 vendor/github.com/stretchr/testify/require/require.go create mode 100644 vendor/github.com/stretchr/testify/require/require.go.tmpl create mode 100644 vendor/github.com/stretchr/testify/require/require_forward.go create mode 100644 vendor/github.com/stretchr/testify/require/require_forward.go.tmpl create mode 100644 vendor/github.com/stretchr/testify/require/requirements.go create mode 100644 vendor/github.com/stretchr/testify/require/requirements_test.go create mode 100644 vendor/github.com/stretchr/testify/suite/doc.go create mode 100644 vendor/github.com/stretchr/testify/suite/interfaces.go create mode 100644 vendor/github.com/stretchr/testify/suite/suite.go create mode 100644 vendor/github.com/stretchr/testify/suite/suite_test.go create mode 100644 vendor/go4.org/.gitignore create mode 100644 vendor/go4.org/.travis.yml create mode 100644 vendor/go4.org/AUTHORS create mode 100644 vendor/go4.org/LICENSE create mode 100644 vendor/go4.org/README.md create mode 100644 vendor/go4.org/bytereplacer/bytereplacer.go create mode 100644 vendor/go4.org/bytereplacer/bytereplacer_test.go create mode 100644 vendor/go4.org/cloud/cloudlaunch/cloudlaunch.go create mode 100644 vendor/go4.org/cloud/google/gceutil/gceutil.go create mode 100644 vendor/go4.org/cloud/google/gcsutil/storage.go create mode 100644 vendor/go4.org/ctxutil/ctxutil.go create mode 100644 vendor/go4.org/fault/fault.go create mode 100644 vendor/go4.org/jsonconfig/eval.go create mode 100644 vendor/go4.org/jsonconfig/jsonconfig.go create mode 100644 vendor/go4.org/jsonconfig/jsonconfig_test.go create mode 100644 vendor/go4.org/jsonconfig/testdata/boolenv.json create mode 100644 vendor/go4.org/jsonconfig/testdata/include1.json create mode 100644 vendor/go4.org/jsonconfig/testdata/include1bis.json create mode 100644 vendor/go4.org/jsonconfig/testdata/include2.json create mode 100644 vendor/go4.org/jsonconfig/testdata/listexpand.json create mode 100644 vendor/go4.org/jsonconfig/testdata/loop1.json create mode 100644 vendor/go4.org/jsonconfig/testdata/loop2.json create mode 100644 vendor/go4.org/legal/legal.go create mode 100644 vendor/go4.org/legal/legal_test.go create mode 100644 vendor/go4.org/lock/.gitignore create mode 100644 vendor/go4.org/lock/lock.go create mode 100644 vendor/go4.org/lock/lock_appengine.go create mode 100644 vendor/go4.org/lock/lock_darwin_amd64.go create mode 100644 vendor/go4.org/lock/lock_freebsd.go create mode 100644 vendor/go4.org/lock/lock_linux_amd64.go create mode 100644 vendor/go4.org/lock/lock_linux_arm.go create mode 100644 vendor/go4.org/lock/lock_plan9.go create mode 100644 vendor/go4.org/lock/lock_sigzero.go create mode 100644 vendor/go4.org/lock/lock_test.go create mode 100644 vendor/go4.org/net/throttle/throttle.go create mode 100644 vendor/go4.org/oauthutil/oauth.go create mode 100644 vendor/go4.org/osutil/exec_plan9.go create mode 100644 vendor/go4.org/osutil/exec_procfs.go create mode 100644 vendor/go4.org/osutil/exec_solaris_amd64.go create mode 100644 vendor/go4.org/osutil/exec_sysctl.go create mode 100644 vendor/go4.org/osutil/exec_test.go create mode 100644 vendor/go4.org/osutil/exec_windows.go create mode 100644 vendor/go4.org/osutil/osutil.go create mode 100644 vendor/go4.org/readerutil/fakeseeker.go create mode 100644 vendor/go4.org/readerutil/fakeseeker_test.go create mode 100644 vendor/go4.org/readerutil/multireaderat.go create mode 100644 vendor/go4.org/readerutil/multireaderat_test.go create mode 100644 vendor/go4.org/readerutil/readerutil.go create mode 100644 vendor/go4.org/readerutil/readerutil_test.go create mode 100644 vendor/go4.org/strutil/intern.go create mode 100644 vendor/go4.org/strutil/strconv.go create mode 100644 vendor/go4.org/strutil/strutil.go create mode 100644 vendor/go4.org/strutil/strutil_test.go create mode 100644 vendor/go4.org/syncutil/gate.go create mode 100644 vendor/go4.org/syncutil/group.go create mode 100644 vendor/go4.org/syncutil/once.go create mode 100644 vendor/go4.org/syncutil/once_test.go create mode 100644 vendor/go4.org/syncutil/sem.go create mode 100644 vendor/go4.org/syncutil/sem_test.go create mode 100644 vendor/go4.org/syncutil/singleflight/singleflight.go create mode 100644 vendor/go4.org/syncutil/singleflight/singleflight_test.go create mode 100644 vendor/go4.org/syncutil/syncdebug/syncdebug.go create mode 100644 vendor/go4.org/syncutil/syncdebug/syncdebug_test.go create mode 100644 vendor/go4.org/syncutil/syncutil.go create mode 100644 vendor/go4.org/types/types.go create mode 100644 vendor/go4.org/types/types_test.go create mode 100644 vendor/go4.org/wkfs/gcs/gcs.go create mode 100644 vendor/go4.org/wkfs/gcs/gcs_test.go create mode 100644 vendor/go4.org/wkfs/wkfs.go create mode 100644 vendor/go4.org/writerutil/writerutil.go create mode 100644 vendor/go4.org/writerutil/writerutil_test.go create mode 100644 vendor/golang.org/x/crypto/.gitattributes create mode 100644 vendor/golang.org/x/crypto/.gitignore create mode 100644 vendor/golang.org/x/crypto/AUTHORS create mode 100644 vendor/golang.org/x/crypto/CONTRIBUTING.md create mode 100644 vendor/golang.org/x/crypto/CONTRIBUTORS create mode 100644 vendor/golang.org/x/crypto/LICENSE create mode 100644 vendor/golang.org/x/crypto/PATENTS create mode 100644 vendor/golang.org/x/crypto/README create mode 100644 vendor/golang.org/x/crypto/bcrypt/base64.go create mode 100644 vendor/golang.org/x/crypto/bcrypt/bcrypt.go create mode 100644 vendor/golang.org/x/crypto/bcrypt/bcrypt_test.go create mode 100644 vendor/golang.org/x/crypto/blowfish/block.go create mode 100644 vendor/golang.org/x/crypto/blowfish/blowfish_test.go create mode 100644 vendor/golang.org/x/crypto/blowfish/cipher.go create mode 100644 vendor/golang.org/x/crypto/blowfish/const.go create mode 100644 vendor/golang.org/x/crypto/bn256/bn256.go create mode 100644 vendor/golang.org/x/crypto/bn256/bn256_test.go create mode 100644 vendor/golang.org/x/crypto/bn256/constants.go create mode 100644 vendor/golang.org/x/crypto/bn256/curve.go create mode 100644 vendor/golang.org/x/crypto/bn256/example_test.go create mode 100644 vendor/golang.org/x/crypto/bn256/gfp12.go create mode 100644 vendor/golang.org/x/crypto/bn256/gfp2.go create mode 100644 vendor/golang.org/x/crypto/bn256/gfp6.go create mode 100644 vendor/golang.org/x/crypto/bn256/optate.go create mode 100644 vendor/golang.org/x/crypto/bn256/twist.go create mode 100644 vendor/golang.org/x/crypto/codereview.cfg create mode 100644 vendor/golang.org/x/crypto/curve25519/const_amd64.s create mode 100644 vendor/golang.org/x/crypto/curve25519/cswap_amd64.s create mode 100644 vendor/golang.org/x/crypto/curve25519/curve25519.go create mode 100644 vendor/golang.org/x/crypto/curve25519/curve25519_test.go create mode 100644 vendor/golang.org/x/crypto/curve25519/doc.go create mode 100644 vendor/golang.org/x/crypto/curve25519/freeze_amd64.s create mode 100644 vendor/golang.org/x/crypto/curve25519/ladderstep_amd64.s create mode 100644 vendor/golang.org/x/crypto/curve25519/mont25519_amd64.go create mode 100644 vendor/golang.org/x/crypto/curve25519/mul_amd64.s create mode 100644 vendor/golang.org/x/crypto/curve25519/square_amd64.s create mode 100644 vendor/golang.org/x/crypto/hkdf/example_test.go create mode 100644 vendor/golang.org/x/crypto/hkdf/hkdf.go create mode 100644 vendor/golang.org/x/crypto/hkdf/hkdf_test.go create mode 100644 vendor/golang.org/x/crypto/md4/md4.go create mode 100644 vendor/golang.org/x/crypto/md4/md4_test.go create mode 100644 vendor/golang.org/x/crypto/md4/md4block.go create mode 100644 vendor/golang.org/x/crypto/nacl/box/box.go create mode 100644 vendor/golang.org/x/crypto/nacl/box/box_test.go create mode 100644 vendor/golang.org/x/crypto/nacl/secretbox/secretbox.go create mode 100644 vendor/golang.org/x/crypto/nacl/secretbox/secretbox_test.go create mode 100644 vendor/golang.org/x/crypto/ocsp/ocsp.go create mode 100644 vendor/golang.org/x/crypto/ocsp/ocsp_test.go create mode 100644 vendor/golang.org/x/crypto/otr/libotr_test_helper.c create mode 100644 vendor/golang.org/x/crypto/otr/otr.go create mode 100644 vendor/golang.org/x/crypto/otr/otr_test.go create mode 100644 vendor/golang.org/x/crypto/otr/smp.go create mode 100644 vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go create mode 100644 vendor/golang.org/x/crypto/pbkdf2/pbkdf2_test.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/bmp-string.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/bmp-string_test.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/crypto.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/crypto_test.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/errors.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/internal/rc2/bench_test.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/internal/rc2/rc2.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/internal/rc2/rc2_test.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/mac.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/mac_test.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/pbkdf.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/pbkdf_test.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/pkcs12.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/pkcs12_test.go create mode 100644 vendor/golang.org/x/crypto/pkcs12/safebags.go create mode 100644 vendor/golang.org/x/crypto/poly1305/const_amd64.s create mode 100644 vendor/golang.org/x/crypto/poly1305/poly1305.go create mode 100644 vendor/golang.org/x/crypto/poly1305/poly1305_amd64.s create mode 100644 vendor/golang.org/x/crypto/poly1305/poly1305_arm.s create mode 100644 vendor/golang.org/x/crypto/poly1305/poly1305_test.go create mode 100644 vendor/golang.org/x/crypto/poly1305/sum_amd64.go create mode 100644 vendor/golang.org/x/crypto/poly1305/sum_arm.go create mode 100644 vendor/golang.org/x/crypto/poly1305/sum_ref.go create mode 100644 vendor/golang.org/x/crypto/ripemd160/ripemd160.go create mode 100644 vendor/golang.org/x/crypto/ripemd160/ripemd160_test.go create mode 100644 vendor/golang.org/x/crypto/ripemd160/ripemd160block.go create mode 100644 vendor/golang.org/x/crypto/salsa20/salsa/hsalsa20.go create mode 100644 vendor/golang.org/x/crypto/salsa20/salsa/salsa2020_amd64.s create mode 100644 vendor/golang.org/x/crypto/salsa20/salsa/salsa208.go create mode 100644 vendor/golang.org/x/crypto/salsa20/salsa/salsa20_amd64.go create mode 100644 vendor/golang.org/x/crypto/salsa20/salsa/salsa20_ref.go create mode 100644 vendor/golang.org/x/crypto/salsa20/salsa/salsa_test.go create mode 100644 vendor/golang.org/x/crypto/salsa20/salsa20.go create mode 100644 vendor/golang.org/x/crypto/salsa20/salsa20_test.go create mode 100644 vendor/golang.org/x/crypto/scrypt/scrypt.go create mode 100644 vendor/golang.org/x/crypto/scrypt/scrypt_test.go create mode 100644 vendor/golang.org/x/crypto/sha3/doc.go create mode 100644 vendor/golang.org/x/crypto/sha3/hashes.go create mode 100644 vendor/golang.org/x/crypto/sha3/keccakf.go create mode 100644 vendor/golang.org/x/crypto/sha3/register.go create mode 100644 vendor/golang.org/x/crypto/sha3/sha3.go create mode 100644 vendor/golang.org/x/crypto/sha3/sha3_test.go create mode 100644 vendor/golang.org/x/crypto/sha3/shake.go create mode 100644 vendor/golang.org/x/crypto/sha3/testdata/keccakKats.json.deflate create mode 100644 vendor/golang.org/x/crypto/sha3/xor.go create mode 100644 vendor/golang.org/x/crypto/sha3/xor_generic.go create mode 100644 vendor/golang.org/x/crypto/sha3/xor_unaligned.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/client.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/client_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/example_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/forward.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/keyring.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/keyring_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/server.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/server_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/testdata_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/benchmark_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/buffer.go create mode 100644 vendor/golang.org/x/crypto/ssh/buffer_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/certs.go create mode 100644 vendor/golang.org/x/crypto/ssh/certs_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/channel.go create mode 100644 vendor/golang.org/x/crypto/ssh/cipher.go create mode 100644 vendor/golang.org/x/crypto/ssh/cipher_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/client.go create mode 100644 vendor/golang.org/x/crypto/ssh/client_auth.go create mode 100644 vendor/golang.org/x/crypto/ssh/client_auth_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/client_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/common.go create mode 100644 vendor/golang.org/x/crypto/ssh/connection.go create mode 100644 vendor/golang.org/x/crypto/ssh/doc.go create mode 100644 vendor/golang.org/x/crypto/ssh/example_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/handshake.go create mode 100644 vendor/golang.org/x/crypto/ssh/handshake_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/kex.go create mode 100644 vendor/golang.org/x/crypto/ssh/kex_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/keys.go create mode 100644 vendor/golang.org/x/crypto/ssh/keys_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/mac.go create mode 100644 vendor/golang.org/x/crypto/ssh/mempipe_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/messages.go create mode 100644 vendor/golang.org/x/crypto/ssh/messages_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/mux.go create mode 100644 vendor/golang.org/x/crypto/ssh/mux_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/server.go create mode 100644 vendor/golang.org/x/crypto/ssh/session.go create mode 100644 vendor/golang.org/x/crypto/ssh/session_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/tcpip.go create mode 100644 vendor/golang.org/x/crypto/ssh/tcpip_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/terminal.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/terminal_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_linux.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_windows.go create mode 100644 vendor/golang.org/x/crypto/ssh/test/agent_unix_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/test/cert_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/test/doc.go create mode 100644 vendor/golang.org/x/crypto/ssh/test/forward_unix_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/test/session_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/test/tcpip_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/test/test_unix_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/test/testdata_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/testdata/doc.go create mode 100644 vendor/golang.org/x/crypto/ssh/testdata/keys.go create mode 100644 vendor/golang.org/x/crypto/ssh/testdata_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/transport.go create mode 100644 vendor/golang.org/x/crypto/ssh/transport_test.go create mode 100644 vendor/golang.org/x/crypto/tea/cipher.go create mode 100644 vendor/golang.org/x/crypto/tea/tea_test.go create mode 100644 vendor/golang.org/x/crypto/twofish/twofish.go create mode 100644 vendor/golang.org/x/crypto/twofish/twofish_test.go create mode 100644 vendor/golang.org/x/crypto/xtea/block.go create mode 100644 vendor/golang.org/x/crypto/xtea/cipher.go create mode 100644 vendor/golang.org/x/crypto/xtea/xtea_test.go create mode 100644 vendor/golang.org/x/crypto/xts/xts.go create mode 100644 vendor/golang.org/x/crypto/xts/xts_test.go create mode 100644 vendor/golang.org/x/net/.gitattributes create mode 100644 vendor/golang.org/x/net/.gitignore create mode 100644 vendor/golang.org/x/net/AUTHORS create mode 100644 vendor/golang.org/x/net/CONTRIBUTING.md create mode 100644 vendor/golang.org/x/net/CONTRIBUTORS create mode 100644 vendor/golang.org/x/net/LICENSE create mode 100644 vendor/golang.org/x/net/PATENTS create mode 100644 vendor/golang.org/x/net/README create mode 100644 vendor/golang.org/x/net/bpf/asm.go create mode 100644 vendor/golang.org/x/net/bpf/constants.go create mode 100644 vendor/golang.org/x/net/bpf/doc.go create mode 100644 vendor/golang.org/x/net/bpf/instructions.go create mode 100644 vendor/golang.org/x/net/bpf/instructions_test.go create mode 100644 vendor/golang.org/x/net/bpf/testdata/all_instructions.bpf create mode 100644 vendor/golang.org/x/net/bpf/testdata/all_instructions.txt create mode 100644 vendor/golang.org/x/net/codereview.cfg create mode 100644 vendor/golang.org/x/net/dict/dict.go create mode 100644 vendor/golang.org/x/net/html/atom/atom.go create mode 100644 vendor/golang.org/x/net/html/atom/atom_test.go create mode 100644 vendor/golang.org/x/net/html/atom/gen.go create mode 100644 vendor/golang.org/x/net/html/atom/table.go create mode 100644 vendor/golang.org/x/net/html/atom/table_test.go create mode 100644 vendor/golang.org/x/net/html/charset/charset.go create mode 100644 vendor/golang.org/x/net/html/charset/charset_test.go create mode 100644 vendor/golang.org/x/net/html/charset/testdata/HTTP-charset.html create mode 100644 vendor/golang.org/x/net/html/charset/testdata/HTTP-vs-UTF-8-BOM.html create mode 100644 vendor/golang.org/x/net/html/charset/testdata/HTTP-vs-meta-charset.html create mode 100644 vendor/golang.org/x/net/html/charset/testdata/HTTP-vs-meta-content.html create mode 100644 vendor/golang.org/x/net/html/charset/testdata/No-encoding-declaration.html create mode 100644 vendor/golang.org/x/net/html/charset/testdata/README create mode 100644 vendor/golang.org/x/net/html/charset/testdata/UTF-16BE-BOM.html create mode 100644 vendor/golang.org/x/net/html/charset/testdata/UTF-16LE-BOM.html create mode 100644 vendor/golang.org/x/net/html/charset/testdata/UTF-8-BOM-vs-meta-charset.html create mode 100644 vendor/golang.org/x/net/html/charset/testdata/UTF-8-BOM-vs-meta-content.html create mode 100644 vendor/golang.org/x/net/html/charset/testdata/meta-charset-attribute.html create mode 100644 vendor/golang.org/x/net/html/charset/testdata/meta-content-attribute.html create mode 100644 vendor/golang.org/x/net/html/const.go create mode 100644 vendor/golang.org/x/net/html/doc.go create mode 100644 vendor/golang.org/x/net/html/doctype.go create mode 100644 vendor/golang.org/x/net/html/entity.go create mode 100644 vendor/golang.org/x/net/html/entity_test.go create mode 100644 vendor/golang.org/x/net/html/escape.go create mode 100644 vendor/golang.org/x/net/html/escape_test.go create mode 100644 vendor/golang.org/x/net/html/example_test.go create mode 100644 vendor/golang.org/x/net/html/foreign.go create mode 100644 vendor/golang.org/x/net/html/node.go create mode 100644 vendor/golang.org/x/net/html/node_test.go create mode 100644 vendor/golang.org/x/net/html/parse.go create mode 100644 vendor/golang.org/x/net/html/parse_test.go create mode 100644 vendor/golang.org/x/net/html/render.go create mode 100644 vendor/golang.org/x/net/html/render_test.go create mode 100644 vendor/golang.org/x/net/html/testdata/go1.html create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/README create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/adoption01.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/adoption02.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/comments01.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/doctype01.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/entities01.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/entities02.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/html5test-com.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/inbody01.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/isindex.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/pending-spec-changes-plain-text-unsafe.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/pending-spec-changes.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/plain-text-unsafe.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/scriptdata01.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/scripted/adoption01.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/scripted/webkit01.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tables01.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests1.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests10.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests11.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests12.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests14.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests15.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests16.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests17.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests18.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests19.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests2.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests20.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests21.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests22.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests23.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests24.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests25.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests26.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests3.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests4.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests5.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests6.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests7.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests8.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests9.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tests_innerHTML_1.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/tricky01.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/webkit01.dat create mode 100644 vendor/golang.org/x/net/html/testdata/webkit/webkit02.dat create mode 100644 vendor/golang.org/x/net/html/token.go create mode 100644 vendor/golang.org/x/net/html/token_test.go create mode 100644 vendor/golang.org/x/net/icmp/dstunreach.go create mode 100644 vendor/golang.org/x/net/icmp/echo.go create mode 100644 vendor/golang.org/x/net/icmp/endpoint.go create mode 100644 vendor/golang.org/x/net/icmp/example_test.go create mode 100644 vendor/golang.org/x/net/icmp/extension.go create mode 100644 vendor/golang.org/x/net/icmp/extension_test.go create mode 100644 vendor/golang.org/x/net/icmp/helper.go create mode 100644 vendor/golang.org/x/net/icmp/helper_posix.go create mode 100644 vendor/golang.org/x/net/icmp/interface.go create mode 100644 vendor/golang.org/x/net/icmp/ipv4.go create mode 100644 vendor/golang.org/x/net/icmp/ipv4_test.go create mode 100644 vendor/golang.org/x/net/icmp/ipv6.go create mode 100644 vendor/golang.org/x/net/icmp/listen_posix.go create mode 100644 vendor/golang.org/x/net/icmp/listen_stub.go create mode 100644 vendor/golang.org/x/net/icmp/message.go create mode 100644 vendor/golang.org/x/net/icmp/message_test.go create mode 100644 vendor/golang.org/x/net/icmp/messagebody.go create mode 100644 vendor/golang.org/x/net/icmp/mpls.go create mode 100644 vendor/golang.org/x/net/icmp/multipart.go create mode 100644 vendor/golang.org/x/net/icmp/multipart_test.go create mode 100644 vendor/golang.org/x/net/icmp/packettoobig.go create mode 100644 vendor/golang.org/x/net/icmp/paramprob.go create mode 100644 vendor/golang.org/x/net/icmp/ping_test.go create mode 100644 vendor/golang.org/x/net/icmp/sys_freebsd.go create mode 100644 vendor/golang.org/x/net/icmp/timeexceeded.go create mode 100644 vendor/golang.org/x/net/idna/idna.go create mode 100644 vendor/golang.org/x/net/idna/idna_test.go create mode 100644 vendor/golang.org/x/net/idna/punycode.go create mode 100644 vendor/golang.org/x/net/idna/punycode_test.go create mode 100644 vendor/golang.org/x/net/internal/iana/const.go create mode 100644 vendor/golang.org/x/net/internal/iana/gen.go create mode 100644 vendor/golang.org/x/net/internal/nettest/error_posix.go create mode 100644 vendor/golang.org/x/net/internal/nettest/error_stub.go create mode 100644 vendor/golang.org/x/net/internal/nettest/interface.go create mode 100644 vendor/golang.org/x/net/internal/nettest/rlimit.go create mode 100644 vendor/golang.org/x/net/internal/nettest/rlimit_stub.go create mode 100644 vendor/golang.org/x/net/internal/nettest/rlimit_unix.go create mode 100644 vendor/golang.org/x/net/internal/nettest/rlimit_windows.go create mode 100644 vendor/golang.org/x/net/internal/nettest/stack.go create mode 100644 vendor/golang.org/x/net/internal/nettest/stack_stub.go create mode 100644 vendor/golang.org/x/net/internal/nettest/stack_unix.go create mode 100644 vendor/golang.org/x/net/internal/nettest/stack_windows.go create mode 100644 vendor/golang.org/x/net/ipv4/control.go create mode 100644 vendor/golang.org/x/net/ipv4/control_bsd.go create mode 100644 vendor/golang.org/x/net/ipv4/control_pktinfo.go create mode 100644 vendor/golang.org/x/net/ipv4/control_stub.go create mode 100644 vendor/golang.org/x/net/ipv4/control_unix.go create mode 100644 vendor/golang.org/x/net/ipv4/control_windows.go create mode 100644 vendor/golang.org/x/net/ipv4/defs_darwin.go create mode 100644 vendor/golang.org/x/net/ipv4/defs_dragonfly.go create mode 100644 vendor/golang.org/x/net/ipv4/defs_freebsd.go create mode 100644 vendor/golang.org/x/net/ipv4/defs_linux.go create mode 100644 vendor/golang.org/x/net/ipv4/defs_netbsd.go create mode 100644 vendor/golang.org/x/net/ipv4/defs_openbsd.go create mode 100644 vendor/golang.org/x/net/ipv4/defs_solaris.go create mode 100644 vendor/golang.org/x/net/ipv4/dgramopt_posix.go create mode 100644 vendor/golang.org/x/net/ipv4/dgramopt_stub.go create mode 100644 vendor/golang.org/x/net/ipv4/doc.go create mode 100644 vendor/golang.org/x/net/ipv4/endpoint.go create mode 100644 vendor/golang.org/x/net/ipv4/example_test.go create mode 100644 vendor/golang.org/x/net/ipv4/gen.go create mode 100644 vendor/golang.org/x/net/ipv4/genericopt_posix.go create mode 100644 vendor/golang.org/x/net/ipv4/genericopt_stub.go create mode 100644 vendor/golang.org/x/net/ipv4/header.go create mode 100644 vendor/golang.org/x/net/ipv4/header_test.go create mode 100644 vendor/golang.org/x/net/ipv4/helper.go create mode 100644 vendor/golang.org/x/net/ipv4/helper_stub.go create mode 100644 vendor/golang.org/x/net/ipv4/helper_unix.go create mode 100644 vendor/golang.org/x/net/ipv4/helper_windows.go create mode 100644 vendor/golang.org/x/net/ipv4/iana.go create mode 100644 vendor/golang.org/x/net/ipv4/icmp.go create mode 100644 vendor/golang.org/x/net/ipv4/icmp_linux.go create mode 100644 vendor/golang.org/x/net/ipv4/icmp_stub.go create mode 100644 vendor/golang.org/x/net/ipv4/icmp_test.go create mode 100644 vendor/golang.org/x/net/ipv4/mocktransponder_test.go create mode 100644 vendor/golang.org/x/net/ipv4/multicast_test.go create mode 100644 vendor/golang.org/x/net/ipv4/multicastlistener_test.go create mode 100644 vendor/golang.org/x/net/ipv4/multicastsockopt_test.go create mode 100644 vendor/golang.org/x/net/ipv4/packet.go create mode 100644 vendor/golang.org/x/net/ipv4/payload.go create mode 100644 vendor/golang.org/x/net/ipv4/payload_cmsg.go create mode 100644 vendor/golang.org/x/net/ipv4/payload_nocmsg.go create mode 100644 vendor/golang.org/x/net/ipv4/readwrite_test.go create mode 100644 vendor/golang.org/x/net/ipv4/sockopt.go create mode 100644 vendor/golang.org/x/net/ipv4/sockopt_asmreq.go create mode 100644 vendor/golang.org/x/net/ipv4/sockopt_asmreq_stub.go create mode 100644 vendor/golang.org/x/net/ipv4/sockopt_asmreq_unix.go create mode 100644 vendor/golang.org/x/net/ipv4/sockopt_asmreq_windows.go create mode 100644 vendor/golang.org/x/net/ipv4/sockopt_asmreqn_stub.go create mode 100644 vendor/golang.org/x/net/ipv4/sockopt_asmreqn_unix.go create mode 100644 vendor/golang.org/x/net/ipv4/sockopt_ssmreq_stub.go create mode 100644 vendor/golang.org/x/net/ipv4/sockopt_ssmreq_unix.go create mode 100644 vendor/golang.org/x/net/ipv4/sockopt_stub.go create mode 100644 vendor/golang.org/x/net/ipv4/sockopt_unix.go create mode 100644 vendor/golang.org/x/net/ipv4/sockopt_windows.go create mode 100644 vendor/golang.org/x/net/ipv4/sys_bsd.go create mode 100644 vendor/golang.org/x/net/ipv4/sys_darwin.go create mode 100644 vendor/golang.org/x/net/ipv4/sys_freebsd.go create mode 100644 vendor/golang.org/x/net/ipv4/sys_linux.go create mode 100644 vendor/golang.org/x/net/ipv4/sys_openbsd.go create mode 100644 vendor/golang.org/x/net/ipv4/sys_stub.go create mode 100644 vendor/golang.org/x/net/ipv4/sys_windows.go create mode 100644 vendor/golang.org/x/net/ipv4/syscall_linux_386.go create mode 100644 vendor/golang.org/x/net/ipv4/syscall_unix.go create mode 100644 vendor/golang.org/x/net/ipv4/thunk_linux_386.s create mode 100644 vendor/golang.org/x/net/ipv4/unicast_test.go create mode 100644 vendor/golang.org/x/net/ipv4/unicastsockopt_test.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_darwin.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_dragonfly.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_freebsd_386.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_freebsd_amd64.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_freebsd_arm.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_linux_386.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_linux_amd64.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_linux_arm.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_linux_arm64.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_linux_mips64.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_linux_mips64le.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_linux_ppc64.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_linux_ppc64le.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_netbsd.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_openbsd.go create mode 100644 vendor/golang.org/x/net/ipv4/zsys_solaris.go create mode 100644 vendor/golang.org/x/net/ipv6/control.go create mode 100644 vendor/golang.org/x/net/ipv6/control_rfc2292_unix.go create mode 100644 vendor/golang.org/x/net/ipv6/control_rfc3542_unix.go create mode 100644 vendor/golang.org/x/net/ipv6/control_stub.go create mode 100644 vendor/golang.org/x/net/ipv6/control_unix.go create mode 100644 vendor/golang.org/x/net/ipv6/control_windows.go create mode 100644 vendor/golang.org/x/net/ipv6/defs_darwin.go create mode 100644 vendor/golang.org/x/net/ipv6/defs_dragonfly.go create mode 100644 vendor/golang.org/x/net/ipv6/defs_freebsd.go create mode 100644 vendor/golang.org/x/net/ipv6/defs_linux.go create mode 100644 vendor/golang.org/x/net/ipv6/defs_netbsd.go create mode 100644 vendor/golang.org/x/net/ipv6/defs_openbsd.go create mode 100644 vendor/golang.org/x/net/ipv6/defs_solaris.go create mode 100644 vendor/golang.org/x/net/ipv6/dgramopt_posix.go create mode 100644 vendor/golang.org/x/net/ipv6/dgramopt_stub.go create mode 100644 vendor/golang.org/x/net/ipv6/doc.go create mode 100644 vendor/golang.org/x/net/ipv6/endpoint.go create mode 100644 vendor/golang.org/x/net/ipv6/example_test.go create mode 100644 vendor/golang.org/x/net/ipv6/gen.go create mode 100644 vendor/golang.org/x/net/ipv6/genericopt_posix.go create mode 100644 vendor/golang.org/x/net/ipv6/genericopt_stub.go create mode 100644 vendor/golang.org/x/net/ipv6/header.go create mode 100644 vendor/golang.org/x/net/ipv6/header_test.go create mode 100644 vendor/golang.org/x/net/ipv6/helper.go create mode 100644 vendor/golang.org/x/net/ipv6/helper_stub.go create mode 100644 vendor/golang.org/x/net/ipv6/helper_unix.go create mode 100644 vendor/golang.org/x/net/ipv6/helper_windows.go create mode 100644 vendor/golang.org/x/net/ipv6/iana.go create mode 100644 vendor/golang.org/x/net/ipv6/icmp.go create mode 100644 vendor/golang.org/x/net/ipv6/icmp_bsd.go create mode 100644 vendor/golang.org/x/net/ipv6/icmp_linux.go create mode 100644 vendor/golang.org/x/net/ipv6/icmp_solaris.go create mode 100644 vendor/golang.org/x/net/ipv6/icmp_stub.go create mode 100644 vendor/golang.org/x/net/ipv6/icmp_test.go create mode 100644 vendor/golang.org/x/net/ipv6/icmp_windows.go create mode 100644 vendor/golang.org/x/net/ipv6/mocktransponder_test.go create mode 100644 vendor/golang.org/x/net/ipv6/multicast_test.go create mode 100644 vendor/golang.org/x/net/ipv6/multicastlistener_test.go create mode 100644 vendor/golang.org/x/net/ipv6/multicastsockopt_test.go create mode 100644 vendor/golang.org/x/net/ipv6/payload.go create mode 100644 vendor/golang.org/x/net/ipv6/payload_cmsg.go create mode 100644 vendor/golang.org/x/net/ipv6/payload_nocmsg.go create mode 100644 vendor/golang.org/x/net/ipv6/readwrite_test.go create mode 100644 vendor/golang.org/x/net/ipv6/sockopt.go create mode 100644 vendor/golang.org/x/net/ipv6/sockopt_asmreq_unix.go create mode 100644 vendor/golang.org/x/net/ipv6/sockopt_asmreq_windows.go create mode 100644 vendor/golang.org/x/net/ipv6/sockopt_ssmreq_stub.go create mode 100644 vendor/golang.org/x/net/ipv6/sockopt_ssmreq_unix.go create mode 100644 vendor/golang.org/x/net/ipv6/sockopt_stub.go create mode 100644 vendor/golang.org/x/net/ipv6/sockopt_test.go create mode 100644 vendor/golang.org/x/net/ipv6/sockopt_unix.go create mode 100644 vendor/golang.org/x/net/ipv6/sockopt_windows.go create mode 100644 vendor/golang.org/x/net/ipv6/sys_bsd.go create mode 100644 vendor/golang.org/x/net/ipv6/sys_darwin.go create mode 100644 vendor/golang.org/x/net/ipv6/sys_freebsd.go create mode 100644 vendor/golang.org/x/net/ipv6/sys_linux.go create mode 100644 vendor/golang.org/x/net/ipv6/sys_stub.go create mode 100644 vendor/golang.org/x/net/ipv6/sys_windows.go create mode 100644 vendor/golang.org/x/net/ipv6/syscall_linux_386.go create mode 100644 vendor/golang.org/x/net/ipv6/syscall_unix.go create mode 100644 vendor/golang.org/x/net/ipv6/thunk_linux_386.s create mode 100644 vendor/golang.org/x/net/ipv6/unicast_test.go create mode 100644 vendor/golang.org/x/net/ipv6/unicastsockopt_test.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_darwin.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_dragonfly.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_freebsd_386.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_freebsd_amd64.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_freebsd_arm.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_linux_386.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_linux_amd64.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_linux_arm.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_linux_arm64.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_linux_mips64.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_linux_mips64le.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_linux_ppc64.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_linux_ppc64le.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_netbsd.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_openbsd.go create mode 100644 vendor/golang.org/x/net/ipv6/zsys_solaris.go create mode 100644 vendor/golang.org/x/net/netutil/listen.go create mode 100644 vendor/golang.org/x/net/netutil/listen_test.go create mode 100644 vendor/golang.org/x/net/proxy/direct.go create mode 100644 vendor/golang.org/x/net/proxy/per_host.go create mode 100644 vendor/golang.org/x/net/proxy/per_host_test.go create mode 100644 vendor/golang.org/x/net/proxy/proxy.go create mode 100644 vendor/golang.org/x/net/proxy/proxy_test.go create mode 100644 vendor/golang.org/x/net/proxy/socks5.go create mode 100644 vendor/golang.org/x/net/publicsuffix/gen.go create mode 100644 vendor/golang.org/x/net/publicsuffix/list.go create mode 100644 vendor/golang.org/x/net/publicsuffix/list_test.go create mode 100644 vendor/golang.org/x/net/publicsuffix/table.go create mode 100644 vendor/golang.org/x/net/publicsuffix/table_test.go create mode 100644 vendor/golang.org/x/net/webdav/file.go create mode 100644 vendor/golang.org/x/net/webdav/file_test.go create mode 100644 vendor/golang.org/x/net/webdav/if.go create mode 100644 vendor/golang.org/x/net/webdav/if_test.go create mode 100644 vendor/golang.org/x/net/webdav/internal/xml/README create mode 100644 vendor/golang.org/x/net/webdav/internal/xml/atom_test.go create mode 100644 vendor/golang.org/x/net/webdav/internal/xml/example_test.go create mode 100644 vendor/golang.org/x/net/webdav/internal/xml/marshal.go create mode 100644 vendor/golang.org/x/net/webdav/internal/xml/marshal_test.go create mode 100644 vendor/golang.org/x/net/webdav/internal/xml/read.go create mode 100644 vendor/golang.org/x/net/webdav/internal/xml/read_test.go create mode 100644 vendor/golang.org/x/net/webdav/internal/xml/typeinfo.go create mode 100644 vendor/golang.org/x/net/webdav/internal/xml/xml.go create mode 100644 vendor/golang.org/x/net/webdav/internal/xml/xml_test.go create mode 100644 vendor/golang.org/x/net/webdav/litmus_test_server.go create mode 100644 vendor/golang.org/x/net/webdav/lock.go create mode 100644 vendor/golang.org/x/net/webdav/lock_test.go create mode 100644 vendor/golang.org/x/net/webdav/prop.go create mode 100644 vendor/golang.org/x/net/webdav/prop_test.go create mode 100644 vendor/golang.org/x/net/webdav/webdav.go create mode 100644 vendor/golang.org/x/net/webdav/webdav_test.go create mode 100644 vendor/golang.org/x/net/webdav/xml.go create mode 100644 vendor/golang.org/x/net/webdav/xml_test.go create mode 100644 vendor/golang.org/x/net/websocket/client.go create mode 100644 vendor/golang.org/x/net/websocket/exampledial_test.go create mode 100644 vendor/golang.org/x/net/websocket/examplehandler_test.go create mode 100644 vendor/golang.org/x/net/websocket/hybi.go create mode 100644 vendor/golang.org/x/net/websocket/hybi_test.go create mode 100644 vendor/golang.org/x/net/websocket/server.go create mode 100644 vendor/golang.org/x/net/websocket/websocket.go create mode 100644 vendor/golang.org/x/net/websocket/websocket_test.go create mode 100644 vendor/golang.org/x/net/xsrftoken/xsrf.go create mode 100644 vendor/golang.org/x/net/xsrftoken/xsrf_test.go mode change 100644 => 100755 vendor/google.golang.org/grpc/codegen.sh mode change 100644 => 100755 vendor/google.golang.org/grpc/coverage.sh mode change 100644 => 100755 vendor/google.golang.org/grpc/interop/grpc_testing/test.pb.go diff --git a/Documentation/dev/develop.md b/Documentation/dev/develop.md index 70143952..6e955cba 100644 --- a/Documentation/dev/develop.md +++ b/Documentation/dev/develop.md @@ -23,7 +23,7 @@ Alternately, build a Docker image `coreos/bootcfg:latest`. sudo ./build-docker -## Check Version +## Version ./bin/bootcfg -version sudo rkt --insecure-options=image run bootcfg.aci -- -version @@ -41,4 +41,19 @@ Run the ACI with rkt on `metal0`. Alternately, run the Docker image on `docker0`. - sudo docker run -p 8080:8080 --rm -v $PWD/examples:/var/lib/bootcfg:Z -v $PWD/examples/groups/etcd-docker:/var/lib/bootcfg/groups:Z coreos/bootcfg:latest -address=0.0.0.0:8080 -log-level=debug \ No newline at end of file + sudo docker run -p 8080:8080 --rm -v $PWD/examples:/var/lib/bootcfg:Z -v $PWD/examples/groups/etcd-docker:/var/lib/bootcfg/groups:Z coreos/bootcfg:latest -address=0.0.0.0:8080 -log-level=debug + +## Dependencies + +Project dependencies are commited to the `vendor` directory, so Go 1.6+ users can clone to their `GOPATH` and build or test immediately. Go 1.5 users should set `GO15VENDOREXPERIMENT=1`. + +Project developers should use [glide](https://github.com/Masterminds/glide) to manage commited dependencies under `vendor`. Configure `glide.yaml` as desired. Use `glide update` to download and update dependencies listed in `glide.yaml` into `/vendor` (do **not** use glide `get`). + + glide update --update-vendored --strip-vendor --strip-vcs + +Recursive dependencies are also vendored. A `glide.lock` will be created to represent the exact versions of each dependency. + +With an empty `vendor` directory, you can install the `glide.lock` dependencies. + + rm -rf vendor/ + glide install --strip-vendor --strip-vcs diff --git a/glide.lock b/glide.lock new file mode 100644 index 00000000..d561e65e --- /dev/null +++ b/glide.lock @@ -0,0 +1,95 @@ +hash: 8f33fd1c87e2136cdff69364e0668a595a129b8ffd686851336c841bf7e4f705 +updated: 2016-05-12T14:04:55.653773498-07:00 +imports: +- name: github.com/alecthomas/units + version: 2efee857e7cfd4f3d0138cc3cbb1b4966962b93a +- name: github.com/camlistore/camlistore + version: 9106ce829629773474c689b34aacd7d3aaa99426 + subpackages: + - pkg/errorutil +- name: github.com/coreos/coreos-cloudinit + version: b3f805dee6a4aa5ed298a1f370284df470eecf43 + subpackages: + - config +- name: github.com/coreos/go-semver + version: 294930c1e79c64e7dbe360054274fdad492c8cf5 + subpackages: + - semver +- name: github.com/coreos/go-systemd + version: 7b2428fec40033549c68f54e26e89e7ca9a9ce31 + subpackages: + - journal +- name: github.com/coreos/ignition + version: 44c274ab414294a8e34b3a940e0ec1afe6b6c610 + subpackages: + - config + - config/types + - config/v1 + - config/v1/types +- name: github.com/coreos/pkg + version: 66fe44ad037ccb80329115cb4db0dbe8e9beb03a + subpackages: + - capnslog + - flagutil +- name: github.com/coreos/yaml + version: 6b16a5714269b2f70720a45406b1babd947a17ef +- name: github.com/davecgh/go-spew + version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d + subpackages: + - spew +- name: github.com/golang/protobuf + version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c + subpackages: + - proto +- name: github.com/inconshreveable/mousetrap + version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +- name: github.com/pmezard/go-difflib + version: 792786c7400a136282c1664665ae0a8db921c6c2 + subpackages: + - difflib +- name: github.com/spf13/cobra + version: 65a708cee0a4424f4e353d031ce440643e312f92 +- name: github.com/spf13/pflag + version: 7f60f83a2c81bc3c3c0d5297f61ddfa68da9d3b7 +- name: github.com/stretchr/testify + version: 1f4a1643a57e798696635ea4c126e9127adb7d3c + subpackages: + - assert +- name: github.com/vincent-petithory/dataurl + version: 9a301d65acbb728fcc3ace14f45f511a4cfeea9c +- name: go4.org + version: 03efcb870d84809319ea509714dd6d19a1498483 + subpackages: + - errorutil +- name: golang.org/x/crypto + version: 5dc8cb4b8a8eb076cbb5a06bc3b8682c15bdbbd3 + subpackages: + - cast5 + - openpgp + - openpgp/armor + - openpgp/errors + - openpgp/packet + - openpgp/s2k + - openpgp/elgamal +- name: golang.org/x/net + version: fb93926129b8ec0056f2f458b1f519654814edf0 + subpackages: + - context + - http2 + - internal/timeseries + - trace + - http2/hpack +- name: google.golang.org/grpc + version: 8eeecf2291de9d171d0b1392a27ff3975679f4f5 + subpackages: + - codes + - credentials + - grpclog + - internal + - metadata + - naming + - transport + - peer +- name: gopkg.in/yaml.v2 + version: f7716cbe52baa25d2e9b0d0da546fcf909fc16b4 +devImports: [] diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 00000000..f49bcd34 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,77 @@ +package: github.com/coreos/coreos-baremetal +import: +- package: github.com/alecthomas/units + version: 2efee857e7cfd4f3d0138cc3cbb1b4966962b93a +- package: github.com/camlistore/camlistore + version: 9106ce829629773474c689b34aacd7d3aaa99426 +- package: github.com/coreos/coreos-cloudinit + version: b3f805dee6a4aa5ed298a1f370284df470eecf43 + subpackages: + - Godeps/_workspace/src/github.com/coreos/yaml + - config +- package: github.com/coreos/go-semver + version: 294930c1e79c64e7dbe360054274fdad492c8cf5 + subpackages: + - semver +- package: github.com/coreos/go-systemd + version: 7b2428fec40033549c68f54e26e89e7ca9a9ce31 + subpackages: + - journal +- package: github.com/coreos/ignition + version: 44c274ab414294a8e34b3a940e0ec1afe6b6c610 + subpackages: + - config + - config/types + - config/v1 + - config/v1/types +- package: github.com/coreos/pkg + version: 66fe44ad037ccb80329115cb4db0dbe8e9beb03a + subpackages: + - capnslog + - flagutil +- package: github.com/davecgh/go-spew + version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d + subpackages: + - spew +- package: github.com/golang/protobuf + version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c + subpackages: + - proto +- package: github.com/pmezard/go-difflib + version: 792786c7400a136282c1664665ae0a8db921c6c2 + subpackages: + - difflib +- package: github.com/spf13/cobra + version: 65a708cee0a4424f4e353d031ce440643e312f92 +- package: github.com/spf13/pflag + version: 7f60f83a2c81bc3c3c0d5297f61ddfa68da9d3b7 +- package: github.com/stretchr/testify + version: 1f4a1643a57e798696635ea4c126e9127adb7d3c + subpackages: + - assert +- package: github.com/vincent-petithory/dataurl + version: 9a301d65acbb728fcc3ace14f45f511a4cfeea9c +- package: go4.org + version: 03efcb870d84809319ea509714dd6d19a1498483 + subpackages: + - errorutil +- package: golang.org/x/crypto + version: 5dc8cb4b8a8eb076cbb5a06bc3b8682c15bdbbd3 + subpackages: + - cast5 + - openpgp +- package: golang.org/x/net + version: fb93926129b8ec0056f2f458b1f519654814edf0 + subpackages: + - context + - http2 + - internal/timeseries + - trace +- package: google.golang.org/grpc + version: 8eeecf2291de9d171d0b1392a27ff3975679f4f5 + subpackages: + - codes +- package: gopkg.in/yaml.v2 + version: f7716cbe52baa25d2e9b0d0da546fcf909fc16b4 +- package: github.com/coreos/yaml + version: 6b16a5714269b2f70720a45406b1babd947a17ef diff --git a/vendor/github.com/camlistore/camlistore/.gitignore b/vendor/github.com/camlistore/camlistore/.gitignore new file mode 100644 index 00000000..57a50967 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/.gitignore @@ -0,0 +1,34 @@ +*~ +*.o +*.pyc +\#*\# +.\#* +.*.swp +logs +_obj +[68].out +_test +_gotest* +_testmain* +_go_.[568] +_cgo* +clients/go/camgsinit/camgsinit +clients/go/camwebdav/camwebdav +.goroot +appengine-sdk +build/root +.DS_Store +bin/cam* +bin/devcam +bin/*_* +bin/hello +bin/publisher +tmp +server/camlistored/newui/all.js +server/camlistored/newui/all.js.map +server/camlistored/newui/zembed_all.js.go +server/appengine/source_root/ +config/tls.* +misc/docker/djpeg-static/djpeg +misc/docker/camlistored/camlistored* +misc/docker/camlistored/djpeg diff --git a/vendor/github.com/camlistore/camlistore/.hackfests/2010-12-01.txt b/vendor/github.com/camlistore/camlistore/.hackfests/2010-12-01.txt new file mode 100644 index 00000000..8f8b6b62 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/.hackfests/2010-12-01.txt @@ -0,0 +1,23 @@ +Brett and I eating Burritos from Little Chihuahua at my house. + +Plan is for Brett to work on Clip-it-Good (the Chrome Extension)'s +camli support (currently non-existent), and get it to: + + -- select an image + -- upload the image blob + -- create the permanode blob + -- create (and sign, with the signing server) the "become" claim, + pointing the permanode at the image + -- create (a signed) "tag" blob, tagging an image e.g. "funny" + +I will work on docs & signing server tests & signing verification +endpoint. + + +-------------- + +Done: + + * Brad: docs re-organized + * Brad: camlistore.{com,org,net,info,us} domains purchased + diff --git a/vendor/github.com/camlistore/camlistore/.hackfests/2012-11-03.txt b/vendor/github.com/camlistore/camlistore/.hackfests/2012-11-03.txt new file mode 100644 index 00000000..41b65d92 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/.hackfests/2012-11-03.txt @@ -0,0 +1,4 @@ +Saturday & Sunday in Paris with Mathieu, meeting for the first time, +hacking on EXIF rotation, thumbnail indexing, Postgres support, and +then Monday at Google Paris, working on different parts of the UI +permanode thumbnail page, and genfileembed problems. diff --git a/vendor/github.com/camlistore/camlistore/.hackfests/2012-12-23.txt b/vendor/github.com/camlistore/camlistore/.hackfests/2012-12-23.txt new file mode 100644 index 00000000..0abf5809 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/.hackfests/2012-12-23.txt @@ -0,0 +1 @@ +Closure newui hacking with bslatkin. diff --git a/vendor/github.com/camlistore/camlistore/.hackfests/2013-01-20.txt b/vendor/github.com/camlistore/camlistore/.hackfests/2013-01-20.txt new file mode 100644 index 00000000..3af57bed --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/.hackfests/2013-01-20.txt @@ -0,0 +1,5 @@ +At Brett's place, with Brett Slatkin, Lindsey Simon, Ryan Barrett. + +Goal: More closure UI stuff. + +Ryan getting up-to-speed and maybe working on Activity Streams import. diff --git a/vendor/github.com/camlistore/camlistore/.hackfests/2013-12-27.txt b/vendor/github.com/camlistore/camlistore/.hackfests/2013-12-27.txt new file mode 100644 index 00000000..23ca80e2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/.hackfests/2013-12-27.txt @@ -0,0 +1,8 @@ +Aaron Boodman, react js permanode UI +Andy Smith, discussing data model, updating HACKING, etc +Brad Fitzpatrick, misc +Brett Slatkin, web screenshotting client app, claim creation API/ACLs +Daisy Stanton, her own thing +Dan Erat, music player app +Emil Eklund, getting up to speed, photo stuff +Nick O'Neill, iOS diff --git a/vendor/github.com/camlistore/camlistore/.header b/vendor/github.com/camlistore/camlistore/.header new file mode 100644 index 00000000..aeaff6c0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/.header @@ -0,0 +1,20 @@ +/* +Copyright 2015 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package x + +import ( +) diff --git a/vendor/github.com/camlistore/camlistore/AUTHORS b/vendor/github.com/camlistore/camlistore/AUTHORS new file mode 100644 index 00000000..983d7b05 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/AUTHORS @@ -0,0 +1,65 @@ +# This is the official list of Camlistore authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +Aaron Bieber +Alessandro Arzilli gh=aarzilli +Amir Mohammad Saied +Amit Levy +Andy Smith +Anthony Martin +Antonin Amand +Antti Rasinen +Armen Baghumian +Bret Comnes +Brian Marete +Caine Tighe +Dan Kortschak +Daniel Coonce +Daniel Dermott Bryan +Daniel Pupius +Dean Landolt +Dustin Sallings +Edward Sheffler III +Emil Hessman +Eric Drechsel +Fabian Wickborn +Gina White +Google Inc. +Govert Versluis +Hernan Grecco +Iain Peet +Jakub Brzeski +Jani Monoses +Jingguo Yao +Josh Bleecher Snyder +Josh Huckabee +Joshua Gay +Jrabbit +Julien Danjou +Kamil Kisiel +Kristopher Cost +Lindsey Simon +Mario Russo +Mateus Braga +Mathieu Lonjaret +Matthieu Rakotojaona Rainimangavelo +Matt Jibson +Maxime Lavigne +Michael Vincent Zuffoletti +Nick O'Neill +Nolan Darilek +Philio +Piotr Staszewski +Ranveer +Ritesh Sinha +Rob Young +Robert Obryk +Robert Hencke +Salman Aljammaz +Sarath Lakshman +Steve Phillips +Steven L. Speek +Tamás Gulácsi +Timo Truyts +Ulf Holm Nielsen diff --git a/vendor/github.com/camlistore/camlistore/BUILDING b/vendor/github.com/camlistore/camlistore/BUILDING new file mode 100644 index 00000000..2ce7ab2e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/BUILDING @@ -0,0 +1,12 @@ +To build Camlistore: + +1) Install Go 1.5 or later. + +2) cd to the root of the Camlistore source (where this file is) + +3) Run: + + $ go run make.go + +4) The compiled binaries should now be in the "bin" subdirectory: + camlistored (the server), camget, camput, and camtool. diff --git a/vendor/github.com/camlistore/camlistore/CONTRIBUTORS b/vendor/github.com/camlistore/camlistore/CONTRIBUTORS new file mode 100644 index 00000000..6e6c3093 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/CONTRIBUTORS @@ -0,0 +1,93 @@ +# People who have agreed to one of the CLAs and can contribute patches. +# The AUTHORS file lists the copyright holders; this file +# lists people. For example, Google employees are listed here +# but not in AUTHORS, because Google holds the copyright. +# +# http://code.google.com/legal/individual-cla-v1.0.html (electronic submission) +# http://code.google.com/legal/corporate-cla-v1.0.html (requires FAX) +# +# Note that the CLA isn't a copyright _assignment_ but rather a +# copyright _license_. You retain the copyright on your +# contributions. + +Aaron Bieber +Aaron Boodman +Aaron Racine +Adam Langley +Alessandro Arzilli gh=aarzilli +Ali Afshar +Amir Mohammad Saied +Amit Levy +Andrew Gerrand +Andy Smith +Anthony Martin +Antonin Amand +Antti Rasinen +Armen Baghumian +Bill Thiede +Brad Fitzpatrick +Bret Comnes +Brett Slatkin +Brian Marete +Burcu Dogan +Caine Tighe +Dan Kortschak +Daniel Coonce +Daniel Dermott Bryan +Daniel Erat +Daniel Pupius +Dean Landolt +Dustin Sallings +Edward Sheffler III +Emil Hessman +Eric Drechsel +Evan Martin +Fabian Wickborn +Gina White +Govert Versluis +Han-Wen Nienhuys +Hernan Grecco +Iain Peet +Jakub Brzeski +Jani Monoses +Jingguo Yao +Johan Euphrosine +Josh Bleecher Snyder +Josh Huckabee +Joshua Gay +Jrabbit +Julien Danjou +Kamil Kisiel +Kristopher Cost +Lindsey Simon +Marc-Antoine Ruel +Mario Russo +Mateus Braga +Mathieu Lonjaret +Matthieu Rakotojaona Rainimangavelo +Matt Jibson +Maxime Lavigne +Michael Vincent Zuffoletti +Nick O'Neill +Nico Weber +Nigel Tao +Nolan Darilek +Pawel Szczur +Philio +Piotr Staszewski +Ranveer +Ritesh Sinha +Rob Young +Robert Hencke +Robert Kroeger +Robert Obryk +Ryan Barrett +Salman Aljammaz +Sarath Lakshman +Steve Phillips +Steven L. Speek +Tamás Gulácsi +Timo Truyts +Tony Chang +Tony Scelfo +Ulf Holm Nielsen diff --git a/vendor/github.com/coreos/ignition/config/vendor/github.com/coreos/go-semver/LICENSE b/vendor/github.com/camlistore/camlistore/COPYING similarity index 100% rename from vendor/github.com/coreos/ignition/config/vendor/github.com/coreos/go-semver/LICENSE rename to vendor/github.com/camlistore/camlistore/COPYING diff --git a/vendor/github.com/camlistore/camlistore/Dockerfile b/vendor/github.com/camlistore/camlistore/Dockerfile new file mode 100644 index 00000000..5892fe04 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/Dockerfile @@ -0,0 +1,56 @@ +# Build everything at least. This is a work in progress. +# +# Useful for testing things before a release. +# +# Will also be used for running the camlistore.org website and public +# read-only blobserver. + +FROM ubuntu:12.04 + +MAINTAINER camlistore + +ENV DEBIAN_FRONTEND noninteractive +RUN apt-get update && apt-get upgrade -y +RUN apt-get install -y curl make git + +RUN curl -o /tmp/go.tar.gz https://storage.googleapis.com/golang/go1.3.1.linux-amd64.tar.gz +RUN tar -C /usr/local -zxvf /tmp/go.tar.gz +RUN rm /tmp/go.tar.gz +RUN /usr/local/go/bin/go version + +ENV GOROOT /usr/local/go +ENV PATH $GOROOT/bin:/gopath/bin:$PATH + +RUN mkdir -p /gopath/src +ADD pkg /gopath/src/camlistore.org/pkg +ADD cmd /gopath/src/camlistore.org/cmd +ADD website /gopath/src/camlistore.org/website +ADD third_party /gopath/src/camlistore.org/third_party +ADD server /gopath/src/camlistore.org/server +ADD dev /gopath/src/camlistore.org/dev +ADD depcheck /gopath/src/camlistore.org/depcheck + +RUN adduser --disabled-password --quiet --gecos Camli camli +RUN mkdir -p /gopath/bin +RUN chown camli.camli /gopath/bin +RUN mkdir -p /gopath/pkg +RUN chown camli.camli /gopath/pkg +USER camli + +ENV GOPATH /gopath + +RUN go install --tags=purego \ + camlistore.org/server/camlistored \ + camlistore.org/cmd/camput \ + camlistore.org/cmd/camget \ + camlistore.org/cmd/camtool \ + camlistore.org/website \ + camlistore.org/dev/devcam + +ENV USER camli +ENV HOME /home/camli +WORKDIR /home/camli + +EXPOSE 80 443 3179 8080 + +CMD /bin/bash diff --git a/vendor/github.com/camlistore/camlistore/HACKING b/vendor/github.com/camlistore/camlistore/HACKING new file mode 100644 index 00000000..ff998e9e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/HACKING @@ -0,0 +1,110 @@ +Camlistore contributors regularly use Linux and OS X, and both are +100% supported. + +Developing on Windows is sometimes broken, but should work. Let us +know if we broke something, or we accidentally depend on some +Unix-specific build tool somewhere. + +See http://camlistore.org/docs/contributing for information on how to +contribute to the project and submit patches. Notably, we use Gerrit +for code review. Our Gerrit instance is at https://camlistore.org/r/ + +See architecture docs: https://camlistore.org/docs/ + +You can view docs for Camlistore packages with local godoc, or +godoc.org. + +It's recommended you use git to fetch the source code, rather than +hack from a Camlistore release's zip file: + +$ git clone https://camlistore.googlesource.com/camlistore + +(We use github for distribution but its code review system is so poor, +we don't use its Pull Request mechanism. The Gerrit git server & code +review system is the main repo. See +http://camlistore.org/docs/contributing for how to use them. We might +support github for pull requests in the future, once it's properly +integrated with external code review tools. We had a meeting with Github +to discuss the ways in which their code review tools are poor.) + +On Debian/Ubuntu, some deps to get started: + +$ sudo apt-get install libsqlite3-dev sqlite3 pkg-config git + +During development, rather than use the main binaries ("camput", +"camget", "camtool", "cammount", etc) directly, we instead use a +wrapper (devcam) that automatically configure the environment to use +the test server & test environment. + +To build devcam: + +$ go run make.go + +And devcam will be in /bin/devcam. You'll probably want to +symlink it into your $PATH. + +Alternatively, if your Camlistore root is checked out at +$GOPATH/src/camlistore.org (optional, but natural for Go users), you +can just: + +$ export GO15VENDOREXPERIMENT=1 # required for all Camlistore builds +$ go install ./dev/devcam + +The subcommands of devcam start the server or run camput/camget/etc: + +$ devcam server # main server +$ devcam appengine # App Engine version of the server +$ devcam put # camput +$ devcam get # camget +$ devcam tool # camtool +$ devcam mount # cammount + +Once the dev server is running, + + - Upload a file: + devcam put file ~/camlistore/COPYING + - Create a permanode: + devcam put permanode + - Use the UI: http://localhost:3179/ui/ + +Before submitting a patch, you should check that all the tests pass with: + +$ devcam test + +You can use your usual git workflow to commit your changes, but for each +change to be reviewed you should merge your commits into one before submitting +to gerrit for review. + +You should also try to write a meaningful commit message, which at least states +in the first sentence what part or package of camlistore this commit is affecting. +The following text should state what problem the change is addressing, and how. +Finally, you should refer to the github issue(s) the commit is addressing, if any, +and with the appropriate keyword if the commit is fixing the issue. (See +https://help.github.com/articles/closing-issues-via-commit-messages/). + +For example: + +" +pkg/search: add "file" predicate to search by file name + +File names were already indexed but there was no way to query the index for a file +by its name. The "file" predicate can now be used in search expressions (e.g. in the +search box of the web user interface) to achieve that. + +Fixes #10987 +" + +If your commit is adding or updating a vendored third party, you must indicate +in your commit message the version (e.g. git commit hash) of said third party. + +You can optionally use our pre-commit hook so that your code gets gofmt'ed +before being submitted (which should be done anyway). + +$ cd .git/hooks +$ ln -s ../../misc/pre-commit.githook pre-commit + +Finally, submit your code to gerrit with: + +$ devcam review + +Please update this file as appropriate. diff --git a/vendor/github.com/camlistore/camlistore/Makefile b/vendor/github.com/camlistore/camlistore/Makefile new file mode 100644 index 00000000..20106875 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/Makefile @@ -0,0 +1,36 @@ +# The normal way to build Camlistore is just "go run make.go", which +# works everywhere, even on systems without Make. The rest of this +# Makefile is mostly historical and should hopefully disappear over +# time. +all: + go run make.go + +# On OS X with "brew install sqlite3", you need PKG_CONFIG_PATH=/usr/local/Cellar/sqlite/3.7.17/lib/pkgconfig/ +full: + go install --ldflags="-X camlistore.org/pkg/buildinfo.GitInfo "`./misc/gitversion` `pkg-config --libs sqlite3 1>/dev/null 2>/dev/null && echo "--tags=with_sqlite"` ./pkg/... ./server/... ./cmd/... ./third_party/... ./dev/... + + +# Workaround Go bug where the $GOPATH/pkg cache doesn't know about tag changes. +# Useful when you accidentally run "make" and then "make presubmit" doesn't work. +# See https://code.google.com/p/go/issues/detail?id=4443 +forcefull: + go install -a --tags=with_sqlite ./pkg/... ./server/camlistored ./cmd/... ./dev/... + +oldpresubmit: fmt + SKIP_DEP_TESTS=1 go test `pkg-config --libs sqlite3 1>/dev/null 2>/dev/null && echo "--tags=with_sqlite"` -short ./pkg/... ./server/camlistored/... ./server/appengine ./cmd/... ./dev/... && echo PASS + +presubmit: fmt + go run dev/devcam/*.go test -short + +embeds: + go install ./pkg/fileembed/genfileembed/ && genfileembed ./server/camlistored/ui && genfileembed ./pkg/server + +UIDIR = server/camlistored/ui + +NEWUIDIR = server/camlistored/newui + +clean: + rm -f $(NEWUIDIR)/all.js $(NEWUIDIR)/all.js.map + +fmt: + go fmt camlistore.org/cmd... camlistore.org/dev... camlistore.org/misc... camlistore.org/pkg... camlistore.org/server... diff --git a/vendor/github.com/camlistore/camlistore/README b/vendor/github.com/camlistore/camlistore/README new file mode 100644 index 00000000..a4b4d47f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/README @@ -0,0 +1,23 @@ +Camlistore is your personal storage system for life. + +It's a way to store, sync, share, model and back up content. + +It stands for "Content-Addressable Multi-Layer Indexed Storage", for +lack of a better name. For more, see: + + http://camlistore.org/ + http://camlistore.org/docs/ + +Other useful files: + + BUILDING how to compile it ("go run make.go") + HACKING how to do development and contribute + +Mailing lists: + + http://camlistore.org/lists + +Bugs and contributing: + + https://github.com/camlistore/camlistore/issues + http://camlistore.org/docs/contributing diff --git a/vendor/github.com/camlistore/camlistore/TESTS b/vendor/github.com/camlistore/camlistore/TESTS new file mode 100644 index 00000000..053a437b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/TESTS @@ -0,0 +1,34 @@ +Tests needed + +-- integration test of reindexing + race detector + +-- support for running race detector on all tests. when in race mode, also run + integration test children in race mode. + +-- test that server/camlistored still builds & starts even when sqlite isn't + available (TODO: hide it from the test by running make.go in a child + process with a faked-out PKG_CONFIG environment or something, to make + cmd/go unable to find it even if it's installed) + +-- search & corpus use of EnumeratePermanodesLastModified + +-- pkg/client --- test FetchVia against a server returning compressed content. + (fix in 3fa6d69405f036308931dd36e5070b2b19dbeadf without a new test) + +-cmd/camput/ + -verify that stat caching works. verify that -filenodes does create the permanode even if the file was already uploaded (and cached) in a previous run. + +-- blobserver/{remote,shard} have no tests. should be easier now that + test.Fetcher is a full blobserver? see encrypt, replica, and cond's + nascent tests for examples. + +-- app engine integration tests (before we make a release, for sure, + but probably in presubmit) + +-- cross-compiling to freebsd and windows etc still works. + +-- pkg/auth -- not enough tests. see regression at + https://camlistore-review.googlesource.com/#/c/556/1 + +-- blobserver.WaitForBlob, and integration tests for the http handlers + for long-polling on Enumerate and Stat diff --git a/vendor/github.com/camlistore/camlistore/TODO b/vendor/github.com/camlistore/camlistore/TODO new file mode 100644 index 00000000..3eeebf10 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/TODO @@ -0,0 +1,253 @@ +There are two TODO lists. This file (good for airplanes) and the online bug tracker: + + https://github.com/camlistore/camlistore/issues + +Offline list: + +-- fix the presubmit's gofmt to be happy about emacs: + + go fmt camlistore.org/cmd... camlistore.org/dev... camlistore.org/misc... camlistore.org/pkg... camlistore.org/server... + stat pkg/blobserver/.#multistream_test.go: no such file or directory + exit status 2 + make: *** [fmt] Error 1 + + +-- add HTTP handler for blobstreamer. stream a tar file? where to put + continuation token? special file after each tar entry? special file + at the end? HTTP Trailers? (but nobody supports them) + +-- reindexing: + * add streaming interface to localdisk? maybe, even though not ideal, but + really: migrate my personal instance from localdisk to blobpacked + + maybe diskpacked for loose blobs? start by migrating to blobpacked and + measuring size of loose. + * add blobserver.EnumerateAllUnsorted (which could use StreamBlobs + if available, else use EnumerateAll, else maybe even use a new + interface method that goes forever and can't resume at a point, + but can be canceled, and localdisk could implement that at least) + * add buffered sorted.KeyValue implementation: a memory one (of + configurable max size) in front of a real disk one. add a Flush method + to it. also Flush when memory gets big enough. + In progress: pkg/sorted/buffer + +-- stop using the "cond" blob router storage type in genconfig, as + well as the /bs-and-index/ "replica" storage type, and just let the + index register its own AddReceiveHook like the sync handler + (pkg/server/sync.go). But whereas the sync handler only synchronously + _enqueues_ the blob to replicate, the indexer should synchronously + do the ReceiveBlob (ooo-reindex) on it too before returning. + But the sync handler, despite technically only synchronously-enqueueing + and being therefore async, is still very fast. It's likely the + sync handler will therefore send a ReceiveBlob to the indexer + at the ~same time the indexer is already indexing it. So the indexer + should have some dup/merge suppression, and not do double work. + singleflight should work. The loser should still consume the + source io.Reader body and reply with the same error value. + +-- ditch the importer.Interrupt type and pass along a context.Context + instead, which has its Done channel for cancelation. + +-- be able to put a search (expr or JSON) into camlistore as a blob, + and search on it. and then name it with a permanode, and then + use a expr search like "named:someset" which looks up someset's + current camliContent, fetches it, and then expands into that blob's + search.expr or search.Constraint. + +-- S3-only mode doesn't work with a local disk index (kvfile) because + there's no directory for us to put the kv in. + +-- fault injection many more places with pkg/fault. maybe even in all + handlers automatically somehow? + +-- sync handler's shard validation doesn't retry on error. + only reports the errors now. + +-- export blobserver.checkHashReader and document it with + the blob.Fetcher docs. + +-- "filestogether" handler, putting related blobs (e.g. files) + next to each other in bigger blobs / separate files, and recording + offsets of small blobs into bigger ones + +-- diskpacked doesn't seem to sync its index quickly enough. + A new blob receieved + process exit + read in a new process + doesn't find that blob. kv bug? Seems to need an explicit Close. + This feels broken. Add tests & debug. + +-- websocket upload protocol. different write & read on same socket, + as opposed to HTTP, to have multiple chunks in flight. + +-- extension to blobserver upload protocol to minimize fsyncs: maybe a + client can say "no rush" on a bunch of data blobs first (which + still don't get acked back over websocket until they've been + fsynced), and then when the client uploads the schema/vivivy blob, + that websocket message won't have the "no rush" flag, calling the + optional blobserver.Storage method to fsync (in the case of + diskpacked/localdisk) and getting all the "uploaded" messages back + for the data chunks that were written-but-not-synced. + +-- measure FUSE operations, latency, round-trips, performance. + see next item: + +-- ... we probaby need a "describe all chunks in file" HTTP handler. + then FUSE (when it sees sequential access) can say "what's the + list of all chunks in this file?" and then fetch them all at once. + see next item: + +-- ... HTTP handler to get multiple blobs at once. multi-download + in multipart/mime body. we have this for stat and upload, but + not download. + +-- ... if we do blob fetching over websocket too, then we can support + cancellation of blob requests. Then we can combine the previous + two items: FUSE client can ask the server, over websockets, for a + list of all chunks, and to also start streaming them all. assume a + high-latency (but acceptable bandwidth) link. the chunks are + already in flight, but some might be redundant. once the client figures + out some might be redundant, it can issue "stop send" messages over + that websocket connection to prevent dups. this should work on + both "files" and "bytes" types. + +-- cacher: configurable policy on max cache size. clean oldest + things (consider mtime+atime) to get back under max cache size. + maybe prefer keeping small things (metadata blobs) too, + and only delete large data chunks. + +-- UI: video, at least thumbnailing (use external program, + like VLC or whatever nautilus uses?) + +-- rename server.ImageHandler to ThumbnailRequest or something? It's + not really a Handler in the normal sense. It's not built once and + called repeatedly; it's built for every ServeHTTP request. + +-- unexport more stuff from pkg/server. Cache, etc. + +-- look into garbage from openpgp signing + +-- make leveldb memdb's iterator struct only 8 bytes, pointing to a recycled + object, and just nil out that pointer at EOF. + +-- bring in the google glog package to third_party and use it in + places that want selective logging (e.g. pkg/index/receive.go) + +-- (Mostly done) verify all ReceiveBlob calls and see which should be + blobserver.Receive instead, or ReceiveNoHash. git grep -E + "\.ReceiveBlob\(" And maybe ReceiveNoHash should go away and be + replaced with a "ReceiveString" method which combines the + blobref-from-string and ReceiveNoHash at once. + +-- union storage target. sharder can be thought of a specialization + of union. sharder already unions, but has a hard-coded policy + of where to put new blobs. union could a library (used by sharder) + with a pluggable policy on that. + +-- support for running cammount under camlistored. especially for OS X, + where the lifetime of the background daemon will be the same as the + user's login session. + +-- website: remove the "Installation" heading for /cmd/*, since + they're misleading and people should use "go run make.go" in the + general case. + +-- website: add godoc for /server/camlistored (also without a "go get" + line) + +-- tests for all cmd/* stuff, perhaps as part of some integration + tests. + +-- move most of camput into a library, not a package main. + +-- server cron support: full syncs, camput file backups, integrity + checks. + +-- status in top right of UI: sync, crons. (in-progress, un-acked + problems) + +-- finish metadata compaction on the encryption blobserver.Storage wrapper. + +-- get security review on encryption wrapper. (agl?) + +-- peer-to-peer server and blobserver target to store encrypted blobs + on stranger's hardrives. server will be open source so groups of + friends/family can run their own for small circles, or some company + could run a huge instance. spray encrypted backup chunks across + friends' machines, and have central server(s) present challenges to + the replicas to have them verify what they have and how big, and + also occasionally say what the SHA-1("challenge" + blob-data) is. + +-- sharing: make camget work with permanode sets too, not just + "directory" and "file" things. + +-- sharing: when hitting e.g. http://myserver/share/sha1-xxxxx, if + a web browser and not a smart client (Accept header? User-Agent?) + then redirect or render a cutesy gallery or file browser instead, + still with machine-readable data for slurping. + +-- rethink the directory schema so it can a) represent directories + with millions of files (without making a >1MB or >16MB schema blob), + probably forming a tree, similar to files. but rather than rolling checksum, + just split lexically when nodes get too big. + +-- delete mostly-obsolete camsigd. see big TODO in camsigd.go. + +-- we used to be able live-edit js/css files in server/camlistored/ui when + running under the App Engine dev_appserver.py. That's now broken with my + latest efforts to revive it. The place to start looking is: + server/camlistored/ui/fileembed_appengine.go + +-- should a "share" claim be not a claim but its own permanode, so it + can be rescinded? right now you can't really unshare a "haveref" + claim. or rather, TODO: verify we support "delete" claims to + delete any claim, and verify the share system and indexer all + support it. I think the indexer might, but not the share system. + Also TODO: "camput delete" or "rescind" subcommand. + Also TODO: document share claims in doc/schema/ and on website. + +-- make the -transitive flag for "camput share -transitive" be a tri-state: + unset, true, false, and unset should then mean default to true for "file" + and "directory" schema blobs, and "false" for other things. + +-- index: static directory recursive sizes: search: ask to see biggest directories? + +-- index: index dates in filenames ("yyyy-mm-dd-Foo-Trip", "yyyy-mm blah", etc). + +-- get webdav server working again, for mounting on Windows. This worked before Go 1 + but bitrot when we moved pkg/fs to use the rsc/fuse. + +-- work on runsit more, so I can start using this more often. runsit should + be able to reload itself, and also watch for binaries changing and restart + when binaries change. (or symlinks to binaries) + +-- BUG: osutil paths.go on OS X: should use Library everywhere instead of mix of + Library and ~/.camlistore? + +OLD: + +-- add CROS support? Access-Control-Allow-Origin: * + w/ OPTIONS + http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ + +-- brackup integration, perhaps sans GPG? (requires Perl client?) + +-- blobserver: clean up channel-closing consistency in blobserver interface + (most close, one doesn't. all should probably close) + +Android: + +[ ] Fix wake locks in UploadThread. need to hold CPU + WiFi whenever + something's enqueued at all and we're running. Move out of the Thread + that's uploading itself. +[ ] GPG signing of blobs (brad) + http://code.google.com/p/android-privacy-guard/ + http://www.thialfihar.org/projects/apg/ + (supports signing in code, but not an Intent?) + http://code.google.com/p/android-privacy-guard/wiki/UsingApgForDevelopment + ... mailed the author. + +Client libraries: + +[X] Go +[X] JavaScript +[/] Python (Brett); but see https://github.com/tsileo/camlipy +[ ] Perl +[ ] Ruby +[ ] PHP diff --git a/vendor/github.com/camlistore/camlistore/app/hello/main.go b/vendor/github.com/camlistore/camlistore/app/hello/main.go new file mode 100644 index 00000000..f7ede0ea --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/app/hello/main.go @@ -0,0 +1,97 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// The hello application serves as an example on how to make stand-alone +// server applications, interacting with a Camlistore server. +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + "runtime" + + "camlistore.org/pkg/app" + "camlistore.org/pkg/buildinfo" + "camlistore.org/pkg/webserver" +) + +var ( + flagVersion = flag.Bool("version", false, "show version") +) + +// config is used to unmarshal the application configuration JSON +// that we get from Camlistore when we request it at $CAMLI_APP_CONFIG_URL. +type config struct { + Word string `json:"word,omitempty"` // Argument printed after "Hello " in the helloHandler response. +} + +func appConfig() *config { + configURL := os.Getenv("CAMLI_APP_CONFIG_URL") + if configURL == "" { + log.Fatalf("Hello application needs a CAMLI_APP_CONFIG_URL env var") + } + cl, err := app.Client() + if err != nil { + log.Fatalf("could not get a client to fetch extra config: %v", err) + } + conf := &config{} + if err := cl.GetJSON(configURL, conf); err != nil { + log.Fatalf("could not get app config at %v: %v", configURL, err) + } + return conf +} + +type helloHandler struct { + who string // who to say hello to. +} + +func (h *helloHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("Content-Type", "text/plain; charset=utf-8") + rw.WriteHeader(200) + fmt.Fprintf(rw, "Hello %s\n", h.who) +} + +func main() { + flag.Parse() + + if *flagVersion { + fmt.Fprintf(os.Stderr, "hello version: %s\nGo version: %s (%s/%s)\n", + buildinfo.Version(), runtime.Version(), runtime.GOOS, runtime.GOARCH) + return + } + + log.Printf("Starting hello version %s; Go %s (%s/%s)", buildinfo.Version(), runtime.Version(), + runtime.GOOS, runtime.GOARCH) + + listenAddr, err := app.ListenAddress() + if err != nil { + log.Fatalf("Listen address: %v", err) + } + conf := appConfig() + ws := webserver.New() + ws.Handle("/", &helloHandler{who: conf.Word}) + // TODO(mpl): handle status requests too. Camlistore will send an auth + // token in the extra config that should be used as the "password" for + // subsequent status requests. + if err := ws.Listen(listenAddr); err != nil { + log.Fatalf("Listen: %v", err) + } + + ws.Serve() +} diff --git a/vendor/github.com/camlistore/camlistore/app/publisher/fileembed.go b/vendor/github.com/camlistore/camlistore/app/publisher/fileembed.go new file mode 100644 index 00000000..3c8694a9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/app/publisher/fileembed.go @@ -0,0 +1,32 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +The publisher application serves and renders items published by Camlistore. +That is, items that are children, through a (direct or not) camliPath relation, +of a camliRoot node (a permanode with a camliRoot attribute set). + +#fileembed pattern .+\.(js|css|html|png|svg)$ +*/ +package main + +import ( + "camlistore.org/pkg/fileembed" +) + +// TODO(mpl): appengine case + +var Files = &fileembed.Files{} diff --git a/vendor/github.com/camlistore/camlistore/app/publisher/gallery.html b/vendor/github.com/camlistore/camlistore/app/publisher/gallery.html new file mode 100644 index 00000000..90a54baa --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/app/publisher/gallery.html @@ -0,0 +1,53 @@ + + +{{if $header := call .Header}} + + {{$header.Title}} + {{range $css := $header.CSSFiles}} + + {{end}} + + + +

{{$header.Title}}

+ {{if $file := call .File}} +
File: {{$file.FileName}}, {{$file.Size}} bytes, type {{$file.MIMEType}}
+ {{if $file.IsImage}} + + {{end}} + + {{if $nav := call $file.Nav}} +
+ {{if $prev := $nav.PrevPath}}[prev] {{end}} + {{if $up := $nav.ParentPath}}[up] {{end}} + {{if $next := $nav.NextPath}}[next] {{end}} +
+ {{end}} + {{else}} + {{if $membersData := call .Members}} + + + + {{end}} + {{end}} +{{end}} + + diff --git a/vendor/github.com/camlistore/camlistore/app/publisher/main.go b/vendor/github.com/camlistore/camlistore/app/publisher/main.go new file mode 100644 index 00000000..cbbb4a82 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/app/publisher/main.go @@ -0,0 +1,1008 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "html" + "html/template" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "sync" + + "camlistore.org/pkg/app" + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/blobserver/localdisk" + "camlistore.org/pkg/buildinfo" + "camlistore.org/pkg/constants" + "camlistore.org/pkg/fileembed" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/publish" + "camlistore.org/pkg/search" + "camlistore.org/pkg/server" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/syncutil" + "camlistore.org/pkg/types/camtypes" + "camlistore.org/pkg/webserver" + + _ "camlistore.org/pkg/sorted/kvfile" +) + +var ( + flagVersion = flag.Bool("version", false, "show version") +) + +var ( + logger = log.New(os.Stderr, "PUBLISHER: ", log.LstdFlags) + logf = logger.Printf +) + +// config is used to unmarshal the application configuration JSON +// that we get from Camlistore when we request it at $CAMLI_APP_CONFIG_URL. +type config struct { + HTTPSCert string `json:"httpsCert,omitempty"` // Path to the HTTPS certificate file. + HTTPSKey string `json:"httpsKey,omitempty"` // Path to the HTTPS key file. + RootName string `json:"camliRoot"` // Publish root name (i.e. value of the camliRoot attribute on the root permanode). + MaxResizeBytes int64 `json:"maxResizeBytes,omitempty"` // See constants.DefaultMaxResizeMem + SourceRoot string `json:"sourceRoot,omitempty"` // Path to the app's resources dir, such as html and css files. + GoTemplate string `json:"goTemplate"` // Go html template to render the publication. + CacheRoot string `json:"cacheRoot,omitempty"` // Root path for the caching blobserver. No caching if empty. +} + +func appConfig() *config { + configURL := os.Getenv("CAMLI_APP_CONFIG_URL") + if configURL == "" { + logger.Fatalf("Publisher application needs a CAMLI_APP_CONFIG_URL env var") + } + cl, err := app.Client() + if err != nil { + logger.Fatalf("could not get a client to fetch extra config: %v", err) + } + conf := &config{} + if err := cl.GetJSON(configURL, conf); err != nil { + logger.Fatalf("could not get app config at %v: %v", configURL, err) + } + return conf +} + +func main() { + flag.Parse() + + if *flagVersion { + fmt.Fprintf(os.Stderr, "publisher version: %s\nGo version: %s (%s/%s)\n", + buildinfo.Version(), runtime.Version(), runtime.GOOS, runtime.GOARCH) + return + } + + logf("Starting publisher version %s; Go %s (%s/%s)", buildinfo.Version(), runtime.Version(), + runtime.GOOS, runtime.GOARCH) + + listenAddr, err := app.ListenAddress() + if err != nil { + logger.Fatalf("Listen address: %v", err) + } + conf := appConfig() + ph := newPublishHandler(conf) + if err := ph.initRootNode(); err != nil { + logf("%v", err) + } + ws := webserver.New() + ws.Logger = logger + ws.Handle("/", ph) + if conf.HTTPSCert != "" && conf.HTTPSKey != "" { + ws.SetTLS(conf.HTTPSCert, conf.HTTPSKey) + } + if err := ws.Listen(listenAddr); err != nil { + logger.Fatalf("Listen: %v", err) + } + ws.Serve() +} + +func newPublishHandler(conf *config) *publishHandler { + cl, err := app.Client() + if err != nil { + logger.Fatalf("could not get a client for the publish handler %v", err) + } + if conf.RootName == "" { + logger.Fatal("camliRoot not found in the app configuration") + } + maxResizeBytes := conf.MaxResizeBytes + if maxResizeBytes == 0 { + maxResizeBytes = constants.DefaultMaxResizeMem + } + var CSSFiles []string + if conf.SourceRoot != "" { + appRoot := filepath.Join(conf.SourceRoot, "app", "publisher") + Files = &fileembed.Files{ + DirFallback: appRoot, + } + // TODO(mpl): Can I readdir by listing with "/" on Files, even with DirFallBack? + // Apparently not, but retry later. + dir, err := os.Open(appRoot) + if err != nil { + logger.Fatal(err) + } + defer dir.Close() + names, err := dir.Readdirnames(-1) + if err != nil { + logger.Fatal(err) + } + for _, v := range names { + if strings.HasSuffix(v, ".css") { + CSSFiles = append(CSSFiles, v) + } + } + } else { + Files.Listable = true + dir, err := Files.Open("/") + if err != nil { + logger.Fatal(err) + } + defer dir.Close() + fis, err := dir.Readdir(-1) + if err != nil { + logger.Fatal(err) + } + for _, v := range fis { + name := v.Name() + if strings.HasSuffix(name, ".css") { + CSSFiles = append(CSSFiles, name) + } + } + } + // TODO(mpl): add all htmls found in Files to the template if none specified? + if conf.GoTemplate == "" { + logger.Fatal("a go template is required in the app configuration") + } + goTemplate, err := goTemplate(Files, conf.GoTemplate) + if err != nil { + logger.Fatal(err) + } + serverURL := os.Getenv("CAMLI_API_HOST") + if serverURL == "" { + logger.Fatal("CAMLI_API_HOST var not set") + } + var cache blobserver.Storage + var thumbMeta *server.ThumbMeta + if conf.CacheRoot != "" { + cache, err = localdisk.New(conf.CacheRoot) + if err != nil { + logger.Fatalf("Could not create localdisk cache: %v", err) + } + thumbsCacheDir := filepath.Join(os.TempDir(), "camli-publisher-cache") + if err := os.MkdirAll(thumbsCacheDir, 0700); err != nil { + logger.Fatalf("Could not create cache dir %s for %v publisher: %v", thumbsCacheDir, conf.RootName, err) + } + kv, err := sorted.NewKeyValue(map[string]interface{}{ + "type": "kv", + "file": filepath.Join(thumbsCacheDir, conf.RootName+"-thumbnails.kv"), + }) + if err != nil { + logger.Fatalf("Could not create kv for %v's thumbs cache: %v", conf.RootName, err) + } + thumbMeta = server.NewThumbMeta(kv) + } + + return &publishHandler{ + rootName: conf.RootName, + cl: cl, + resizeSem: syncutil.NewSem(maxResizeBytes), + staticFiles: Files, + goTemplate: goTemplate, + CSSFiles: CSSFiles, + describedCache: make(map[string]*search.DescribedBlob), + cache: cache, + thumbMeta: thumbMeta, + } +} + +func goTemplate(files *fileembed.Files, templateFile string) (*template.Template, error) { + f, err := files.Open(templateFile) + if err != nil { + return nil, fmt.Errorf("Could not open template %v: %v", templateFile, err) + } + defer f.Close() + templateBytes, err := ioutil.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("Could not read template %v: %v", templateFile, err) + } + return template.Must(template.New("subject").Parse(string(templateBytes))), nil +} + +// We're using this interface in a publishHandler, instead of directly +// a *client.Client, so we can use a fake client in tests. +type client interface { + search.QueryDescriber + GetJSON(url string, data interface{}) error + Post(url string, bodyType string, body io.Reader) error + blob.Fetcher +} + +type publishHandler struct { + rootName string // Publish root name (i.e. value of the camliRoot attribute on the root permanode). + + rootNodeMu sync.Mutex + rootNode blob.Ref // Root permanode, origin of all camliPaths for this publish handler. + + cl client // Used for searching, and remote storage. + + staticFiles *fileembed.Files // For static resources. + goTemplate *template.Template // For publishing/rendering. + CSSFiles []string + resizeSem *syncutil.Sem // Limit peak RAM used by concurrent image thumbnail calls. + + describedCacheMu sync.RWMutex + describedCache map[string]*search.DescribedBlob // So that each item in a gallery does not actually require a describe round-trip. + + cache blobserver.Storage // For caching images and files, or nil. + thumbMeta *server.ThumbMeta // For keeping track of cached images, or nil. +} + +func (ph *publishHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ph.rootNodeMu.Lock() + if !ph.rootNode.Valid() { + // we want to retry doing this every time because the rootNode could have been created + // (by e.g. the owner) since last time. + err := ph.initRootNode() + if err != nil { + httputil.ServeError(w, r, fmt.Errorf("No publish root node: %v", err)) + ph.rootNodeMu.Unlock() + return + } + } + ph.rootNodeMu.Unlock() + + preq, err := ph.NewRequest(w, r) + if err != nil { + httputil.ServeError(w, r, fmt.Errorf("Could not create publish request: %v", err)) + return + } + preq.serveHTTP() +} + +func (ph *publishHandler) initRootNode() error { + var getRootNode = func() (blob.Ref, error) { + result, err := ph.camliRootQuery() + if err != nil { + return blob.Ref{}, fmt.Errorf("could not find permanode for root %q of publish handler: %v", ph.rootName, err) + } + if len(result.Blobs) == 0 || !result.Blobs[0].Blob.Valid() { + return blob.Ref{}, fmt.Errorf("could not find permanode for root %q of publish handler: %v", ph.rootName, os.ErrNotExist) + } + return result.Blobs[0].Blob, nil + } + node, err := getRootNode() + if err != nil { + return err + } + ph.rootNode = node + return nil +} + +func (ph *publishHandler) camliRootQuery() (*search.SearchResult, error) { + // TODO(mpl): I've voluntarily omitted the owner because it's not clear to + // me that we actually care about that. Same for signer in lookupPathTarget. + return ph.cl.Query(&search.SearchQuery{ + Limit: 1, + Constraint: &search.Constraint{ + Permanode: &search.PermanodeConstraint{ + Attr: "camliRoot", + Value: ph.rootName, + }, + }, + }) +} + +func (ph *publishHandler) lookupPathTarget(root blob.Ref, suffix string) (blob.Ref, error) { + if suffix == "" { + return root, nil + } + // TODO: verify it's optimized: http://camlistore.org/issue/405 + result, err := ph.cl.Query(&search.SearchQuery{ + Limit: 1, + Constraint: &search.Constraint{ + Permanode: &search.PermanodeConstraint{ + SkipHidden: true, + Relation: &search.RelationConstraint{ + Relation: "parent", + EdgeType: "camliPath:" + suffix, + Any: &search.Constraint{ + BlobRefPrefix: root.String(), + }, + }, + }, + }, + }) + if err != nil { + return blob.Ref{}, err + } + if len(result.Blobs) == 0 || !result.Blobs[0].Blob.Valid() { + return blob.Ref{}, os.ErrNotExist + } + return result.Blobs[0].Blob, nil +} + +// Given a blobref and a few hex characters of the digest of the next hop, return the complete +// blobref of the prefix, if that's a valid next hop. +func (ph *publishHandler) resolvePrefixHop(parent blob.Ref, prefix string) (child blob.Ref, err error) { + // TODO: this is a linear scan right now. this should be + // optimized to use a new database table of members so this is + // a quick lookup. in the meantime it should be in memcached + // at least. + if len(prefix) < 8 { + return blob.Ref{}, fmt.Errorf("Member prefix %q too small", prefix) + } + des, err := ph.describe(parent) + if err != nil { + return blob.Ref{}, fmt.Errorf("Failed to describe member %q in parent %q", prefix, parent) + } + if des.Permanode != nil { + cr, ok := des.ContentRef() + if ok && strings.HasPrefix(cr.Digest(), prefix) { + return cr, nil + } + for _, member := range des.Members() { + if strings.HasPrefix(member.BlobRef.Digest(), prefix) { + return member.BlobRef, nil + } + } + crdes, err := ph.describe(cr) + if err != nil { + return blob.Ref{}, fmt.Errorf("Failed to describe content %q of parent %q", cr, parent) + } + if crdes.Dir != nil { + return ph.resolvePrefixHop(cr, prefix) + } + } else if des.Dir != nil { + for _, child := range des.DirChildren { + if strings.HasPrefix(child.Digest(), prefix) { + return child, nil + } + } + } + return blob.Ref{}, fmt.Errorf("Member prefix %q not found in %q", prefix, parent) +} + +func (ph *publishHandler) describe(br blob.Ref) (*search.DescribedBlob, error) { + ph.describedCacheMu.RLock() + if des, ok := ph.describedCache[br.String()]; ok { + ph.describedCacheMu.RUnlock() + return des, nil + } + ph.describedCacheMu.RUnlock() + res, err := ph.cl.Describe(&search.DescribeRequest{ + BlobRef: br, + Depth: 1, + }) + if err != nil { + return nil, fmt.Errorf("Could not describe %v: %v", br, err) + } + return res.Meta[br.String()], nil +} + +func (ph *publishHandler) deepDescribe(br blob.Ref) (*search.DescribeResponse, error) { + res, err := ph.cl.Query(&search.SearchQuery{ + Constraint: &search.Constraint{ + BlobRefPrefix: br.String(), + CamliType: "permanode", + }, + Describe: &search.DescribeRequest{ + Depth: 1, + Rules: []*search.DescribeRule{ + { + Attrs: []string{"camliContent", "camliContentImage", "camliMember", "camliPath:*"}, + }, + }, + }, + Limit: -1, + }) + if err != nil { + return nil, fmt.Errorf("Could not deep describe %v: %v", br, err) + } + if res == nil || res.Describe == nil { + return nil, fmt.Errorf("no describe result for %v", br) + } + return res.Describe, nil +} + +// publishRequest is the state around a single HTTP request to the +// publish handler +type publishRequest struct { + ph *publishHandler + rw http.ResponseWriter + req *http.Request + base, suffix, subres string + rootpn blob.Ref + subject blob.Ref + inSubjectChain map[string]bool // blobref -> true + subjectBasePath string +} + +func (ph *publishHandler) NewRequest(rw http.ResponseWriter, req *http.Request) (*publishRequest, error) { + // splits a path request into its suffix and subresource parts. + // e.g. /blog/foo/camli/res/file/xxx -> ("foo", "file/xxx") + suffix, res := httputil.PathSuffix(req), "" + if strings.HasPrefix(suffix, "-/") { + suffix, res = "", suffix[2:] + } else if s := strings.SplitN(suffix, "/-/", 2); len(s) == 2 { + suffix, res = s[0], s[1] + } + + return &publishRequest{ + ph: ph, + rw: rw, + req: req, + suffix: suffix, + base: httputil.PathBase(req), + subres: res, + rootpn: ph.rootNode, + inSubjectChain: make(map[string]bool), + subjectBasePath: "", + }, nil +} + +func (pr *publishRequest) serveHTTP() { + if !pr.rootpn.Valid() { + pr.rw.WriteHeader(404) + return + } + + if pr.Debug() { + pr.rw.Header().Set("Content-Type", "text/html") + pr.pf("I am publish handler at base %q, serving root %q (permanode=%s), suffix %q, subreq %q
", + pr.base, pr.ph.rootName, pr.rootpn, html.EscapeString(pr.suffix), html.EscapeString(pr.subres)) + } + + if err := pr.findSubject(); err != nil { + if err == os.ErrNotExist { + pr.rw.WriteHeader(404) + return + } + logf("Error looking up %s/%q: %v", pr.rootpn, pr.suffix, err) + pr.rw.WriteHeader(500) + return + } + + if pr.Debug() { + pr.pf("

Subject: %s

", pr.subject, pr.subject) + return + } + + switch pr.subresourceType() { + case "": + pr.serveSubjectTemplate() + case "b": + // TODO: download a raw blob + case "f": // file download + pr.serveSubresFileDownload() + case "i": // image, scaled + pr.serveSubresImage() + case "s": // static + pr.req.URL.Path = pr.subres[len("/=s"):] + if len(pr.req.URL.Path) <= 1 { + http.Error(pr.rw, "Illegal URL.", http.StatusNotFound) + return + } + file := pr.req.URL.Path[1:] + server.ServeStaticFile(pr.rw, pr.req, pr.ph.staticFiles, file) + case "z": + pr.serveZip() + default: + pr.rw.WriteHeader(400) + pr.pf("

Invalid or unsupported resource request.

") + } +} + +func (pr *publishRequest) Debug() bool { + return pr.req.FormValue("debug") == "1" +} + +var memberRE = regexp.MustCompile(`^/?h([0-9a-f]+)`) + +func (pr *publishRequest) findSubject() error { + if strings.HasPrefix(pr.suffix, "=s/") { + pr.subres = "/" + pr.suffix + return nil + } + + subject, err := pr.ph.lookupPathTarget(pr.rootpn, pr.suffix) + if err != nil { + return err + } + if strings.HasPrefix(pr.subres, "=z/") { + // this happens when we are at the root of the published path, + // e.g /base/suffix/-/=z/foo.zip + // so we need to reset subres as fullpath so that it is detected + // properly when switching on pr.subresourceType() + pr.subres = "/" + pr.subres + // since we return early, we set the subject because that is + // what is going to be used as a root node by the zip handler. + pr.subject = subject + return nil + } + + pr.inSubjectChain[subject.String()] = true + pr.subjectBasePath = pr.base + pr.suffix + + // Chase /h hops in suffix. + for { + m := memberRE.FindStringSubmatch(pr.subres) + if m == nil { + break + } + match, memberPrefix := m[0], m[1] + + if err != nil { + return fmt.Errorf("Error looking up potential member %q in describe of subject %q: %v", + memberPrefix, subject, err) + } + + subject, err = pr.ph.resolvePrefixHop(subject, memberPrefix) + if err != nil { + return err + } + pr.inSubjectChain[subject.String()] = true + pr.subres = pr.subres[len(match):] + pr.subjectBasePath = addPathComponent(pr.subjectBasePath, match) + } + + pr.subject = subject + return nil +} + +func (pr *publishRequest) subresourceType() string { + if len(pr.subres) >= 3 && strings.HasPrefix(pr.subres, "/=") { + return pr.subres[2:3] + } + return "" +} + +func (pr *publishRequest) pf(format string, args ...interface{}) { + fmt.Fprintf(pr.rw, format, args...) +} + +func addPathComponent(base, addition string) string { + if !strings.HasPrefix(addition, "/") { + addition = "/" + addition + } + if strings.Contains(base, "/-/") { + return base + addition + } + return base + "/-" + addition +} + +const ( + resSeparator = "/-" + digestPrefix = "h" + digestLen = 10 +) + +// var hopRE = regexp.MustCompile(fmt.Sprintf("^/%s([0-9a-f]{%d})", digestPrefix, digestLen)) + +func getFileInfo(item blob.Ref, peers map[string]*search.DescribedBlob) (path []blob.Ref, fi *camtypes.FileInfo, ok bool) { + described := peers[item.String()] + if described == nil || + described.Permanode == nil || + described.Permanode.Attr == nil { + return + } + contentRef := described.Permanode.Attr.Get("camliContent") + if contentRef == "" { + return + } + if cdes := peers[contentRef]; cdes != nil && cdes.File != nil { + return []blob.Ref{described.BlobRef, cdes.BlobRef}, cdes.File, true + } + return +} + +// serveSubjectTemplate creates the funcs to generate the PageHeader, PageFile, +// and pageMembers that can be used by the subject template, and serves the template. +func (pr *publishRequest) serveSubjectTemplate() { + res, err := pr.ph.deepDescribe(pr.subject) + if err != nil { + httputil.ServeError(pr.rw, pr.req, err) + return + } + pr.ph.cacheDescribed(res.Meta) + + subdes := res.Meta[pr.subject.String()] + if subdes.CamliType == "file" { + pr.serveFileDownload(subdes) + return + } + + headerFunc := func() *publish.PageHeader { + return pr.subjectHeader(res.Meta) + } + fileFunc := func() *publish.PageFile { + file, err := pr.subjectFile(res.Meta) + if err != nil { + logf("%v", err) + return nil + } + return file + } + membersFunc := func() *publish.PageMembers { + members, err := pr.subjectMembers(res.Meta) + if err != nil { + logf("%v", err) + return nil + } + return members + } + page := &publish.SubjectPage{ + Header: headerFunc, + File: fileFunc, + Members: membersFunc, + } + + err = pr.ph.goTemplate.Execute(pr.rw, page) + if err != nil { + logf("Error serving subject template: %v", err) + http.Error(pr.rw, "Error serving template", http.StatusInternalServerError) + return + } +} + +const cacheSize = 1000 + +func (ph *publishHandler) cacheDescribed(described map[string]*search.DescribedBlob) { + ph.describedCacheMu.Lock() + defer ph.describedCacheMu.Unlock() + if len(ph.describedCache) > cacheSize { + ph.describedCache = described + return + } + for k, v := range described { + ph.describedCache[k] = v + } +} + +func (pr *publishRequest) serveFileDownload(des *search.DescribedBlob) { + fileref, fileinfo, ok := pr.fileSchemaRefFromBlob(des) + if !ok { + logf("Didn't get file schema from described blob %q", des.BlobRef) + return + } + mime := "" + if fileinfo != nil && fileinfo.IsImage() { + mime = fileinfo.MIMEType + } + dh := &server.DownloadHandler{ + Fetcher: pr.ph.cl, + Cache: pr.ph.cache, + ForceMIME: mime, + } + dh.ServeHTTP(pr.rw, pr.req, fileref) +} + +// Given a described blob, optionally follows a camliContent and +// returns the file's schema blobref and its fileinfo (if found). +func (pr *publishRequest) fileSchemaRefFromBlob(des *search.DescribedBlob) (fileref blob.Ref, fileinfo *camtypes.FileInfo, ok bool) { + if des == nil { + http.NotFound(pr.rw, pr.req) + return + } + if des.Permanode != nil { + // TODO: get "forceMime" attr out of the permanode? or + // fileName content-disposition? + if cref := des.Permanode.Attr.Get("camliContent"); cref != "" { + cbr, ok2 := blob.Parse(cref) + if !ok2 { + http.Error(pr.rw, "bogus camliContent", 500) + return + } + des = des.PeerBlob(cbr) + if des == nil { + http.Error(pr.rw, "camliContent not a peer in describe", 500) + return + } + } + } + if des.CamliType == "file" { + return des.BlobRef, des.File, true + } + http.Error(pr.rw, "failed to find fileSchemaRefFromBlob", 404) + return +} + +// subjectHeader returns the PageHeader corresponding to the described subject. +func (pr *publishRequest) subjectHeader(described map[string]*search.DescribedBlob) *publish.PageHeader { + subdes := described[pr.subject.String()] + header := &publish.PageHeader{ + Title: html.EscapeString(getTitle(subdes.BlobRef, described)), + CSSFiles: pr.cssFiles(), + Meta: func() string { + jsonRes, _ := json.MarshalIndent(described, "", " ") + return string(jsonRes) + }(), + Subject: pr.subject.String(), + } + return header +} + +func (pr *publishRequest) cssFiles() []string { + files := []string{} + for _, filename := range pr.ph.CSSFiles { + files = append(files, pr.staticPath(filename)) + } + return files +} + +func (pr *publishRequest) staticPath(fileName string) string { + return pr.base + "=s/" + fileName +} + +func getTitle(item blob.Ref, peers map[string]*search.DescribedBlob) string { + described := peers[item.String()] + if described == nil { + return "" + } + if described.Permanode != nil { + if t := described.Permanode.Attr.Get("title"); t != "" { + return t + } + if contentRef := described.Permanode.Attr.Get("camliContent"); contentRef != "" { + if cdes := peers[contentRef]; cdes != nil { + return getTitle(cdes.BlobRef, peers) + } + } + } + if described.File != nil { + return described.File.FileName + } + if described.Dir != nil { + return described.Dir.FileName + } + return "" +} + +// subjectFile returns the relevant PageFile if the described subject is a file permanode. +func (pr *publishRequest) subjectFile(described map[string]*search.DescribedBlob) (*publish.PageFile, error) { + subdes := described[pr.subject.String()] + contentRef, ok := subdes.ContentRef() + if !ok { + return nil, nil + } + fileDes, err := pr.ph.describe(contentRef) + if err != nil { + return nil, err + } + if fileDes.File == nil { + // most likely a dir + return nil, nil + } + + path := []blob.Ref{pr.subject, contentRef} + downloadURL := pr.SubresFileURL(path, fileDes.File.FileName) + thumbnailURL := "" + if fileDes.File.IsImage() { + thumbnailURL = pr.SubresThumbnailURL(path, fileDes.File.FileName, 600) + } + fileName := html.EscapeString(fileDes.File.FileName) + return &publish.PageFile{ + FileName: fileName, + Size: fileDes.File.Size, + MIMEType: fileDes.File.MIMEType, + IsImage: fileDes.File.IsImage(), + DownloadURL: downloadURL, + ThumbnailURL: thumbnailURL, + DomID: contentRef.DomID(), + Nav: func() *publish.Nav { + return nil + }, + }, nil +} + +func (pr *publishRequest) SubresFileURL(path []blob.Ref, fileName string) string { + return pr.SubresThumbnailURL(path, fileName, -1) +} + +func (pr *publishRequest) SubresThumbnailURL(path []blob.Ref, fileName string, maxDimen int) string { + var buf bytes.Buffer + resType := "i" + if maxDimen == -1 { + resType = "f" + } + fmt.Fprintf(&buf, "%s", pr.subjectBasePath) + if !strings.Contains(pr.subjectBasePath, "/-/") { + buf.Write([]byte("/-")) + } + for _, br := range path { + if pr.inSubjectChain[br.String()] { + continue + } + fmt.Fprintf(&buf, "/h%s", br.DigestPrefix(10)) + } + fmt.Fprintf(&buf, "/=%s", resType) + fmt.Fprintf(&buf, "/%s", url.QueryEscape(fileName)) + if maxDimen != -1 { + fmt.Fprintf(&buf, "?mw=%d&mh=%d", maxDimen, maxDimen) + } + return buf.String() +} + +// subjectMembers returns the relevant PageMembers if the described subject is a permanode with members. +func (pr *publishRequest) subjectMembers(resMap map[string]*search.DescribedBlob) (*publish.PageMembers, error) { + subdes := resMap[pr.subject.String()] + res, err := pr.ph.describeMembers(pr.subject) + if err != nil { + return nil, err + } + members := []*search.DescribedBlob{} + for _, v := range res.Blobs { + members = append(members, res.Describe.Meta[v.Blob.String()]) + } + if len(members) == 0 { + return nil, nil + } + + zipName := "" + if title := getTitle(subdes.BlobRef, resMap); title == "" { + zipName = "download.zip" + } else { + zipName = title + ".zip" + } + subjectPath := pr.subjectBasePath + if !strings.Contains(subjectPath, "/-/") { + subjectPath += "/-" + } + + return &publish.PageMembers{ + SubjectPath: subjectPath, + ZipName: zipName, + Members: members, + Description: func(member *search.DescribedBlob) string { + des := member.Description() + if des != "" { + des = " - " + des + } + return des + }, + Title: func(member *search.DescribedBlob) string { + memberTitle := getTitle(member.BlobRef, resMap) + if memberTitle == "" { + memberTitle = member.BlobRef.DigestPrefix(10) + } + return html.EscapeString(memberTitle) + }, + Path: func(member *search.DescribedBlob) string { + return pr.memberPath(member.BlobRef) + }, + DomID: func(member *search.DescribedBlob) string { + return member.DomID() + }, + FileInfo: func(member *search.DescribedBlob) *publish.MemberFileInfo { + if path, fileInfo, ok := getFileInfo(member.BlobRef, resMap); ok { + info := &publish.MemberFileInfo{ + FileName: fileInfo.FileName, + FileDomID: path[len(path)-1].DomID(), + FilePath: html.EscapeString(pr.SubresFileURL(path, fileInfo.FileName)), + } + if fileInfo.IsImage() { + info.FileThumbnailURL = pr.SubresThumbnailURL(path, fileInfo.FileName, 200) + } + return info + } + return nil + }, + }, nil +} + +func (ph *publishHandler) describeMembers(br blob.Ref) (*search.SearchResult, error) { + res, err := ph.cl.Query(&search.SearchQuery{ + Constraint: &search.Constraint{ + Permanode: &search.PermanodeConstraint{ + Relation: &search.RelationConstraint{ + Relation: "parent", + Any: &search.Constraint{ + BlobRefPrefix: br.String(), + }, + }, + }, + CamliType: "permanode", + }, + Describe: &search.DescribeRequest{ + Depth: 1, + Rules: []*search.DescribeRule{ + { + Attrs: []string{"camliContent", "camliContentImage"}, + }, + }, + }, + Limit: -1, + }) + if err != nil { + return nil, fmt.Errorf("Could not describe members of %v: %v", br, err) + } + return res, nil +} + +func (pr *publishRequest) memberPath(member blob.Ref) string { + return addPathComponent(pr.subjectBasePath, "/h"+member.DigestPrefix(10)) +} + +func (pr *publishRequest) serveSubresFileDownload() { + des, err := pr.ph.describe(pr.subject) + if err != nil { + logf("error describing subject %q: %v", pr.subject, err) + return + } + pr.serveFileDownload(des) +} + +func (pr *publishRequest) serveSubresImage() { + params := pr.req.URL.Query() + mw, _ := strconv.Atoi(params.Get("mw")) + mh, _ := strconv.Atoi(params.Get("mh")) + des, err := pr.ph.describe(pr.subject) + if err != nil { + logf("error describing subject %q: %v", pr.subject, err) + return + } + pr.serveScaledImage(des, mw, mh, params.Get("square") == "1") +} + +func (pr *publishRequest) serveScaledImage(des *search.DescribedBlob, maxWidth, maxHeight int, square bool) { + fileref, _, ok := pr.fileSchemaRefFromBlob(des) + if !ok { + logf("scaled image fail; failed to get file schema from des %q", des.BlobRef) + return + } + ih := &server.ImageHandler{ + Fetcher: pr.ph.cl, + Cache: pr.ph.cache, + MaxWidth: maxWidth, + MaxHeight: maxHeight, + Square: square, + ThumbMeta: pr.ph.thumbMeta, + ResizeSem: pr.ph.resizeSem, + } + ih.ServeHTTP(pr.rw, pr.req, fileref) +} + +// serveZip streams a zip archive of all the files "under" +// pr.subject. That is, all the files pointed by file permanodes, +// which are directly members of pr.subject or recursively down +// directory permanodes and permanodes members. +func (pr *publishRequest) serveZip() { + filename := "" + if len(pr.subres) > len("/=z/") { + filename = pr.subres[4:] + } + zh := &zipHandler{ + fetcher: pr.ph.cl, + cl: pr.ph.cl, + root: pr.subject, + filename: filename, + } + zh.ServeHTTP(pr.rw, pr.req) +} diff --git a/vendor/github.com/camlistore/camlistore/app/publisher/pics.css b/vendor/github.com/camlistore/camlistore/app/publisher/pics.css new file mode 100644 index 00000000..be091a75 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/app/publisher/pics.css @@ -0,0 +1,112 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* Something arbitrary for testing. */ +body { + font: 13px/1.3 normal Verdana, Geneva, sans-serif; + background: #000; + color: #aaa; + margin: 0; + padding: 30px; +} +a { + color: #aaa; +} +a:hover { + color: #bbb; +} +h1 { + border: 3px dashed #aaa; + padding: 1em; +} +ul { + list-style: none; + background: #262626; + + margin: 0; + padding: 20px; + border-radius: 10px; +} +li { + display: inline-block; + vertical-align: top; + padding: 1em; + margin: 1em; + text-align: right; +} +li:hover { + background: #000; + border-radius: 10px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +li:hover img { + xmax-height: none; + xmax-width: none; +} +li a { + font-weight: bold; +} +li img { + border: 0; + border-radius: 6px; + display: block; + max-height: 200px; + max-width: 200px; + margin-bottom: 1em; +} +li .camlifile { + display: none; +} +li a span { + padding: 2px; + border: 1px solid transparent; +} +li input { + text-align: right; +} + +a.title-edit, +a.title-edit:hover { + font-size: 70%; + color: #f00; + margin-right: .5em; + font-weight: normal; +} + +a.hidden { + display: none; +} + +a.visible { + display: inline; +} + +input.hidden { + display: none; +} + +input.visible { + display: inline-block; +} + +span.hidden { + display: none; +} + +span.visible { + display: inline-block; +} diff --git a/vendor/github.com/camlistore/camlistore/app/publisher/publish_test.go b/vendor/github.com/camlistore/camlistore/app/publisher/publish_test.go new file mode 100644 index 00000000..62f0ff91 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/app/publisher/publish_test.go @@ -0,0 +1,234 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + camliClient "camlistore.org/pkg/client" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/index" + "camlistore.org/pkg/index/indextest" + "camlistore.org/pkg/search" +) + +type publishURLTest struct { + path string // input + subject, subres string // expected +} + +var publishURLTests []publishURLTest + +func setupContent(rootName string) *indextest.IndexDeps { + idx := index.NewMemoryIndex() + idxd := indextest.NewIndexDeps(idx) + + picNode := idxd.NewPlannedPermanode("picpn-1234") // sha1-f5e90fcc50a79caa8b22a4aa63ba92e436cab9ec + galRef := idxd.NewPlannedPermanode("gal-1234") // sha1-2bdf2053922c3dfa70b01a4827168fce1c1df691 + rootRef := idxd.NewPlannedPermanode("root-abcd") // sha1-dbb3e5f28c7e01536d43ce194f3dd7b921b8460d + camp0 := idxd.NewPlannedPermanode("picpn-9876543210") // sha1-2d473e07ca760231dd82edeef4019d5b7d0ccb42 + camp1 := idxd.NewPlannedPermanode("picpn-9876543211") // sha1-961b700536d5151fc1f3920955cc92767572a064 + camp0f, _ := idxd.UploadFile("picfile-f00ff00f00a5.jpg", "picfile-f00ff00f00a5", time.Time{}) // sha1-01dbcb193fc789033fb2d08ed22abe7105b48640 + camp1f, _ := idxd.UploadFile("picfile-f00ff00f00b6.jpg", "picfile-f00ff00f00b6", time.Time{}) // sha1-1213ec17a42cc51bdeb95ff91ac1b5fc5157740f + + idxd.SetAttribute(rootRef, "camliRoot", rootName) + idxd.SetAttribute(rootRef, "camliPath:singlepic", picNode.String()) + idxd.SetAttribute(picNode, "title", "picnode without a pic?") + idxd.SetAttribute(rootRef, "camliPath:camping", galRef.String()) + idxd.AddAttribute(galRef, "camliMember", camp0.String()) + idxd.AddAttribute(galRef, "camliMember", camp1.String()) + idxd.SetAttribute(camp0, "camliContent", camp0f.String()) + idxd.SetAttribute(camp1, "camliContent", camp1f.String()) + + publishURLTests = []publishURLTest{ + // URL to a single picture permanode (returning its HTML wrapper page) + { + path: "/pics/singlepic", + subject: picNode.String(), + }, + + // URL to a gallery permanode (returning its HTML wrapper page) + { + path: "/pics/camping", + subject: galRef.String(), + }, + + // URL to a picture permanode within a gallery (following one hop, returning HTML) + { + path: "/pics/camping/-/h2d473e07ca", + subject: camp0.String(), + }, + + // URL to a gallery -> picture permanode -> its file + // (following two hops, returning HTML) + { + path: "/pics/camping/-/h2d473e07ca/h01dbcb193f", + subject: camp0f.String(), + }, + + // URL to a gallery -> picture permanode -> its file + // (following two hops, returning the file download) + { + path: "/pics/camping/-/h2d473e07ca/h01dbcb193f/=f/marshmallow.jpg", + subject: camp0f.String(), + subres: "/=f/marshmallow.jpg", + }, + + // URL to a gallery -> picture permanode -> its file + // (following two hops, returning the file, scaled as an image) + { + path: "/pics/camping/-/h961b700536/h1213ec17a4/=i/marshmallow.jpg?mw=200&mh=200", + subject: camp1f.String(), + subres: "/=i/marshmallow.jpg", + }, + + // Path to a static file in the root. + // TODO: ditch these and use content-addressable javascript + css, having + // the server digest them on start, or rather part of fileembed. This is + // a short-term hack to unblock Lindsey. + { + path: "/pics/=s/pics.js", + subject: "", + subres: "/=s/pics.js", + }, + } + + return idxd +} + +type fakeClient struct { + *camliClient.Client // for blob.Fetcher + sh *search.Handler +} + +func (fc *fakeClient) Search(req *search.SearchQuery) (*search.SearchResult, error) { + return fc.sh.Query(req) +} + +func (fc *fakeClient) Describe(req *search.DescribeRequest) (*search.DescribeResponse, error) { + return fc.sh.Describe(req) +} + +func (fc *fakeClient) GetJSON(url string, data interface{}) error { + // no need to implement + return nil +} + +func (fc *fakeClient) Post(url string, bodyType string, body io.Reader) error { + // no need to implement + return nil +} + +func TestPublishURLs(t *testing.T) { + rootName := "foo" + idxd := setupContent(rootName) + sh := search.NewHandler(idxd.Index, idxd.SignerBlobRef) + corpus, err := idxd.Index.KeepInMemory() + if err != nil { + t.Fatalf("error slurping index to memory: %v", err) + } + sh.SetCorpus(corpus) + cl := camliClient.New("http://whatever.fake") + fcl := &fakeClient{cl, sh} + ph := &publishHandler{ + rootName: rootName, + cl: fcl, + } + if err := ph.initRootNode(); err != nil { + t.Fatalf("initRootNode: %v", err) + } + + for ti, tt := range publishURLTests { + rw := httptest.NewRecorder() + if !strings.HasPrefix(tt.path, "/pics/") { + panic("expected /pics/ prefix on " + tt.path) + } + req, _ := http.NewRequest("GET", "http://foo.com"+tt.path, nil) + + pfxh := &httputil.PrefixHandler{ + Prefix: "/pics/", + Handler: http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { + pr, err := ph.NewRequest(rw, req) + if err != nil { + t.Fatalf("test #%d, NewRequest: %v", ti, err) + } + + err = pr.findSubject() + if tt.subject != "" { + if err != nil { + t.Errorf("test #%d, findSubject: %v", ti, err) + return + } + if pr.subject.String() != tt.subject { + t.Errorf("test #%d, got subject %q, want %q", ti, pr.subject, tt.subject) + } + } + if pr.subres != tt.subres { + t.Errorf("test #%d, got subres %q, want %q", ti, pr.subres, tt.subres) + } + }), + } + pfxh.ServeHTTP(rw, req) + } +} + +func TestPublishMembers(t *testing.T) { + rootName := "foo" + idxd := setupContent(rootName) + + sh := search.NewHandler(idxd.Index, idxd.SignerBlobRef) + corpus, err := idxd.Index.KeepInMemory() + if err != nil { + t.Fatalf("error slurping index to memory: %v", err) + } + sh.SetCorpus(corpus) + cl := camliClient.New("http://whatever.fake") + fcl := &fakeClient{cl, sh} + ph := &publishHandler{ + rootName: rootName, + cl: fcl, + } + + rw := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://foo.com/pics", nil) + + pfxh := &httputil.PrefixHandler{ + Prefix: "/pics/", + Handler: http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { + pr, err := ph.NewRequest(rw, req) + if err != nil { + t.Fatalf("NewRequest: %v", err) + } + + res, err := pr.ph.deepDescribe(pr.subject) + if err != nil { + t.Fatalf("deepDescribe: %v", err) + } + + members, err := pr.subjectMembers(res.Meta) + if len(members.Members) != 2 { + t.Errorf("Expected two members in publish root (one camlipath, one camlimember), got %d", len(members.Members)) + } + }), + } + pfxh.ServeHTTP(rw, req) +} diff --git a/vendor/github.com/camlistore/camlistore/app/publisher/zip.go b/vendor/github.com/camlistore/camlistore/app/publisher/zip.go new file mode 100644 index 00000000..2202e9e9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/app/publisher/zip.go @@ -0,0 +1,308 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "archive/zip" + "crypto/sha1" + "fmt" + "io" + "log" + "mime" + "net/http" + "path" + "sort" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/search" + "camlistore.org/pkg/types/camtypes" +) + +type zipHandler struct { + fetcher blob.Fetcher + cl client // Used for search and describe requests. + // root is the "parent" permanode of everything to zip. + // Either a directory permanode, or a permanode with members. + root blob.Ref + // Optional name to use in the response header + filename string +} + +// blobFile contains all the information we need about +// a file blob to add the corresponding file to a zip. +type blobFile struct { + blobRef blob.Ref + // path is the full path of the file from the root of the zip. + // slashes are always forward slashes, per the zip spec. + path string +} + +type sortedFiles []*blobFile + +func (s sortedFiles) Less(i, j int) bool { return s[i].path < s[j].path } +func (s sortedFiles) Len() int { return len(s) } +func (s sortedFiles) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func (zh *zipHandler) describeMembers(br blob.Ref) (*search.DescribeResponse, error) { + res, err := zh.cl.Query(&search.SearchQuery{ + Constraint: &search.Constraint{ + BlobRefPrefix: br.String(), + CamliType: "permanode", + }, + Describe: &search.DescribeRequest{ + Depth: 1, + Rules: []*search.DescribeRule{ + { + Attrs: []string{"camliContent", "camliContentImage", "camliMember"}, + }, + }, + }, + Limit: -1, + }) + if err != nil { + return nil, fmt.Errorf("Could not describe %v: %v", br, err) + } + if res == nil || res.Describe == nil { + return nil, fmt.Errorf("no describe result for %v", br) + } + return res.Describe, nil +} + +// blobList returns the list of file blobs "under" dirBlob. +// It traverses permanode directories and permanode with members (collections). +func (zh *zipHandler) blobList(dirPath string, dirBlob blob.Ref) ([]*blobFile, error) { + // dr := zh.search.NewDescribeRequest() + // dr.Describe(dirBlob, 3) + // res, err := dr.Result() + // if err != nil { + // return nil, fmt.Errorf("Could not describe %v: %v", dirBlob, err) + // } + res, err := zh.describeMembers(dirBlob) + if err != nil { + return nil, err + } + + described := res.Meta[dirBlob.String()] + members := described.Members() + dirBlobPath, _, isDir := described.PermanodeDir() + if len(members) == 0 && !isDir { + return nil, nil + } + var list []*blobFile + if isDir { + dirRoot := dirBlobPath[1] + children, err := zh.blobsFromDir("/", dirRoot) + if err != nil { + return nil, fmt.Errorf("Could not get list of blobs from %v: %v", dirRoot, err) + } + list = append(list, children...) + return list, nil + } + for _, member := range members { + if fileBlobPath, fileInfo, ok := getFileInfo(member.BlobRef, res.Meta); ok { + // file + list = append(list, + &blobFile{fileBlobPath[1], path.Join(dirPath, fileInfo.FileName)}) + continue + } + if dirBlobPath, dirInfo, ok := getDirInfo(member.BlobRef, res.Meta); ok { + // directory + newZipRoot := dirBlobPath[1] + children, err := zh.blobsFromDir( + path.Join(dirPath, dirInfo.FileName), newZipRoot) + if err != nil { + return nil, fmt.Errorf("Could not get list of blobs from %v: %v", newZipRoot, err) + } + list = append(list, children...) + // TODO(mpl): we assume a directory permanode does not also have members. + // I know there is nothing preventing it, but does it make any sense? + continue + } + // it might have members, so recurse + // If it does have members, we must consider it as a pseudo dir, + // so we can build a fullpath for each of its members. + // As a dir name, we're using its title if it has one, its (shortened) + // blobref otherwise. + pseudoDirName := member.Title() + if pseudoDirName == "" { + pseudoDirName = member.BlobRef.DigestPrefix(10) + } + fullpath := path.Join(dirPath, pseudoDirName) + moreMembers, err := zh.blobList(fullpath, member.BlobRef) + if err != nil { + return nil, fmt.Errorf("Could not get list of blobs from %v: %v", member.BlobRef, err) + } + list = append(list, moreMembers...) + } + return list, nil +} + +// blobsFromDir returns the list of file blobs in directory dirBlob. +// It only traverses permanode directories. +func (zh *zipHandler) blobsFromDir(dirPath string, dirBlob blob.Ref) ([]*blobFile, error) { + var list []*blobFile + dr, err := schema.NewDirReader(zh.fetcher, dirBlob) + if err != nil { + return nil, fmt.Errorf("Could not read dir blob %v: %v", dirBlob, err) + } + ent, err := dr.Readdir(-1) + if err != nil { + return nil, fmt.Errorf("Could not read dir entries: %v", err) + } + for _, v := range ent { + fullpath := path.Join(dirPath, v.FileName()) + switch v.CamliType() { + case "file": + list = append(list, &blobFile{v.BlobRef(), fullpath}) + case "directory": + children, err := zh.blobsFromDir(fullpath, v.BlobRef()) + if err != nil { + return nil, fmt.Errorf("Could not get list of blobs from %v: %v", v.BlobRef(), err) + } + list = append(list, children...) + } + } + return list, nil +} + +// renameDuplicates goes through bf to check for duplicate filepaths. +// It renames duplicate filepaths and returns a new slice, sorted by +// file path. +func renameDuplicates(bf []*blobFile) sortedFiles { + noDup := make(map[string]blob.Ref) + // use a map to detect duplicates and rename them + for _, file := range bf { + if _, ok := noDup[file.path]; ok { + // path already exists, so rename + suffix := 0 + var newname string + for { + suffix++ + ext := path.Ext(file.path) + newname = fmt.Sprintf("%s(%d)%s", + file.path[:len(file.path)-len(ext)], suffix, ext) + if _, ok := noDup[newname]; !ok { + break + } + } + noDup[newname] = file.blobRef + } else { + noDup[file.path] = file.blobRef + } + } + + // reinsert in a slice and sort it + var sorted sortedFiles + for p, b := range noDup { + sorted = append(sorted, &blobFile{path: p, blobRef: b}) + } + sort.Sort(sorted) + return sorted +} + +// ServeHTTP streams a zip archive of all the files "under" +// zh.root. That is, all the files pointed by file permanodes, +// which are directly members of zh.root or recursively down +// directory permanodes and permanodes members. +// To build the fullpath of a file in a collection, it uses +// the collection title if present, its blobRef otherwise, as +// a directory name. +func (zh *zipHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + // TODO: use http.ServeContent, so Range requests work and downloads can be resumed. + // Will require calculating the zip length once first (ideally as cheaply as possible, + // with dummy counting writer and dummy all-zero-byte-files of a fixed size), + // and then making a dummy ReadSeeker for ServeContent that can seek to the end, + // and then seek back to the beginning, but then seeks forward make it remember + // to skip that many bytes from the archive/zip writer when answering Reads. + if !httputil.IsGet(req) { + http.Error(rw, "Invalid method", http.StatusMethodNotAllowed) + return + } + bf, err := zh.blobList("/", zh.root) + if err != nil { + log.Printf("Could not serve zip for %v: %v", zh.root, err) + http.Error(rw, "Server error", http.StatusInternalServerError) + return + } + blobFiles := renameDuplicates(bf) + + // TODO(mpl): streaming directly won't work on appengine if the size goes + // over 32 MB. Deal with that. + h := rw.Header() + h.Set("Content-Type", "application/zip") + filename := zh.filename + if filename == "" { + filename = "download.zip" + } + h.Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename})) + zw := zip.NewWriter(rw) + etag := sha1.New() + for _, file := range blobFiles { + etag.Write([]byte(file.blobRef.String())) + } + h.Set("Etag", fmt.Sprintf(`"%x"`, etag.Sum(nil))) + + for _, file := range blobFiles { + fr, err := schema.NewFileReader(zh.fetcher, file.blobRef) + if err != nil { + log.Printf("Can not add %v in zip, not a file: %v", file.blobRef, err) + http.Error(rw, "Server error", http.StatusInternalServerError) + return + } + f, err := zw.CreateHeader( + &zip.FileHeader{ + Name: file.path, + Method: zip.Store, + }) + if err != nil { + log.Printf("Could not create %q in zip: %v", file.path, err) + http.Error(rw, "Server error", http.StatusInternalServerError) + return + } + _, err = io.Copy(f, fr) + fr.Close() + if err != nil { + log.Printf("Could not zip %q: %v", file.path, err) + return + } + } + err = zw.Close() + if err != nil { + log.Printf("Could not close zipwriter: %v", err) + return + } +} + +// TODO(mpl): refactor with getFileInfo +func getDirInfo(item blob.Ref, peers map[string]*search.DescribedBlob) (path []blob.Ref, di *camtypes.FileInfo, ok bool) { + described := peers[item.String()] + if described == nil || + described.Permanode == nil || + described.Permanode.Attr == nil { + return + } + contentRef := described.Permanode.Attr.Get("camliContent") + if contentRef == "" { + return + } + if cdes := peers[contentRef]; cdes != nil && cdes.Dir != nil { + return []blob.Ref{described.BlobRef, cdes.BlobRef}, cdes.Dir, true + } + return +} diff --git a/vendor/github.com/camlistore/camlistore/clients/android/.classpath b/vendor/github.com/camlistore/camlistore/clients/android/.classpath new file mode 100644 index 00000000..d57ec025 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/clients/android/.gitignore b/vendor/github.com/camlistore/camlistore/clients/android/.gitignore new file mode 100644 index 00000000..48dba237 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/.gitignore @@ -0,0 +1,8 @@ +build +gen +bin +local.properties +test/local.properties +test/build +test/gen +test/bin diff --git a/vendor/github.com/camlistore/camlistore/clients/android/.project b/vendor/github.com/camlistore/camlistore/clients/android/.project new file mode 100644 index 00000000..71acf2ff --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/.project @@ -0,0 +1,33 @@ + + + camlistore + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/vendor/github.com/camlistore/camlistore/clients/android/.settings/org.eclipse.jdt.core.prefs b/vendor/github.com/camlistore/camlistore/clients/android/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..24758068 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,301 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.codeComplete.argumentPrefixes= +org.eclipse.jdt.core.codeComplete.argumentSuffixes= +org.eclipse.jdt.core.codeComplete.fieldPrefixes= +org.eclipse.jdt.core.codeComplete.fieldSuffixes= +org.eclipse.jdt.core.codeComplete.localPrefixes= +org.eclipse.jdt.core.codeComplete.localSuffixes= +org.eclipse.jdt.core.codeComplete.staticFieldPrefixes= +org.eclipse.jdt.core.codeComplete.staticFieldSuffixes= +org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes= +org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes= +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6 +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=false +org.eclipse.jdt.core.formatter.comment.format_source_code=true +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true +org.eclipse.jdt.core.formatter.comment.indent_root_tags=true +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert +org.eclipse.jdt.core.formatter.comment.line_length=500 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.jdt.core.formatter.indentation.size=4 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=false +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.lineSplit=200 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=space +org.eclipse.jdt.core.formatter.tabulation.size=4 +org.eclipse.jdt.core.formatter.use_on_off_tags=false +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false +org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true diff --git a/vendor/github.com/camlistore/camlistore/clients/android/.settings/org.eclipse.jdt.ui.prefs b/vendor/github.com/camlistore/camlistore/clients/android/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..d8c2527c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,60 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +formatter_profile=_Camlistore Policy +formatter_settings_version=12 +org.eclipse.jdt.ui.exception.name=e +org.eclipse.jdt.ui.gettersetter.use.is=true +org.eclipse.jdt.ui.keywordthis=false +org.eclipse.jdt.ui.overrideannotation=true +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=true +sp_cleanup.format_source_code=true +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.make_local_variable_final=false +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=true +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.organize_imports=true +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=true +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=true +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/vendor/github.com/camlistore/camlistore/clients/android/AndroidManifest.xml b/vendor/github.com/camlistore/camlistore/clients/android/AndroidManifest.xml new file mode 100644 index 00000000..e9bf0cdc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/AndroidManifest.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/clients/android/Makefile b/vendor/github.com/camlistore/camlistore/clients/android/Makefile new file mode 100644 index 00000000..5cab0c3f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/Makefile @@ -0,0 +1,17 @@ +all: + ./check-environment.pl + ant debug + +# Dummy target to make build.pl happy +install: + ./check-environment.pl + ant debug + +env: + docker build -t camlistore/android devenv + +dockerdebug: + docker run -v $(GOPATH)/src/camlistore.org:/src/camlistore.org camlistore/android /src/camlistore.org/clients/android/build-in-docker.pl debug + +dockerrelease: + docker run -i -t -v $(GOPATH)/src/camlistore.org:/src/camlistore.org -v $(HOME)/keys/android-camlistore:/keys camlistore/android /src/camlistore.org/clients/android/build-in-docker.pl release diff --git a/vendor/github.com/camlistore/camlistore/clients/android/build-in-docker.pl b/vendor/github.com/camlistore/camlistore/clients/android/build-in-docker.pl new file mode 100755 index 00000000..820dc201 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/build-in-docker.pl @@ -0,0 +1,64 @@ +#!/usr/bin/perl + +use strict; +use File::Path qw(make_path); + +die "This script is meant to be run within the camlistore/android Docker contain. Run 'make env' to build it.\n" + unless $ENV{IN_DOCKER}; + +my $mode = shift || "debug"; + +my $ANDROID = "/src/camlistore.org/clients/android"; +my $ASSETS = "$ANDROID/assets"; +my $GENDIR = "$ANDROID/gen/org/camlistore"; + +umask 0; +make_path($GENDIR, { mode => 0755 }) unless -d $GENDIR; + +$ENV{GOROOT} = "/usr/local/go"; +$ENV{GOBIN} = $GENDIR; +$ENV{GOPATH} = "/"; +$ENV{GOARCH} = "arm"; +print "Building ARM camlistore.org/cmd/camput\n"; +system("/usr/local/go/bin/go", "install", "camlistore.org/cmd/camput") + and die "Failed to build camput"; + +system("cp", "-p", "$GENDIR/linux_arm/camput", "$ASSETS/camput.arm") + and die "cp failure"; +# TODO: build an x86 version too? if/when those Android devices matter. + +{ + open(my $vfh, ">$ASSETS/camput-version.txt") or die "open camput-version error: $!"; + # TODO(bradfitz): make these values automatic, and don't make the + # "Version" menu say "camput version" when it runs. Also maybe + # keep a history of these somewhere more convenient. + print $vfh "app 0.6.1 camput ccacf764 go 70499e5fbe5b"; +} + +chdir $ASSETS or die "can't cd to assets dir"; + +my $digest = `openssl sha1 camput.arm`; +chomp $digest; +print "ARM camput is $digest\n"; +die "No digest" unless $digest; +write_file("$GENDIR/ChildProcessConfig.java", "package org.camlistore; public final class ChildProcessConfig { // $digest\n}"); + +print "Running ant $mode\n"; +chdir $ANDROID or die "can't cd to android dir"; +exec "ant", + "-Dsdk.dir=/usr/local/android-sdk-linux", + "-Dkey.store=/keys/android-camlistore.keystore", + "-Dkey.alias=camkey", + $mode; + +sub write_file { + my ($file, $contents) = @_; + if (open(my $fh, $file)) { + my $cur = do { local $/; <$fh> }; + return if $cur eq $contents; + } + open(my $fh, ">$file") or die "Failed to open $file: $!"; + print $fh $contents; + close($fh) or die "Close: $!"; + print "Wrote $file\n"; +} diff --git a/vendor/github.com/camlistore/camlistore/clients/android/build.properties b/vendor/github.com/camlistore/camlistore/clients/android/build.properties new file mode 100644 index 00000000..4ccf4d1c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/build.properties @@ -0,0 +1,2 @@ +out.dir=build +gen.dir=build/gen diff --git a/vendor/github.com/camlistore/camlistore/clients/android/build.xml b/vendor/github.com/camlistore/camlistore/clients/android/build.xml new file mode 100644 index 00000000..d1d6357f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/build.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/clients/android/check-environment.pl b/vendor/github.com/camlistore/camlistore/clients/android/check-environment.pl new file mode 100755 index 00000000..daf1610e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/check-environment.pl @@ -0,0 +1,14 @@ +#!/usr/bin/perl + +use strict; +use FindBin qw($Bin); + +my $props = "$Bin/local.properties"; +unless (-e $props) { + die "\n". + "**************************************************************\n". + "Can't build the Camlistore Android client; SDK not configured.\n". + "You need to create your $props file.\n". + "See local.properties.TEMPLATE for instructions.\n". + "**************************************************************\n\n"; +} diff --git a/vendor/github.com/camlistore/camlistore/clients/android/default.properties b/vendor/github.com/camlistore/camlistore/clients/android/default.properties new file mode 100644 index 00000000..b6f30167 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/default.properties @@ -0,0 +1,12 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "build.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-17 + diff --git a/vendor/github.com/camlistore/camlistore/clients/android/devenv/Dockerfile b/vendor/github.com/camlistore/camlistore/clients/android/devenv/Dockerfile new file mode 100644 index 00000000..39acc747 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/devenv/Dockerfile @@ -0,0 +1,38 @@ +# Build environment in which to build the Camlistore Android app. +# +# This extends the Dockerfile from https://index.docker.io/u/wasabeef/android/ + +FROM wasabeef/android +MAINTAINER bradfitz + +# Found these from: android list sdk -u -e +RUN android list sdk -u -e | grep build-tools- | perl -npe 's/.+"(.+)"/$1/' > /tmp/build-tools-version +RUN perl -e 'die "No Android build tools version found." unless -s "/tmp/build-tools-version"' +RUN echo y | android update sdk -u -t $(cat /tmp/build-tools-version) +RUN echo y | android update sdk -u -t android-17 + +# Don't need mercurial yet, since we're just using the archive URL to fetch Go. +# But it's possible we may want to switch to using hg, in which case: +# RUN yum -y install mercurial + +# Update the GOVERS to depend on a new version of Go. +# +# The 073fc578434b version is Go 1.3.1 (2014-02-21), +# to satisfy the dependency for Go 1.3 in the Docker build of +# camput. +ENV GOVERS 073fc578434b + +RUN cd /usr/local && curl -O http://go.googlecode.com/archive/$GOVERS.zip +RUN cd /usr/local && unzip -q $GOVERS.zip +RUN cd /usr/local && mv go-$GOVERS go +RUN chmod 0755 /usr/local/go/src/make.bash +RUN echo $GOVERS > /usr/local/go/VERSION +RUN GOROOT=/usr/local/go GOARCH=arm bash -c "cd /usr/local/go/src && ./make.bash" + + +ENV ANDROID_HOME /usr/local/android-sdk-linux +ENV ANT_HOME /usr/local/apache-ant-1.9.2 +ENV PATH $PATH:$ANDROID_HOME/tools +ENV PATH $PATH:$ANDROID_HOME/platform-tools +ENV PATH $PATH:$ANT_HOME/bin +ENV IN_DOCKER 1 diff --git a/vendor/github.com/camlistore/camlistore/clients/android/local.properties.TEMPLATE b/vendor/github.com/camlistore/camlistore/clients/android/local.properties.TEMPLATE new file mode 100644 index 00000000..32c51f69 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/local.properties.TEMPLATE @@ -0,0 +1,3 @@ +# Copy this file to one named "local.properties" and update this path to +# wherever your Android SDK is located: +sdk.dir=/home/bradfitz/sdk/android diff --git a/vendor/github.com/camlistore/camlistore/clients/android/project.properties b/vendor/github.com/camlistore/camlistore/clients/android/project.properties new file mode 100644 index 00000000..a3ee5ab6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 diff --git a/vendor/github.com/camlistore/camlistore/clients/android/res/drawable-hdpi/icon.png b/vendor/github.com/camlistore/camlistore/clients/android/res/drawable-hdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..978e3fabfa839fd08c04869ee9d172595cc2d3f8 GIT binary patch literal 10518 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/clients/android/res/drawable-mdpi/icon.png b/vendor/github.com/camlistore/camlistore/clients/android/res/drawable-mdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..14f58169caa0c7ce38ace03bb11719a50ab4a08b GIT binary patch literal 7395 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 yz4}D9uRqiR0r@|hXnf%XEXcJ$c)Tf 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 a|M(A^rE^~Nj;AdE0000 zYd4AvIRA;`(pk)(FoK&>L>IG|265rS7r61+VhcV{tJ~4(wyQdC=FN;aE+X#Dn_1c2 z)xvbZ$p|9j@x||)6Ok!q#vd)0zJBaqqW&q-LPWfXdKE21)QhM`v^XQ`pBtWf`Gz(r z{wUC|XYc!~sDEB!K(s(KAQliSavhLZJS!Uh1H5u0Ys+z!+W%|`rky0Lsfq$S{#V@Ls5TOw74MZe-ickqQ&PRA29s89UJPq_s6db zJdGBBN5p}8kOCyZARg5MRgbt-JLgeXpv3@R3^pI2ZHxa#w0QAYP$DAly-|MaoczfE zj0BMuLeyn-9Fn25?b8BvX`FK&KlBU+1IlVdRSomJ6~MQ}%!pUcoVLY}_h8N45br|V z4%Vo#a9D6L&dT5ZckVgS644TMC88rhFatvn02v~#LUb#Ssfckwm|voqou{dPU|N5V z*^I&u*u7)CLABzXG(SE23B(81v;pxT1IQ;q2Dxv&-G0AxWvUCnfheFr?q|riU;L9r zbBS9|m6X*exqGQptq?C9^_POQ3%aF=%Ns7+(gJj_1CU`d9nq?r++-jVZry!?xR`Qy za0LrfVyKB>nru;{z`3JQq)9C_IYzX1;?^9}!MhbuqyQZtn!wNsv`mmPV0_ENAj)J? z6BaE@wlKY{z?U0Bduh_Puh!o;P5qJ&li`={OotVW@yWliSWKBtCM`^B2MWA%o#b09 zc8xlcB0pjlz!qS0h*O9)SOYf4tY+urgT(oax~}^`Nh!a(_c5@4bmMH+YZH5$N6b=< zg`Ul`HJHt?SYvUL>l%wwuo#6ofd5eO{fYLTyl z*1fizYYt?d$Fcgo1)L~;u+4OO2@8M)VtTR;HbYqpwF+<~j;!6v5PEZLkrKnSH@>fa z03y})om{+do@Qov9}rz2<}bETZ)F0vJkb)UQ^R^eZw6xBSj#XXF3sz*wNOnIm4yx_f%R2dze;S?EK*k6h!)Idb7II)F`Vy1 zy$QU?da=-RRqR|PmSHGT?1;-Tq#N$AR;5)J3Q%p`Mm;&7vN<#$VZTcazq|JtaP|lk zSa6D5GUEz+cm^1R9hzr!H6*;yYky; z4(l?lRY|kcmhoiWXaD|-l+_qBXyz~)0&oQCwQEAHC>QfividIeTxS`rkF_4`RaFAe z%#G1d4o^RNc-X($@Vk3wflsdw)i2h&$!9%RuDh)7x@iuF1!cK~8O-NEepz(GF0b7f z)OCP6+gzM07>%}QZXVR}O0{+3+E9LX?vAryqL%DS+Prh+&}szF4uL00000NkvXXu0mjf DQI3s0 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/clients/android/res/layout/main.xml b/vendor/github.com/camlistore/camlistore/clients/android/res/layout/main.xml new file mode 100644 index 00000000..6d3debd9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/android/res/layout/main.xml @@ -0,0 +1,77 @@ + + + + + + + + ",h).unbind("click").click(function(){h.click.apply(b.element[0],arguments)}).appendTo(g);c.fn.button&&e.button()});f.appendTo(b.uiDialog)}},_makeDraggable:function(){function a(e){return{position:e.position, +offset:e.offset}}var b=this,d=b.options,f=c(document),g;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(e,h){g=d.height==="auto"?"auto":c(this).height();c(this).height(c(this).height()).addClass("ui-dialog-dragging");b._trigger("dragStart",e,a(h))},drag:function(e,h){b._trigger("drag",e,a(h))},stop:function(e,h){d.position=[h.position.left-f.scrollLeft(),h.position.top-f.scrollTop()];c(this).removeClass("ui-dialog-dragging").height(g); +b._trigger("dragStop",e,a(h));c.ui.dialog.overlay.resize()}})},_makeResizable:function(a){function b(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}a=a===j?this.options.resizable:a;var d=this,f=d.options,g=d.uiDialog.css("position");a=typeof a==="string"?a:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:f.maxWidth,maxHeight:f.maxHeight,minWidth:f.minWidth,minHeight:d._minHeight(), +handles:a,start:function(e,h){c(this).addClass("ui-dialog-resizing");d._trigger("resizeStart",e,b(h))},resize:function(e,h){d._trigger("resize",e,b(h))},stop:function(e,h){c(this).removeClass("ui-dialog-resizing");f.height=c(this).height();f.width=c(this).width();d._trigger("resizeStop",e,b(h));c.ui.dialog.overlay.resize()}}).css("position",g).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight, +a.height)},_position:function(a){var b=[],d=[0,0],f;if(a){if(typeof a==="string"||typeof a==="object"&&"0"in a){b=a.split?a.split(" "):[a[0],a[1]];if(b.length===1)b[1]=b[0];c.each(["left","top"],function(g,e){if(+b[g]===b[g]){d[g]=b[g];b[g]=e}});a={my:b.join(" "),at:b.join(" "),offset:d.join(" ")}}a=c.extend({},c.ui.dialog.prototype.options.position,a)}else a=c.ui.dialog.prototype.options.position;(f=this.uiDialog.is(":visible"))||this.uiDialog.show();this.uiDialog.css({top:0,left:0}).position(a); +f||this.uiDialog.hide()},_setOption:function(a,b){var d=this,f=d.uiDialog,g=f.is(":data(resizable)"),e=false;switch(a){case "beforeclose":a="beforeClose";break;case "buttons":d._createButtons(b);e=true;break;case "closeText":d.uiDialogTitlebarCloseText.text(""+b);break;case "dialogClass":f.removeClass(d.options.dialogClass).addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b);break;case "disabled":b?f.addClass("ui-dialog-disabled"):f.removeClass("ui-dialog-disabled");break;case "draggable":b? +d._makeDraggable():f.draggable("destroy");break;case "height":e=true;break;case "maxHeight":g&&f.resizable("option","maxHeight",b);e=true;break;case "maxWidth":g&&f.resizable("option","maxWidth",b);e=true;break;case "minHeight":g&&f.resizable("option","minHeight",b);e=true;break;case "minWidth":g&&f.resizable("option","minWidth",b);e=true;break;case "position":d._position(b);break;case "resizable":g&&!b&&f.resizable("destroy");g&&typeof b==="string"&&f.resizable("option","handles",b);!g&&b!==false&& +d._makeResizable(b);break;case "title":c(".ui-dialog-title",d.uiDialogTitlebar).html(""+(b||" "));break;case "width":e=true;break}c.Widget.prototype._setOption.apply(d,arguments);e&&d._size()},_size:function(){var a=this.options,b;this.element.css({width:"auto",minHeight:0,height:0});if(a.minWidth>a.width)a.width=a.minWidth;b=this.uiDialog.css({height:"auto",width:a.width}).height();this.element.css(a.height==="auto"?{minHeight:Math.max(a.minHeight-b,0),height:c.support.minHeight?"auto":Math.max(a.minHeight- +b,0)}:{minHeight:0,height:Math.max(a.height-b,0)}).show();this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}});c.extend(c.ui.dialog,{version:"1.8.5",uuid:0,maxZ:0,getTitleId:function(a){a=a.attr("id");if(!a){this.uuid+=1;a=this.uuid}return"ui-dialog-title-"+a},overlay:function(a){this.$el=c.ui.dialog.overlay.create(a)}});c.extend(c.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:c.map("focus,mousedown,mouseup,keydown,keypress,click".split(","), +function(a){return a+".dialog-overlay"}).join(" "),create:function(a){if(this.instances.length===0){setTimeout(function(){c.ui.dialog.overlay.instances.length&&c(document).bind(c.ui.dialog.overlay.events,function(d){if(c(d.target).zIndex()").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});c.fn.bgiframe&&b.bgiframe();this.instances.push(b);return b},destroy:function(a){this.oldInstances.push(this.instances.splice(c.inArray(a,this.instances),1)[0]);this.instances.length===0&&c([document,window]).unbind(".dialog-overlay");a.remove();var b=0;c.each(this.instances,function(){b=Math.max(b,this.css("z-index"))});this.maxZ=b},height:function(){var a, +b;if(c.browser.msie&&c.browser.version<7){a=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);b=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return a");if(!b.values)b.values=[this._valueMin(),this._valueMin()];if(b.values.length&&b.values.length!==2)b.values=[b.values[0],b.values[0]]}else this.range=d("
");this.range.appendTo(this.element).addClass("ui-slider-range");if(b.range==="min"||b.range==="max")this.range.addClass("ui-slider-range-"+b.range);this.range.addClass("ui-widget-header")}d(".ui-slider-handle",this.element).length===0&&d("").appendTo(this.element).addClass("ui-slider-handle"); +if(b.values&&b.values.length)for(;d(".ui-slider-handle",this.element).length").appendTo(this.element).addClass("ui-slider-handle");this.handles=d(".ui-slider-handle",this.element).addClass("ui-state-default ui-corner-all");this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(c){c.preventDefault()}).hover(function(){b.disabled||d(this).addClass("ui-state-hover")},function(){d(this).removeClass("ui-state-hover")}).focus(function(){if(b.disabled)d(this).blur(); +else{d(".ui-slider .ui-state-focus").removeClass("ui-state-focus");d(this).addClass("ui-state-focus")}}).blur(function(){d(this).removeClass("ui-state-focus")});this.handles.each(function(c){d(this).data("index.ui-slider-handle",c)});this.handles.keydown(function(c){var e=true,f=d(this).data("index.ui-slider-handle"),h,g,i;if(!a.options.disabled){switch(c.keyCode){case d.ui.keyCode.HOME:case d.ui.keyCode.END:case d.ui.keyCode.PAGE_UP:case d.ui.keyCode.PAGE_DOWN:case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:e= +false;if(!a._keySliding){a._keySliding=true;d(this).addClass("ui-state-active");h=a._start(c,f);if(h===false)return}break}i=a.options.step;h=a.options.values&&a.options.values.length?(g=a.values(f)):(g=a.value());switch(c.keyCode){case d.ui.keyCode.HOME:g=a._valueMin();break;case d.ui.keyCode.END:g=a._valueMax();break;case d.ui.keyCode.PAGE_UP:g=a._trimAlignValue(h+(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.PAGE_DOWN:g=a._trimAlignValue(h-(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:if(h=== +a._valueMax())return;g=a._trimAlignValue(h+i);break;case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:if(h===a._valueMin())return;g=a._trimAlignValue(h-i);break}a._slide(c,f,g);return e}}).keyup(function(c){var e=d(this).data("index.ui-slider-handle");if(a._keySliding){a._keySliding=false;a._stop(c,e);a._change(c,e);d(this).removeClass("ui-state-active")}});this._refreshValue();this._animateOff=false},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider"); +this._mouseDestroy();return this},_mouseCapture:function(a){var b=this.options,c,e,f,h,g;if(b.disabled)return false;this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();c=this._normValueFromMouse({x:a.pageX,y:a.pageY});e=this._valueMax()-this._valueMin()+1;h=this;this.handles.each(function(i){var j=Math.abs(c-h.values(i));if(e>j){e=j;f=d(this);g=i}});if(b.range===true&&this.values(1)===b.min){g+=1;f=d(this.handles[g])}if(this._start(a, +g)===false)return false;this._mouseSliding=true;h._handleIndex=g;f.addClass("ui-state-active").focus();b=f.offset();this._clickOffset=!d(a.target).parents().andSelf().is(".ui-slider-handle")?{left:0,top:0}:{left:a.pageX-b.left-f.width()/2,top:a.pageY-b.top-f.height()/2-(parseInt(f.css("borderTopWidth"),10)||0)-(parseInt(f.css("borderBottomWidth"),10)||0)+(parseInt(f.css("marginTop"),10)||0)};this._slide(a,g,c);return this._animateOff=true},_mouseStart:function(){return true},_mouseDrag:function(a){var b= +this._normValueFromMouse({x:a.pageX,y:a.pageY});this._slide(a,this._handleIndex,b);return false},_mouseStop:function(a){this.handles.removeClass("ui-state-active");this._mouseSliding=false;this._stop(a,this._handleIndex);this._change(a,this._handleIndex);this._clickOffset=this._handleIndex=null;return this._animateOff=false},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b;if(this.orientation==="horizontal"){b= +this.elementSize.width;a=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{b=this.elementSize.height;a=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}b=a/b;if(b>1)b=1;if(b<0)b=0;if(this.orientation==="vertical")b=1-b;a=this._valueMax()-this._valueMin();return this._trimAlignValue(this._valueMin()+b*a)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b); +c.values=this.values()}return this._trigger("start",a,c)},_slide:function(a,b,c){var e;if(this.options.values&&this.options.values.length){e=this.values(b?0:1);if(this.options.values.length===2&&this.options.range===true&&(b===0&&c>e||b===1&&c1){this.options.values[a]=this._trimAlignValue(b);this._refreshValue();this._change(null,a)}if(arguments.length)if(d.isArray(arguments[0])){c=this.options.values;e=arguments[0];for(f=0;fthis._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=a%b;a=a-c;if(Math.abs(c)*2>=b)a+=c>0?b:-b;return parseFloat(a.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var a= +this.options.range,b=this.options,c=this,e=!this._animateOff?b.animate:false,f,h={},g,i,j,l;if(this.options.values&&this.options.values.length)this.handles.each(function(k){f=(c.values(k)-c._valueMin())/(c._valueMax()-c._valueMin())*100;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";d(this).stop(1,1)[e?"animate":"css"](h,b.animate);if(c.options.range===true)if(c.orientation==="horizontal"){if(k===0)c.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({width:f- +g+"%"},{queue:false,duration:b.animate})}else{if(k===0)c.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({height:f-g+"%"},{queue:false,duration:b.animate})}g=f});else{i=this.value();j=this._valueMin();l=this._valueMax();f=l!==j?(i-j)/(l-j)*100:0;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";this.handle.stop(1,1)[e?"animate":"css"](h,b.animate);if(a==="min"&&this.orientation==="horizontal")this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"}, +b.animate);if(a==="max"&&this.orientation==="horizontal")this.range[e?"animate":"css"]({width:100-f+"%"},{queue:false,duration:b.animate});if(a==="min"&&this.orientation==="vertical")this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},b.animate);if(a==="max"&&this.orientation==="vertical")this.range[e?"animate":"css"]({height:100-f+"%"},{queue:false,duration:b.animate})}}});d.extend(d.ui.slider,{version:"1.8.5"})})(jQuery); +;/* + * jQuery UI Tabs 1.8.5 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Tabs + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + */ +(function(d,p){function u(){return++v}function w(){return++x}var v=0,x=0;d.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:false,cookie:null,collapsible:false,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"
",remove:null,select:null,show:null,spinner:"Loading…",tabTemplate:"
  • #{label}
  • "},_create:function(){this._tabify(true)},_setOption:function(a,e){if(a=="selected")this.options.collapsible&& +e==this.options.selected||this.select(e);else{this.options[a]=e;this._tabify()}},_tabId:function(a){return a.title&&a.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+u()},_sanitizeSelector:function(a){return a.replace(/:/g,"\\:")},_cookie:function(){var a=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+w());return d.cookie.apply(null,[a].concat(d.makeArray(arguments)))},_ui:function(a,e){return{tab:a,panel:e,index:this.anchors.index(a)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var a= +d(this);a.html(a.data("label.tabs")).removeData("label.tabs")})},_tabify:function(a){function e(g,f){g.css("display","");!d.support.opacity&&f.opacity&&g[0].style.removeAttribute("filter")}var b=this,c=this.options,h=/^#.+/;this.list=this.element.find("ol,ul").eq(0);this.lis=d(" > li:has(a[href])",this.list);this.anchors=this.lis.map(function(){return d("a",this)[0]});this.panels=d([]);this.anchors.each(function(g,f){var i=d(f).attr("href"),l=i.split("#")[0],q;if(l&&(l===location.toString().split("#")[0]|| +(q=d("base")[0])&&l===q.href)){i=f.hash;f.href=i}if(h.test(i))b.panels=b.panels.add(b._sanitizeSelector(i));else if(i&&i!=="#"){d.data(f,"href.tabs",i);d.data(f,"load.tabs",i.replace(/#.*$/,""));i=b._tabId(f);f.href="#"+i;f=d("#"+i);if(!f.length){f=d(c.panelTemplate).attr("id",i).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(b.panels[g-1]||b.list);f.data("destroy.tabs",true)}b.panels=b.panels.add(f)}else c.disabled.push(g)});if(a){this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all"); +this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.lis.addClass("ui-state-default ui-corner-top");this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom");if(c.selected===p){location.hash&&this.anchors.each(function(g,f){if(f.hash==location.hash){c.selected=g;return false}});if(typeof c.selected!=="number"&&c.cookie)c.selected=parseInt(b._cookie(),10);if(typeof c.selected!=="number"&&this.lis.filter(".ui-tabs-selected").length)c.selected= +this.lis.index(this.lis.filter(".ui-tabs-selected"));c.selected=c.selected||(this.lis.length?0:-1)}else if(c.selected===null)c.selected=-1;c.selected=c.selected>=0&&this.anchors[c.selected]||c.selected<0?c.selected:0;c.disabled=d.unique(c.disabled.concat(d.map(this.lis.filter(".ui-state-disabled"),function(g){return b.lis.index(g)}))).sort();d.inArray(c.selected,c.disabled)!=-1&&c.disabled.splice(d.inArray(c.selected,c.disabled),1);this.panels.addClass("ui-tabs-hide");this.lis.removeClass("ui-tabs-selected ui-state-active"); +if(c.selected>=0&&this.anchors.length){this.panels.eq(c.selected).removeClass("ui-tabs-hide");this.lis.eq(c.selected).addClass("ui-tabs-selected ui-state-active");b.element.queue("tabs",function(){b._trigger("show",null,b._ui(b.anchors[c.selected],b.panels[c.selected]))});this.load(c.selected)}d(window).bind("unload",function(){b.lis.add(b.anchors).unbind(".tabs");b.lis=b.anchors=b.panels=null})}else c.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"));this.element[c.collapsible?"addClass": +"removeClass"]("ui-tabs-collapsible");c.cookie&&this._cookie(c.selected,c.cookie);a=0;for(var j;j=this.lis[a];a++)d(j)[d.inArray(a,c.disabled)!=-1&&!d(j).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");c.cache===false&&this.anchors.removeData("cache.tabs");this.lis.add(this.anchors).unbind(".tabs");if(c.event!=="mouseover"){var k=function(g,f){f.is(":not(.ui-state-disabled)")&&f.addClass("ui-state-"+g)},n=function(g,f){f.removeClass("ui-state-"+g)};this.lis.bind("mouseover.tabs", +function(){k("hover",d(this))});this.lis.bind("mouseout.tabs",function(){n("hover",d(this))});this.anchors.bind("focus.tabs",function(){k("focus",d(this).closest("li"))});this.anchors.bind("blur.tabs",function(){n("focus",d(this).closest("li"))})}var m,o;if(c.fx)if(d.isArray(c.fx)){m=c.fx[0];o=c.fx[1]}else m=o=c.fx;var r=o?function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.hide().removeClass("ui-tabs-hide").animate(o,o.duration||"normal",function(){e(f,o);b._trigger("show", +null,b._ui(g,f[0]))})}:function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.removeClass("ui-tabs-hide");b._trigger("show",null,b._ui(g,f[0]))},s=m?function(g,f){f.animate(m,m.duration||"normal",function(){b.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");e(f,m);b.element.dequeue("tabs")})}:function(g,f){b.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");b.element.dequeue("tabs")};this.anchors.bind(c.event+".tabs", +function(){var g=this,f=d(g).closest("li"),i=b.panels.filter(":not(.ui-tabs-hide)"),l=d(b._sanitizeSelector(g.hash));if(f.hasClass("ui-tabs-selected")&&!c.collapsible||f.hasClass("ui-state-disabled")||f.hasClass("ui-state-processing")||b.panels.filter(":animated").length||b._trigger("select",null,b._ui(this,l[0]))===false){this.blur();return false}c.selected=b.anchors.index(this);b.abort();if(c.collapsible)if(f.hasClass("ui-tabs-selected")){c.selected=-1;c.cookie&&b._cookie(c.selected,c.cookie);b.element.queue("tabs", +function(){s(g,i)}).dequeue("tabs");this.blur();return false}else if(!i.length){c.cookie&&b._cookie(c.selected,c.cookie);b.element.queue("tabs",function(){r(g,l)});b.load(b.anchors.index(this));this.blur();return false}c.cookie&&b._cookie(c.selected,c.cookie);if(l.length){i.length&&b.element.queue("tabs",function(){s(g,i)});b.element.queue("tabs",function(){r(g,l)});b.load(b.anchors.index(this))}else throw"jQuery UI Tabs: Mismatching fragment identifier.";d.browser.msie&&this.blur()});this.anchors.bind("click.tabs", +function(){return false})},_getIndex:function(a){if(typeof a=="string")a=this.anchors.index(this.anchors.filter("[href$="+a+"]"));return a},destroy:function(){var a=this.options;this.abort();this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs");this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.anchors.each(function(){var e=d.data(this,"href.tabs");if(e)this.href= +e;var b=d(this).unbind(".tabs");d.each(["href","load","cache"],function(c,h){b.removeData(h+".tabs")})});this.lis.unbind(".tabs").add(this.panels).each(function(){d.data(this,"destroy.tabs")?d(this).remove():d(this).removeClass("ui-state-default ui-corner-top ui-tabs-selected ui-state-active ui-state-hover ui-state-focus ui-state-disabled ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide")});a.cookie&&this._cookie(null,a.cookie);return this},add:function(a,e,b){if(b===p)b=this.anchors.length; +var c=this,h=this.options;e=d(h.tabTemplate.replace(/#\{href\}/g,a).replace(/#\{label\}/g,e));a=!a.indexOf("#")?a.replace("#",""):this._tabId(d("a",e)[0]);e.addClass("ui-state-default ui-corner-top").data("destroy.tabs",true);var j=d("#"+a);j.length||(j=d(h.panelTemplate).attr("id",a).data("destroy.tabs",true));j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide");if(b>=this.lis.length){e.appendTo(this.list);j.appendTo(this.list[0].parentNode)}else{e.insertBefore(this.lis[b]); +j.insertBefore(this.panels[b])}h.disabled=d.map(h.disabled,function(k){return k>=b?++k:k});this._tabify();if(this.anchors.length==1){h.selected=0;e.addClass("ui-tabs-selected ui-state-active");j.removeClass("ui-tabs-hide");this.element.queue("tabs",function(){c._trigger("show",null,c._ui(c.anchors[0],c.panels[0]))});this.load(0)}this._trigger("add",null,this._ui(this.anchors[b],this.panels[b]));return this},remove:function(a){a=this._getIndex(a);var e=this.options,b=this.lis.eq(a).remove(),c=this.panels.eq(a).remove(); +if(b.hasClass("ui-tabs-selected")&&this.anchors.length>1)this.select(a+(a+1=a?--h:h});this._tabify();this._trigger("remove",null,this._ui(b.find("a")[0],c[0]));return this},enable:function(a){a=this._getIndex(a);var e=this.options;if(d.inArray(a,e.disabled)!=-1){this.lis.eq(a).removeClass("ui-state-disabled");e.disabled=d.grep(e.disabled,function(b){return b!=a});this._trigger("enable",null, +this._ui(this.anchors[a],this.panels[a]));return this}},disable:function(a){a=this._getIndex(a);var e=this.options;if(a!=e.selected){this.lis.eq(a).addClass("ui-state-disabled");e.disabled.push(a);e.disabled.sort();this._trigger("disable",null,this._ui(this.anchors[a],this.panels[a]))}return this},select:function(a){a=this._getIndex(a);if(a==-1)if(this.options.collapsible&&this.options.selected!=-1)a=this.options.selected;else return this;this.anchors.eq(a).trigger(this.options.event+".tabs");return this}, +load:function(a){a=this._getIndex(a);var e=this,b=this.options,c=this.anchors.eq(a)[0],h=d.data(c,"load.tabs");this.abort();if(!h||this.element.queue("tabs").length!==0&&d.data(c,"cache.tabs"))this.element.dequeue("tabs");else{this.lis.eq(a).addClass("ui-state-processing");if(b.spinner){var j=d("span",c);j.data("label.tabs",j.html()).html(b.spinner)}this.xhr=d.ajax(d.extend({},b.ajaxOptions,{url:h,success:function(k,n){d(e._sanitizeSelector(c.hash)).html(k);e._cleanup();b.cache&&d.data(c,"cache.tabs", +true);e._trigger("load",null,e._ui(e.anchors[a],e.panels[a]));try{b.ajaxOptions.success(k,n)}catch(m){}},error:function(k,n){e._cleanup();e._trigger("load",null,e._ui(e.anchors[a],e.panels[a]));try{b.ajaxOptions.error(k,n,a,c)}catch(m){}}}));e.element.dequeue("tabs");return this}},abort:function(){this.element.queue([]);this.panels.stop(false,true);this.element.queue("tabs",this.element.queue("tabs").splice(-2,2));if(this.xhr){this.xhr.abort();delete this.xhr}this._cleanup();return this},url:function(a, +e){this.anchors.eq(a).removeData("cache.tabs").data("load.tabs",e);return this},length:function(){return this.anchors.length}});d.extend(d.ui.tabs,{version:"1.8.5"});d.extend(d.ui.tabs.prototype,{rotation:null,rotate:function(a,e){var b=this,c=this.options,h=b._rotate||(b._rotate=function(j){clearTimeout(b.rotation);b.rotation=setTimeout(function(){var k=c.selected;b.select(++k')}function E(a,b){d.extend(a, +b);for(var c in b)if(b[c]==null||b[c]==G)a[c]=b[c];return a}d.extend(d.ui,{datepicker:{version:"1.8.5"}});var y=(new Date).getTime();d.extend(L.prototype,{markerClassName:"hasDatepicker",log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(a){E(this._defaults,a||{});return this},_attachDatepicker:function(a,b){var c=null;for(var e in this._defaults){var f=a.getAttribute("date:"+e);if(f){c=c||{};try{c[e]=eval(f)}catch(h){c[e]= +f}}}e=a.nodeName.toLowerCase();f=e=="div"||e=="span";if(!a.id){this.uuid+=1;a.id="dp"+this.uuid}var i=this._newInst(d(a),f);i.settings=d.extend({},b||{},c||{});if(e=="input")this._connectDatepicker(a,i);else f&&this._inlineDatepicker(a,i)},_newInst:function(a,b){return{id:a[0].id.replace(/([^A-Za-z0-9_])/g,"\\\\$1"),input:a,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:b,dpDiv:!b?this.dpDiv:d('
    ')}}, +_connectDatepicker:function(a,b){var c=d(a);b.append=d([]);b.trigger=d([]);if(!c.hasClass(this.markerClassName)){this._attachments(c,b);c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});this._autoSize(b);d.data(a,"datepicker",b)}},_attachments:function(a,b){var c=this._get(b,"appendText"),e=this._get(b,"isRTL");b.append&& +b.append.remove();if(c){b.append=d(''+c+"");a[e?"before":"after"](b.append)}a.unbind("focus",this._showDatepicker);b.trigger&&b.trigger.remove();c=this._get(b,"showOn");if(c=="focus"||c=="both")a.focus(this._showDatepicker);if(c=="button"||c=="both"){c=this._get(b,"buttonText");var f=this._get(b,"buttonImage");b.trigger=d(this._get(b,"buttonImageOnly")?d("").addClass(this._triggerClass).attr({src:f,alt:c,title:c}):d('').addClass(this._triggerClass).html(f== +""?c:d("").attr({src:f,alt:c,title:c})));a[e?"before":"after"](b.trigger);b.trigger.click(function(){d.datepicker._datepickerShowing&&d.datepicker._lastInput==a[0]?d.datepicker._hideDatepicker():d.datepicker._showDatepicker(a[0]);return false})}},_autoSize:function(a){if(this._get(a,"autoSize")&&!a.inline){var b=new Date(2009,11,20),c=this._get(a,"dateFormat");if(c.match(/[DM]/)){var e=function(f){for(var h=0,i=0,g=0;gh){h=f[g].length;i=g}return i};b.setMonth(e(this._get(a, +c.match(/MM/)?"monthNames":"monthNamesShort")));b.setDate(e(this._get(a,c.match(/DD/)?"dayNames":"dayNamesShort"))+20-b.getDay())}a.input.attr("size",this._formatDate(a,b).length)}},_inlineDatepicker:function(a,b){var c=d(a);if(!c.hasClass(this.markerClassName)){c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});d.data(a,"datepicker",b);this._setDate(b,this._getDefaultDate(b), +true);this._updateDatepicker(b);this._updateAlternate(b)}},_dialogDatepicker:function(a,b,c,e,f){a=this._dialogInst;if(!a){this.uuid+=1;this._dialogInput=d('');this._dialogInput.keydown(this._doKeyDown);d("body").append(this._dialogInput);a=this._dialogInst=this._newInst(this._dialogInput,false);a.settings={};d.data(this._dialogInput[0],"datepicker",a)}E(a.settings,e||{});b=b&&b.constructor== +Date?this._formatDate(a,b):b;this._dialogInput.val(b);this._pos=f?f.length?f:[f.pageX,f.pageY]:null;if(!this._pos)this._pos=[document.documentElement.clientWidth/2-100+(document.documentElement.scrollLeft||document.body.scrollLeft),document.documentElement.clientHeight/2-150+(document.documentElement.scrollTop||document.body.scrollTop)];this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px");a.settings.onSelect=c;this._inDialog=true;this.dpDiv.addClass(this._dialogClass);this._showDatepicker(this._dialogInput[0]); +d.blockUI&&d.blockUI(this.dpDiv);d.data(this._dialogInput[0],"datepicker",a);return this},_destroyDatepicker:function(a){var b=d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();d.removeData(a,"datepicker");if(e=="input"){c.append.remove();c.trigger.remove();b.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)}else if(e=="div"||e=="span")b.removeClass(this.markerClassName).empty()}}, +_enableDatepicker:function(a){var b=d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();if(e=="input"){a.disabled=false;c.trigger.filter("button").each(function(){this.disabled=false}).end().filter("img").css({opacity:"1.0",cursor:""})}else if(e=="div"||e=="span")b.children("."+this._inlineClass).children().removeClass("ui-state-disabled");this._disabledInputs=d.map(this._disabledInputs,function(f){return f==a?null:f})}},_disableDatepicker:function(a){var b= +d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();if(e=="input"){a.disabled=true;c.trigger.filter("button").each(function(){this.disabled=true}).end().filter("img").css({opacity:"0.5",cursor:"default"})}else if(e=="div"||e=="span")b.children("."+this._inlineClass).children().addClass("ui-state-disabled");this._disabledInputs=d.map(this._disabledInputs,function(f){return f==a?null:f});this._disabledInputs[this._disabledInputs.length]=a}},_isDisabledDatepicker:function(a){if(!a)return false; +for(var b=0;b-1}},_doKeyUp:function(a){a=d.datepicker._getInst(a.target);if(a.input.val()!=a.lastVal)try{if(d.datepicker.parseDate(d.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,d.datepicker._getFormatConfig(a))){d.datepicker._setDateFromField(a);d.datepicker._updateAlternate(a);d.datepicker._updateDatepicker(a)}}catch(b){d.datepicker.log(b)}return true},_showDatepicker:function(a){a=a.target|| +a;if(a.nodeName.toLowerCase()!="input")a=d("input",a.parentNode)[0];if(!(d.datepicker._isDisabledDatepicker(a)||d.datepicker._lastInput==a)){var b=d.datepicker._getInst(a);d.datepicker._curInst&&d.datepicker._curInst!=b&&d.datepicker._curInst.dpDiv.stop(true,true);var c=d.datepicker._get(b,"beforeShow");E(b.settings,c?c.apply(a,[a,b]):{});b.lastVal=null;d.datepicker._lastInput=a;d.datepicker._setDateFromField(b);if(d.datepicker._inDialog)a.value="";if(!d.datepicker._pos){d.datepicker._pos=d.datepicker._findPos(a); +d.datepicker._pos[1]+=a.offsetHeight}var e=false;d(a).parents().each(function(){e|=d(this).css("position")=="fixed";return!e});if(e&&d.browser.opera){d.datepicker._pos[0]-=document.documentElement.scrollLeft;d.datepicker._pos[1]-=document.documentElement.scrollTop}c={left:d.datepicker._pos[0],top:d.datepicker._pos[1]};d.datepicker._pos=null;b.dpDiv.css({position:"absolute",display:"block",top:"-1000px"});d.datepicker._updateDatepicker(b);c=d.datepicker._checkOffset(b,c,e);b.dpDiv.css({position:d.datepicker._inDialog&& +d.blockUI?"static":e?"fixed":"absolute",display:"none",left:c.left+"px",top:c.top+"px"});if(!b.inline){c=d.datepicker._get(b,"showAnim");var f=d.datepicker._get(b,"duration"),h=function(){d.datepicker._datepickerShowing=true;var i=d.datepicker._getBorders(b.dpDiv);b.dpDiv.find("iframe.ui-datepicker-cover").css({left:-i[0],top:-i[1],width:b.dpDiv.outerWidth(),height:b.dpDiv.outerHeight()})};b.dpDiv.zIndex(d(a).zIndex()+1);d.effects&&d.effects[c]?b.dpDiv.show(c,d.datepicker._get(b,"showOptions"),f, +h):b.dpDiv[c||"show"](c?f:null,h);if(!c||!f)h();b.input.is(":visible")&&!b.input.is(":disabled")&&b.input.focus();d.datepicker._curInst=b}}},_updateDatepicker:function(a){var b=this,c=d.datepicker._getBorders(a.dpDiv);a.dpDiv.empty().append(this._generateHTML(a)).find("iframe.ui-datepicker-cover").css({left:-c[0],top:-c[1],width:a.dpDiv.outerWidth(),height:a.dpDiv.outerHeight()}).end().find("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a").bind("mouseout",function(){d(this).removeClass("ui-state-hover"); +this.className.indexOf("ui-datepicker-prev")!=-1&&d(this).removeClass("ui-datepicker-prev-hover");this.className.indexOf("ui-datepicker-next")!=-1&&d(this).removeClass("ui-datepicker-next-hover")}).bind("mouseover",function(){if(!b._isDisabledDatepicker(a.inline?a.dpDiv.parent()[0]:a.input[0])){d(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover");d(this).addClass("ui-state-hover");this.className.indexOf("ui-datepicker-prev")!=-1&&d(this).addClass("ui-datepicker-prev-hover"); +this.className.indexOf("ui-datepicker-next")!=-1&&d(this).addClass("ui-datepicker-next-hover")}}).end().find("."+this._dayOverClass+" a").trigger("mouseover").end();c=this._getNumberOfMonths(a);var e=c[1];e>1?a.dpDiv.addClass("ui-datepicker-multi-"+e).css("width",17*e+"em"):a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("");a.dpDiv[(c[0]!=1||c[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi");a.dpDiv[(this._get(a,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"); +a==d.datepicker._curInst&&d.datepicker._datepickerShowing&&a.input&&a.input.is(":visible")&&!a.input.is(":disabled")&&a.input.focus()},_getBorders:function(a){var b=function(c){return{thin:1,medium:2,thick:3}[c]||c};return[parseFloat(b(a.css("border-left-width"))),parseFloat(b(a.css("border-top-width")))]},_checkOffset:function(a,b,c){var e=a.dpDiv.outerWidth(),f=a.dpDiv.outerHeight(),h=a.input?a.input.outerWidth():0,i=a.input?a.input.outerHeight():0,g=document.documentElement.clientWidth+d(document).scrollLeft(), +k=document.documentElement.clientHeight+d(document).scrollTop();b.left-=this._get(a,"isRTL")?e-h:0;b.left-=c&&b.left==a.input.offset().left?d(document).scrollLeft():0;b.top-=c&&b.top==a.input.offset().top+i?d(document).scrollTop():0;b.left-=Math.min(b.left,b.left+e>g&&g>e?Math.abs(b.left+e-g):0);b.top-=Math.min(b.top,b.top+f>k&&k>f?Math.abs(f+i):0);return b},_findPos:function(a){for(var b=this._get(this._getInst(a),"isRTL");a&&(a.type=="hidden"||a.nodeType!=1);)a=a[b?"previousSibling":"nextSibling"]; +a=d(a).offset();return[a.left,a.top]},_hideDatepicker:function(a){var b=this._curInst;if(!(!b||a&&b!=d.data(a,"datepicker")))if(this._datepickerShowing){a=this._get(b,"showAnim");var c=this._get(b,"duration"),e=function(){d.datepicker._tidyDialog(b);this._curInst=null};d.effects&&d.effects[a]?b.dpDiv.hide(a,d.datepicker._get(b,"showOptions"),c,e):b.dpDiv[a=="slideDown"?"slideUp":a=="fadeIn"?"fadeOut":"hide"](a?c:null,e);a||e();if(a=this._get(b,"onClose"))a.apply(b.input?b.input[0]:null,[b.input?b.input.val(): +"",b]);this._datepickerShowing=false;this._lastInput=null;if(this._inDialog){this._dialogInput.css({position:"absolute",left:"0",top:"-100px"});if(d.blockUI){d.unblockUI();d("body").append(this.dpDiv)}}this._inDialog=false}},_tidyDialog:function(a){a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(a){if(d.datepicker._curInst){a=d(a.target);a[0].id!=d.datepicker._mainDivId&&a.parents("#"+d.datepicker._mainDivId).length==0&&!a.hasClass(d.datepicker.markerClassName)&& +!a.hasClass(d.datepicker._triggerClass)&&d.datepicker._datepickerShowing&&!(d.datepicker._inDialog&&d.blockUI)&&d.datepicker._hideDatepicker()}},_adjustDate:function(a,b,c){a=d(a);var e=this._getInst(a[0]);if(!this._isDisabledDatepicker(a[0])){this._adjustInstDate(e,b+(c=="M"?this._get(e,"showCurrentAtPos"):0),c);this._updateDatepicker(e)}},_gotoToday:function(a){a=d(a);var b=this._getInst(a[0]);if(this._get(b,"gotoCurrent")&&b.currentDay){b.selectedDay=b.currentDay;b.drawMonth=b.selectedMonth=b.currentMonth; +b.drawYear=b.selectedYear=b.currentYear}else{var c=new Date;b.selectedDay=c.getDate();b.drawMonth=b.selectedMonth=c.getMonth();b.drawYear=b.selectedYear=c.getFullYear()}this._notifyChange(b);this._adjustDate(a)},_selectMonthYear:function(a,b,c){a=d(a);var e=this._getInst(a[0]);e._selectingMonthYear=false;e["selected"+(c=="M"?"Month":"Year")]=e["draw"+(c=="M"?"Month":"Year")]=parseInt(b.options[b.selectedIndex].value,10);this._notifyChange(e);this._adjustDate(a)},_clickMonthYear:function(a){var b= +this._getInst(d(a)[0]);b.input&&b._selectingMonthYear&&setTimeout(function(){b.input.focus()},0);b._selectingMonthYear=!b._selectingMonthYear},_selectDay:function(a,b,c,e){var f=d(a);if(!(d(e).hasClass(this._unselectableClass)||this._isDisabledDatepicker(f[0]))){f=this._getInst(f[0]);f.selectedDay=f.currentDay=d("a",e).html();f.selectedMonth=f.currentMonth=b;f.selectedYear=f.currentYear=c;this._selectDate(a,this._formatDate(f,f.currentDay,f.currentMonth,f.currentYear))}},_clearDate:function(a){a= +d(a);this._getInst(a[0]);this._selectDate(a,"")},_selectDate:function(a,b){a=this._getInst(d(a)[0]);b=b!=null?b:this._formatDate(a);a.input&&a.input.val(b);this._updateAlternate(a);var c=this._get(a,"onSelect");if(c)c.apply(a.input?a.input[0]:null,[b,a]);else a.input&&a.input.trigger("change");if(a.inline)this._updateDatepicker(a);else{this._hideDatepicker();this._lastInput=a.input[0];typeof a.input[0]!="object"&&a.input.focus();this._lastInput=null}},_updateAlternate:function(a){var b=this._get(a, +"altField");if(b){var c=this._get(a,"altFormat")||this._get(a,"dateFormat"),e=this._getDate(a),f=this.formatDate(c,e,this._getFormatConfig(a));d(b).each(function(){d(this).val(f)})}},noWeekends:function(a){a=a.getDay();return[a>0&&a<6,""]},iso8601Week:function(a){a=new Date(a.getTime());a.setDate(a.getDate()+4-(a.getDay()||7));var b=a.getTime();a.setMonth(0);a.setDate(1);return Math.floor(Math.round((b-a)/864E5)/7)+1},parseDate:function(a,b,c){if(a==null||b==null)throw"Invalid arguments";b=typeof b== +"object"?b.toString():b+"";if(b=="")return null;for(var e=(c?c.shortYearCutoff:null)||this._defaults.shortYearCutoff,f=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,h=(c?c.dayNames:null)||this._defaults.dayNames,i=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,g=(c?c.monthNames:null)||this._defaults.monthNames,k=c=-1,l=-1,u=-1,j=false,o=function(p){(p=z+1 +-1){k=1;l=u;do{e=this._getDaysInMonth(c,k-1);if(l<=e)break;k++;l-=e}while(1)}v=this._daylightSavingAdjust(new Date(c,k-1,l));if(v.getFullYear()!=c||v.getMonth()+1!=k||v.getDate()!=l)throw"Invalid date";return v},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24* +60*60*1E7,formatDate:function(a,b,c){if(!b)return"";var e=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,f=(c?c.dayNames:null)||this._defaults.dayNames,h=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort;c=(c?c.monthNames:null)||this._defaults.monthNames;var i=function(o){(o=j+112?a.getHours()+2:0);return a},_setDate:function(a,b,c){var e=!b,f=a.selectedMonth,h=a.selectedYear;b=this._restrictMinMax(a,this._determineDate(a,b,new Date));a.selectedDay=a.currentDay=b.getDate();a.drawMonth=a.selectedMonth=a.currentMonth=b.getMonth();a.drawYear=a.selectedYear=a.currentYear=b.getFullYear();if((f!=a.selectedMonth||h!=a.selectedYear)&&!c)this._notifyChange(a);this._adjustInstDate(a);if(a.input)a.input.val(e? +"":this._formatDate(a))},_getDate:function(a){return!a.currentYear||a.input&&a.input.val()==""?null:this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay))},_generateHTML:function(a){var b=new Date;b=this._daylightSavingAdjust(new Date(b.getFullYear(),b.getMonth(),b.getDate()));var c=this._get(a,"isRTL"),e=this._get(a,"showButtonPanel"),f=this._get(a,"hideIfNoPrevNext"),h=this._get(a,"navigationAsDateFormat"),i=this._getNumberOfMonths(a),g=this._get(a,"showCurrentAtPos"),k= +this._get(a,"stepMonths"),l=i[0]!=1||i[1]!=1,u=this._daylightSavingAdjust(!a.currentDay?new Date(9999,9,9):new Date(a.currentYear,a.currentMonth,a.currentDay)),j=this._getMinMaxDate(a,"min"),o=this._getMinMaxDate(a,"max");g=a.drawMonth-g;var m=a.drawYear;if(g<0){g+=12;m--}if(o){var n=this._daylightSavingAdjust(new Date(o.getFullYear(),o.getMonth()-i[0]*i[1]+1,o.getDate()));for(n=j&&nn;){g--;if(g<0){g=11;m--}}}a.drawMonth=g;a.drawYear=m;n=this._get(a, +"prevText");n=!h?n:this.formatDate(n,this._daylightSavingAdjust(new Date(m,g-k,1)),this._getFormatConfig(a));n=this._canAdjustMonth(a,-1,m,g)?''+n+"":f?"":''+ +n+"";var r=this._get(a,"nextText");r=!h?r:this.formatDate(r,this._daylightSavingAdjust(new Date(m,g+k,1)),this._getFormatConfig(a));f=this._canAdjustMonth(a,+1,m,g)?''+r+"":f?"":''+r+"";k=this._get(a,"currentText");r=this._get(a,"gotoCurrent")&&a.currentDay?u:b;k=!h?k:this.formatDate(k,r,this._getFormatConfig(a));h=!a.inline?'":"";e=e?'
    '+(c?h:"")+(this._isInRange(a,r)?'":"")+(c?"":h)+"
    ":"";h=parseInt(this._get(a,"firstDay"),10);h=isNaN(h)?0:h;k=this._get(a,"showWeek");r=this._get(a,"dayNames");this._get(a,"dayNamesShort");var s=this._get(a,"dayNamesMin"),z=this._get(a,"monthNames"),v=this._get(a,"monthNamesShort"),p=this._get(a,"beforeShowDay"),w=this._get(a,"showOtherMonths"),H=this._get(a,"selectOtherMonths");this._get(a,"calculateWeek");for(var M=this._getDefaultDate(a),I="",C=0;C1)switch(D){case 0:x+=" ui-datepicker-group-first";t=" ui-corner-"+(c?"right":"left");break;case i[1]-1:x+=" ui-datepicker-group-last";t=" ui-corner-"+(c?"left":"right");break;default:x+=" ui-datepicker-group-middle";t="";break}x+='">'}x+='
    '+(/all|left/.test(t)&&C==0?c? +f:n:"")+(/all|right/.test(t)&&C==0?c?n:f:"")+this._generateMonthYearHeader(a,g,m,j,o,C>0||D>0,z,v)+'
    ';var A=k?'":"";for(t=0;t<7;t++){var q=(t+h)%7;A+="=5?' class="ui-datepicker-week-end"':"")+'>'+s[q]+""}x+=A+"";A=this._getDaysInMonth(m,g);if(m==a.selectedYear&&g==a.selectedMonth)a.selectedDay=Math.min(a.selectedDay, +A);t=(this._getFirstDayOfMonth(m,g)-h+7)%7;A=l?6:Math.ceil((t+A)/7);q=this._daylightSavingAdjust(new Date(m,g,1-t));for(var O=0;O";var P=!k?"":'";for(t=0;t<7;t++){var F=p?p.apply(a.input?a.input[0]:null,[q]):[true,""],B=q.getMonth()!=g,K=B&&!H||!F[0]||j&&qo;P+='";q.setDate(q.getDate()+1);q=this._daylightSavingAdjust(q)}x+=P+""}g++;if(g>11){g=0;m++}x+="
    '+this._get(a,"weekHeader")+"
    '+this._get(a,"calculateWeek")(q)+""+(B&&!w?" ":K?''+q.getDate()+ +"":''+q.getDate()+"")+"
    "+(l?""+(i[0]>0&&D==i[1]-1?'
    ':""):"");N+=x}I+=N}I+=e+(d.browser.msie&&parseInt(d.browser.version,10)<7&&!a.inline?'': +"");a._keyEvent=false;return I},_generateMonthYearHeader:function(a,b,c,e,f,h,i,g){var k=this._get(a,"changeMonth"),l=this._get(a,"changeYear"),u=this._get(a,"showMonthAfterYear"),j='
    ',o="";if(h||!k)o+=''+i[b]+"";else{i=e&&e.getFullYear()==c;var m=f&&f.getFullYear()==c;o+='"}u||(j+=o+(h||!(k&&l)?" ":""));if(h||!l)j+=''+c+"";else{g=this._get(a,"yearRange").split(":");var r=(new Date).getFullYear();i=function(s){s=s.match(/c[+-].*/)?c+parseInt(s.substring(1),10):s.match(/[+-].*/)?r+parseInt(s,10):parseInt(s,10);return isNaN(s)?r:s};b=i(g[0]);g=Math.max(b, +i(g[1]||""));b=e?Math.max(b,e.getFullYear()):b;g=f?Math.min(g,f.getFullYear()):g;for(j+='"}j+=this._get(a,"yearSuffix");if(u)j+=(h||!(k&&l)?" ":"")+o;j+="
    ";return j},_adjustInstDate:function(a,b,c){var e= +a.drawYear+(c=="Y"?b:0),f=a.drawMonth+(c=="M"?b:0);b=Math.min(a.selectedDay,this._getDaysInMonth(e,f))+(c=="D"?b:0);e=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(e,f,b)));a.selectedDay=e.getDate();a.drawMonth=a.selectedMonth=e.getMonth();a.drawYear=a.selectedYear=e.getFullYear();if(c=="M"||c=="Y")this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");b=c&&ba?a:b},_notifyChange:function(a){var b=this._get(a, +"onChangeMonthYear");if(b)b.apply(a.input?a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){a=this._get(a,"numberOfMonths");return a==null?[1,1]:typeof a=="number"?[1,a]:a},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,e){var f=this._getNumberOfMonths(a); +c=this._daylightSavingAdjust(new Date(c,e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(this._getDaysInMonth(c.getFullYear(),c.getMonth()));return this._isInRange(a,c)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!a||b.getTime()<=a.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10);return{shortYearCutoff:b,dayNamesShort:this._get(a, +"dayNamesShort"),dayNames:this._get(a,"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,e){if(!b){a.currentDay=a.selectedDay;a.currentMonth=a.selectedMonth;a.currentYear=a.selectedYear}b=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(e,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),b,this._getFormatConfig(a))}});d.fn.datepicker= +function(a){if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b)); +return this.each(function(){typeof a=="string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new L;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.5";window["DP_jQuery_"+y]=d})(jQuery); +;/* + * jQuery UI Progressbar 1.8.5 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Progressbar + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + */ +(function(b,c){b.widget("ui.progressbar",{options:{value:0},min:0,max:100,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.max,"aria-valuenow":this._value()});this.valueDiv=b("
    ").appendTo(this.element);this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"); +this.valueDiv.remove();b.Widget.prototype.destroy.apply(this,arguments)},value:function(a){if(a===c)return this._value();this._setOption("value",a);return this},_setOption:function(a,d){if(a==="value"){this.options.value=d;this._refreshValue();this._trigger("change")}b.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;if(typeof a!=="number")a=0;return Math.min(this.max,Math.max(this.min,a))},_refreshValue:function(){var a=this.value();this.valueDiv.toggleClass("ui-corner-right", +a===this.max).width(a+"%");this.element.attr("aria-valuenow",a)}});b.extend(b.ui.progressbar,{version:"1.8.5"})})(jQuery); +;/* + * jQuery UI Effects 1.8.5 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/ + */ +jQuery.effects||function(f,j){function l(c){var a;if(c&&c.constructor==Array&&c.length==3)return c;if(a=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c))return[parseInt(a[1],10),parseInt(a[2],10),parseInt(a[3],10)];if(a=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c))return[parseFloat(a[1])*2.55,parseFloat(a[2])*2.55,parseFloat(a[3])*2.55];if(a=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c))return[parseInt(a[1], +16),parseInt(a[2],16),parseInt(a[3],16)];if(a=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c))return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16)];if(/rgba\(0, 0, 0, 0\)/.exec(c))return m.transparent;return m[f.trim(c).toLowerCase()]}function r(c,a){var b;do{b=f.curCSS(c,a);if(b!=""&&b!="transparent"||f.nodeName(c,"body"))break;a="backgroundColor"}while(c=c.parentNode);return l(b)}function n(){var c=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle, +a={},b,d;if(c&&c.length&&c[0]&&c[c[0]])for(var e=c.length;e--;){b=c[e];if(typeof c[b]=="string"){d=b.replace(/\-(\w)/g,function(g,h){return h.toUpperCase()});a[d]=c[b]}}else for(b in c)if(typeof c[b]==="string")a[b]=c[b];return a}function o(c){var a,b;for(a in c){b=c[a];if(b==null||f.isFunction(b)||a in s||/scrollbar/.test(a)||!/color/i.test(a)&&isNaN(parseFloat(b)))delete c[a]}return c}function t(c,a){var b={_:0},d;for(d in a)if(c[d]!=a[d])b[d]=a[d];return b}function k(c,a,b,d){if(typeof c=="object"){d= +a;b=null;a=c;c=a.effect}if(f.isFunction(a)){d=a;b=null;a={}}if(typeof a=="number"||f.fx.speeds[a]){d=b;b=a;a={}}if(f.isFunction(b)){d=b;b=null}a=a||{};b=b||a.duration;b=f.fx.off?0:typeof b=="number"?b:f.fx.speeds[b]||f.fx.speeds._default;d=d||a.complete;return[c,a,b,d]}f.effects={};f.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","color","outlineColor"],function(c,a){f.fx.step[a]=function(b){if(!b.colorInit){b.start=r(b.elem,a);b.end=l(b.end);b.colorInit= +true}b.elem.style[a]="rgb("+Math.max(Math.min(parseInt(b.pos*(b.end[0]-b.start[0])+b.start[0],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[1]-b.start[1])+b.start[1],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[2]-b.start[2])+b.start[2],10),255),0)+")"}});var m={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189, +183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255, +165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},p=["add","remove","toggle"],s={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};f.effects.animateClass=function(c,a,b,d){if(f.isFunction(b)){d=b;b=null}return this.each(function(){var e=f(this),g=e.attr("style")||" ",h=o(n.call(this)),q,u=e.attr("className");f.each(p,function(v, +i){c[i]&&e[i+"Class"](c[i])});q=o(n.call(this));e.attr("className",u);e.animate(t(h,q),a,b,function(){f.each(p,function(v,i){c[i]&&e[i+"Class"](c[i])});if(typeof e.attr("style")=="object"){e.attr("style").cssText="";e.attr("style").cssText=g}else e.attr("style",g);d&&d.apply(this,arguments)})})};f.fn.extend({_addClass:f.fn.addClass,addClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{add:c},a,b,d]):this._addClass(c)},_removeClass:f.fn.removeClass,removeClass:function(c,a,b,d){return a? +f.effects.animateClass.apply(this,[{remove:c},a,b,d]):this._removeClass(c)},_toggleClass:f.fn.toggleClass,toggleClass:function(c,a,b,d,e){return typeof a=="boolean"||a===j?b?f.effects.animateClass.apply(this,[a?{add:c}:{remove:c},b,d,e]):this._toggleClass(c,a):f.effects.animateClass.apply(this,[{toggle:c},a,b,d])},switchClass:function(c,a,b,d,e){return f.effects.animateClass.apply(this,[{add:a,remove:c},b,d,e])}});f.extend(f.effects,{version:"1.8.5",save:function(c,a){for(var b=0;b").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0});c.wrap(b);b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(d,e){a[e]=c.css(e);if(isNaN(parseInt(a[e],10)))a[e]="auto"}); +c.css({position:"relative",top:0,left:0})}return b.css(a).show()},removeWrapper:function(c){if(c.parent().is(".ui-effects-wrapper"))return c.parent().replaceWith(c);return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments);a={options:a[1],duration:a[2],callback:a[3]};var b=f.effects[c];return b&&!f.fx.off?b.call(this,a):this},_show:f.fn.show,show:function(c){if(!c|| +typeof c=="number"||f.fx.speeds[c]||!f.effects[c])return this._show.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||!f.effects[c])return this._hide.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||!f.effects[c]||typeof c== +"boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c, +a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/= +e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b,d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+ +b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c,a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/ +2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b,d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h").css({position:"absolute",visibility:"visible",left:-f*(h/d),top:-e*(i/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:h/d,height:i/c,left:g.left+f*(h/d)+(a.options.mode=="show"?(f-Math.floor(d/2))*(h/d):0),top:g.top+e*(i/c)+(a.options.mode=="show"?(e-Math.floor(c/2))*(i/c):0),opacity:a.options.mode=="show"?0:1}).animate({left:g.left+f*(h/d)+(a.options.mode=="show"?0:(f-Math.floor(d/2))*(h/d)),top:g.top+ +e*(i/c)+(a.options.mode=="show"?0:(e-Math.floor(c/2))*(i/c)),opacity:a.options.mode=="show"?1:0},a.duration||500);setTimeout(function(){a.options.mode=="show"?b.css({visibility:"visible"}):b.css({visibility:"visible"}).hide();a.callback&&a.callback.apply(b[0]);b.dequeue();j("div.ui-effects-explode").remove()},a.duration||500)})}})(jQuery); +;/* + * jQuery UI Effects Fade 1.8.5 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Fade + * + * Depends: + * jquery.effects.core.js + */ +(function(b){b.effects.fade=function(a){return this.queue(function(){var c=b(this),d=b.effects.setMode(c,a.options.mode||"hide");c.animate({opacity:d},{queue:false,duration:a.duration,easing:a.options.easing,complete:function(){a.callback&&a.callback.apply(this,arguments);c.dequeue()}})})}})(jQuery); +;/* + * jQuery UI Effects Fold 1.8.5 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Fold + * + * Depends: + * jquery.effects.core.js + */ +(function(c){c.effects.fold=function(a){return this.queue(function(){var b=c(this),j=["position","top","left"],d=c.effects.setMode(b,a.options.mode||"hide"),g=a.options.size||15,h=!!a.options.horizFirst,k=a.duration?a.duration/2:c.fx.speeds._default/2;c.effects.save(b,j);b.show();var e=c.effects.createWrapper(b).css({overflow:"hidden"}),f=d=="show"!=h,l=f?["width","height"]:["height","width"];f=f?[e.width(),e.height()]:[e.height(),e.width()];var i=/([0-9]+)%/.exec(g);if(i)g=parseInt(i[1],10)/100* +f[d=="hide"?0:1];if(d=="show")e.css(h?{height:0,width:g}:{height:g,width:0});h={};i={};h[l[0]]=d=="show"?f[0]:g;i[l[1]]=d=="show"?f[1]:0;e.animate(h,k,a.options.easing).animate(i,k,a.options.easing,function(){d=="hide"&&b.hide();c.effects.restore(b,j);c.effects.removeWrapper(b);a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery); +;/* + * jQuery UI Effects Highlight 1.8.5 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Highlight + * + * Depends: + * jquery.effects.core.js + */ +(function(b){b.effects.highlight=function(c){return this.queue(function(){var a=b(this),e=["backgroundImage","backgroundColor","opacity"],d=b.effects.setMode(a,c.options.mode||"show"),f={backgroundColor:a.css("backgroundColor")};if(d=="hide")f.opacity=0;b.effects.save(a,e);a.show().css({backgroundImage:"none",backgroundColor:c.options.color||"#ffff99"}).animate(f,{queue:false,duration:c.duration,easing:c.options.easing,complete:function(){d=="hide"&&a.hide();b.effects.restore(a,e);d=="show"&&!b.support.opacity&& +this.style.removeAttribute("filter");c.callback&&c.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery); +;/* + * jQuery UI Effects Pulsate 1.8.5 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Pulsate + * + * Depends: + * jquery.effects.core.js + */ +(function(d){d.effects.pulsate=function(a){return this.queue(function(){var b=d(this),c=d.effects.setMode(b,a.options.mode||"show");times=(a.options.times||5)*2-1;duration=a.duration?a.duration/2:d.fx.speeds._default/2;isVisible=b.is(":visible");animateTo=0;if(!isVisible){b.css("opacity",0).show();animateTo=1}if(c=="hide"&&isVisible||c=="show"&&!isVisible)times--;for(c=0;c').appendTo(document.body).addClass(a.options.className).css({top:d.top,left:d.left,height:b.innerHeight(),width:b.innerWidth(),position:"absolute"}).animate(c,a.duration,a.options.easing,function(){f.remove();a.callback&&a.callback.apply(b[0],arguments); +b.dequeue()})})}})(jQuery); +; \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/json2.js b/vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/json2.js new file mode 100644 index 00000000..39d8f370 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/json2.js @@ -0,0 +1,481 @@ +/* + http://www.JSON.org/json2.js + 2009-09-29 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, strict: false */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (!this.JSON) { + this.JSON = {}; +} + +(function () { + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function (key) { + + return isFinite(this.valueOf()) ? + this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function (key) { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + k = rep[i]; + if (typeof k === 'string') { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : + gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/. +test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); diff --git a/vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/manifest.json b/vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/manifest.json new file mode 100644 index 00000000..4e2830ca --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/manifest.json @@ -0,0 +1,25 @@ +{ + "name": "Clip It Good", + "version": "0.1", + "description": "Save webpage clippings and images to your Picasa Web Albums.", + "background_page": "background.html", + "options_page": "options.html", + "permissions": [ + "tabs", + "http://*/*", + "https://*/*", + "https://www.google.com/accounts/OAuthGetRequestToken", + "https://www.google.com/accounts/OAuthAuthorizeToken", + "https://www.google.com/accounts/OAuthGetAccessToken", + "https://picasaweb.google.com/data/*", + "contextMenus" + ], + "page_action": { + "default_icon": "icon19.png" + }, + "minimum_chrome_version": "9.0", + "icons": { + "48": "icon48.png", + "128": "icon128.png" + } +} diff --git a/vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/options.html b/vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/options.html new file mode 100644 index 00000000..ce4e00ee --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/chrome/clip-it-good/options.html @@ -0,0 +1,314 @@ + + + + Clip It Good: Configure options + + + + + + + + + + + + + +

    Clip It Good: Configure options

    + +
    + Loading... +
    + +
    + + +
    + +

    Security information

    +
    +Connecting an album to Clip It Good will require giving this extension permission to access your albums even when you are not logged into your account. At any time you may revoke access to this extension by using the authorized access control panel for each photo hosting provider: Google Accounts +
    + +

    About

    +
    + +

    + Brett Slatkin, ©2010 +
    + Email +

    +

    + Extension and source licensed under the + Apache License, + Version 2.0. Uses jQuery UI (MIT/GPL), + json2 parser (public domain), + Fred Palmer's Base64 + (BSD compat), and Jeff + Mott's SHA1 (BSD compat). +

    +
    + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/clients/curl/example.sh b/vendor/github.com/camlistore/camlistore/clients/curl/example.sh new file mode 100755 index 00000000..50ed2c52 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/curl/example.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Example client accesses to blob server using curl. + +# Configuration variables here: +BSHOST=localhost:3179/bs +BSUSER=user +BSPASS=foo + +# Shorter name for curl auth param: +AUTH=$BSUSER:$BSPASS + +# Stat -- 200 response +curl -u $AUTH -d camliversion=1 http://$BSHOST/camli/stat + +# Upload -- 200 response +curl -u $AUTH -v -L \ + -F sha1-126249fd8c18cbb5312a5705746a2af87fba9538=@./test_data.txt \ + # + +# Put with bad blob_ref parameter -- 400 response +curl -v -L \ + -F sha1-22a7fdd575f4c3e7caa3a55cc83db8b8a6714f0f=@./test_data.txt \ + # + +# Get present -- the blob +curl -u $AUTH -v http://$BSHOST/camli/sha1-126249fd8c18cbb5312a5705746a2af87fba9538 + +# Get missing -- 404 +curl -u $AUTH -v http://$BSHOST/camli/sha1-22a7fdd575f4c3e7caa3a55cc83db8b8a6714f0f + +# Check present -- 200 with only headers +curl -u $AUTH -I http://$BSHOST/camli/sha1-126249fd8c18cbb5312a5705746a2af87fba9538 + +# Check missing -- 404 with empty list response +curl -I http://$BSHOST/camli/sha1-22a7fdd575f4c3e7caa3a55cc83db8b8a6714f0f + +# List -- 200 with list of blobs (just one) +curl -v -u $AUTH http://$BSHOST/camli/enumerate-blobs?limit=1 + +# List offset -- 200 with list of no blobs +curl -v -u $AUTH http://$BSHOST/camli/enumerate-blobs?after=sha1-126249fd8c18cbb5312a5705746a2af87fba9538 diff --git a/vendor/github.com/camlistore/camlistore/clients/curl/test_data.txt b/vendor/github.com/camlistore/camlistore/clients/curl/test_data.txt new file mode 100644 index 00000000..a26826a5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/curl/test_data.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque at tortor in tellus accumsan euismod. Quisque scelerisque velit vel nisi ornare lacinia. Vivamus viverra eleifend congue. Maecenas dolor magna, rhoncus vitae fermentum id, convallis id. diff --git a/vendor/github.com/camlistore/camlistore/clients/curl/upload-file.pl b/vendor/github.com/camlistore/camlistore/clients/curl/upload-file.pl new file mode 100755 index 00000000..bee85afd --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/curl/upload-file.pl @@ -0,0 +1,21 @@ +#!/usr/bin/perl +# +# Lame upload script for development testing only. Doesn't do the +# stat step and hard-codes the Go server's upload path (not +# conformant to spec). + +use strict; +my $file = shift or die + "Usage: upload-file.pl: "; +-r $file or die "$file isn't readable."; +-f $file or die "$file isn't a file."; + +die "bogus filename" if $file =~ /[ <>&\!]/; + +my $sha1 = `sha1sum $file`; +chomp $sha1; +$sha1 =~ s/\s.+//; + +system("curl", "-u", "foo:foo", "-F", "sha1-$sha1=\@$file", + "http://127.0.0.1:3179/bs/camli/upload") and die "upload failed."; +print "Uploaded http://127.0.0.1:3179/bs/camli/sha1-$sha1\n"; diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/.gitignore b/vendor/github.com/camlistore/camlistore/clients/ios-objc/.gitignore new file mode 100644 index 00000000..6a25330b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/.gitignore @@ -0,0 +1 @@ +Pods \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/Podfile b/vendor/github.com/camlistore/camlistore/clients/ios-objc/Podfile new file mode 100644 index 00000000..c3e4a8fe --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/Podfile @@ -0,0 +1,5 @@ +platform :ios, '7.0' + +pod 'HockeySDK', '~> 3.5.0' +pod 'SSKeychain', '~> 1.2.1' +pod 'BugshotKit', :podspec => 'https://raw.github.com/marcoarment/BugshotKit/e4031a8e5a863939f9c91f0d86352cba07d82d79/BugshotKit.podspec' \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/Podfile.lock b/vendor/github.com/camlistore/camlistore/clients/ios-objc/Podfile.lock new file mode 100644 index 00000000..d12a2f27 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/Podfile.lock @@ -0,0 +1,20 @@ +PODS: + - BugshotKit (0.1.0) + - HockeySDK (3.5.2) + - SSKeychain (1.2.1) + +DEPENDENCIES: + - BugshotKit (from `https://raw.github.com/marcoarment/BugshotKit/e4031a8e5a863939f9c91f0d86352cba07d82d79/BugshotKit.podspec`) + - HockeySDK (~> 3.5.0) + - SSKeychain (~> 1.2.1) + +EXTERNAL SOURCES: + BugshotKit: + :podspec: https://raw.github.com/marcoarment/BugshotKit/e4031a8e5a863939f9c91f0d86352cba07d82d79/BugshotKit.podspec + +SPEC CHECKSUMS: + BugshotKit: c94c3c580d179b034791a26a8e1196e72c3c6312 + HockeySDK: 203d3af93c2a229bfb528ff085201670ff65e1cf + SSKeychain: d18926838c2e7cd342e2a49e9f869858e49f035a + +COCOAPODS: 0.29.0 diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/Readme.md b/vendor/github.com/camlistore/camlistore/clients/ios-objc/Readme.md new file mode 100644 index 00000000..6bb95a52 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/Readme.md @@ -0,0 +1,5 @@ +== SETUP + +Use the podfile to setup the (ignored) dependencies, `pod install` should be all you need, then open the .xcworkspace file in xcode. + +We use clang-format in the form of the ClangFormat-Xcode plugin (https://github.com/travisjeffery/ClangFormat-Xcode) for style consistency. Please set your formatting tool to use the WebKit style (http://www.webkit.org/coding/coding-style.html). \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup.xcodeproj/project.pbxproj b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup.xcodeproj/project.pbxproj new file mode 100644 index 00000000..abee3320 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup.xcodeproj/project.pbxproj @@ -0,0 +1,617 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 2051975F9A3B42668D11045C /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C6265C93000C47B5BFE9BB61 /* libPods.a */; }; + D075B282184944330054FED3 /* LACamliUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = D075B281184944330054FED3 /* LACamliUtil.m */; }; + D075B28518494DB20054FED3 /* LACamliUploadOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = D075B28418494DB20054FED3 /* LACamliUploadOperation.m */; }; + D078FB0918726D1300F2ABF7 /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D078FB0818726D1300F2ABF7 /* CoreText.framework */; }; + D078FB0B18726D1C00F2ABF7 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D078FB0A18726D1C00F2ABF7 /* QuartzCore.framework */; }; + D078FB0D18726D2100F2ABF7 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D078FB0C18726D2100F2ABF7 /* Security.framework */; }; + D078FB0F18726D2900F2ABF7 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D078FB0E18726D2900F2ABF7 /* SystemConfiguration.framework */; }; + D08F9118187B417E006F6B8D /* UploadStatusCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D08F9117187B417E006F6B8D /* UploadStatusCell.m */; }; + D08F911B187B4189006F6B8D /* UploadTaskCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D08F911A187B4189006F6B8D /* UploadTaskCell.m */; }; + D095AE131814AF10008163F2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D095AE121814AF10008163F2 /* Foundation.framework */; }; + D095AE151814AF10008163F2 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D095AE141814AF10008163F2 /* CoreGraphics.framework */; }; + D095AE171814AF10008163F2 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D095AE161814AF10008163F2 /* UIKit.framework */; }; + D095AE1D1814AF10008163F2 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D095AE1B1814AF10008163F2 /* InfoPlist.strings */; }; + D095AE1F1814AF10008163F2 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D095AE1E1814AF10008163F2 /* main.m */; }; + D095AE231814AF10008163F2 /* LAAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D095AE221814AF10008163F2 /* LAAppDelegate.m */; }; + D095AE261814AF10008163F2 /* Main_iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D095AE241814AF10008163F2 /* Main_iPhone.storyboard */; }; + D095AE2C1814AF10008163F2 /* LAViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D095AE2B1814AF10008163F2 /* LAViewController.m */; }; + D095AE2E1814AF10008163F2 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D095AE2D1814AF10008163F2 /* Images.xcassets */; }; + D095AE351814AF10008163F2 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D095AE341814AF10008163F2 /* XCTest.framework */; }; + D095AE361814AF10008163F2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D095AE121814AF10008163F2 /* Foundation.framework */; }; + D095AE371814AF10008163F2 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D095AE161814AF10008163F2 /* UIKit.framework */; }; + D095AE3F1814AF10008163F2 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D095AE3D1814AF10008163F2 /* InfoPlist.strings */; }; + D095AE411814AF10008163F2 /* photobackupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D095AE401814AF10008163F2 /* photobackupTests.m */; }; + D095AE501814B1B9008163F2 /* LACamliFile.m in Sources */ = {isa = PBXBuildFile; fileRef = D095AE4C1814B1B9008163F2 /* LACamliFile.m */; }; + D095AE511814B1B9008163F2 /* LACamliClient.m in Sources */ = {isa = PBXBuildFile; fileRef = D095AE4E1814B1B9008163F2 /* LACamliClient.m */; }; + D0D45EA1185FE2BE00EBC0A2 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D0D45EA0185FE2BE00EBC0A2 /* SettingsViewController.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + D095AE381814AF10008163F2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D095AE071814AF10008163F2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D095AE0E1814AF10008163F2; + remoteInfo = photobackup; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 208E1D70286D49129C896012 /* Pods.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.xcconfig; path = Pods/Pods.xcconfig; sourceTree = ""; }; + C6265C93000C47B5BFE9BB61 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + D075B280184944330054FED3 /* LACamliUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LACamliUtil.h; sourceTree = ""; }; + D075B281184944330054FED3 /* LACamliUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LACamliUtil.m; sourceTree = ""; }; + D075B28318494DB20054FED3 /* LACamliUploadOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LACamliUploadOperation.h; sourceTree = ""; }; + D075B28418494DB20054FED3 /* LACamliUploadOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LACamliUploadOperation.m; sourceTree = ""; }; + D078FB0818726D1300F2ABF7 /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; + D078FB0A18726D1C00F2ABF7 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + D078FB0C18726D2100F2ABF7 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + D078FB0E18726D2900F2ABF7 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + D08F9116187B417E006F6B8D /* UploadStatusCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UploadStatusCell.h; sourceTree = ""; }; + D08F9117187B417E006F6B8D /* UploadStatusCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UploadStatusCell.m; sourceTree = ""; }; + D08F9119187B4189006F6B8D /* UploadTaskCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UploadTaskCell.h; sourceTree = ""; }; + D08F911A187B4189006F6B8D /* UploadTaskCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UploadTaskCell.m; sourceTree = ""; }; + D095AE0F1814AF10008163F2 /* photobackup.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = photobackup.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D095AE121814AF10008163F2 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + D095AE141814AF10008163F2 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + D095AE161814AF10008163F2 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + D095AE1A1814AF10008163F2 /* photobackup-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "photobackup-Info.plist"; sourceTree = ""; }; + D095AE1C1814AF10008163F2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + D095AE1E1814AF10008163F2 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + D095AE201814AF10008163F2 /* photobackup-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "photobackup-Prefix.pch"; sourceTree = ""; }; + D095AE211814AF10008163F2 /* LAAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LAAppDelegate.h; sourceTree = ""; }; + D095AE221814AF10008163F2 /* LAAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LAAppDelegate.m; sourceTree = ""; }; + D095AE251814AF10008163F2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main_iPhone.storyboard; sourceTree = ""; }; + D095AE2A1814AF10008163F2 /* LAViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LAViewController.h; sourceTree = ""; }; + D095AE2B1814AF10008163F2 /* LAViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LAViewController.m; sourceTree = ""; }; + D095AE2D1814AF10008163F2 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + D095AE331814AF10008163F2 /* photobackupTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = photobackupTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D095AE341814AF10008163F2 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + D095AE3C1814AF10008163F2 /* photobackupTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "photobackupTests-Info.plist"; sourceTree = ""; }; + D095AE3E1814AF10008163F2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + D095AE401814AF10008163F2 /* photobackupTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = photobackupTests.m; sourceTree = ""; }; + D095AE4B1814B1B9008163F2 /* LACamliFile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LACamliFile.h; sourceTree = ""; }; + D095AE4C1814B1B9008163F2 /* LACamliFile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LACamliFile.m; sourceTree = ""; }; + D095AE4D1814B1B9008163F2 /* LACamliClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LACamliClient.h; sourceTree = ""; }; + D095AE4E1814B1B9008163F2 /* LACamliClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LACamliClient.m; sourceTree = ""; }; + D0D45E9F185FE2BE00EBC0A2 /* SettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsViewController.h; sourceTree = ""; }; + D0D45EA0185FE2BE00EBC0A2 /* SettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsViewController.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D095AE0C1814AF10008163F2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D078FB0F18726D2900F2ABF7 /* SystemConfiguration.framework in Frameworks */, + D078FB0D18726D2100F2ABF7 /* Security.framework in Frameworks */, + D078FB0B18726D1C00F2ABF7 /* QuartzCore.framework in Frameworks */, + D078FB0918726D1300F2ABF7 /* CoreText.framework in Frameworks */, + D095AE151814AF10008163F2 /* CoreGraphics.framework in Frameworks */, + D095AE171814AF10008163F2 /* UIKit.framework in Frameworks */, + D095AE131814AF10008163F2 /* Foundation.framework in Frameworks */, + 2051975F9A3B42668D11045C /* libPods.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D095AE301814AF10008163F2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D095AE351814AF10008163F2 /* XCTest.framework in Frameworks */, + D095AE371814AF10008163F2 /* UIKit.framework in Frameworks */, + D095AE361814AF10008163F2 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D08F911C187B4190006F6B8D /* Main UI */ = { + isa = PBXGroup; + children = ( + D095AE241814AF10008163F2 /* Main_iPhone.storyboard */, + D095AE2A1814AF10008163F2 /* LAViewController.h */, + D095AE2B1814AF10008163F2 /* LAViewController.m */, + D08F9116187B417E006F6B8D /* UploadStatusCell.h */, + D08F9117187B417E006F6B8D /* UploadStatusCell.m */, + D08F9119187B4189006F6B8D /* UploadTaskCell.h */, + D08F911A187B4189006F6B8D /* UploadTaskCell.m */, + ); + name = "Main UI"; + sourceTree = ""; + }; + D095AE061814AF10008163F2 = { + isa = PBXGroup; + children = ( + D095AE181814AF10008163F2 /* photobackup */, + D095AE3A1814AF10008163F2 /* photobackupTests */, + D095AE111814AF10008163F2 /* Frameworks */, + D095AE101814AF10008163F2 /* Products */, + 208E1D70286D49129C896012 /* Pods.xcconfig */, + ); + sourceTree = ""; + }; + D095AE101814AF10008163F2 /* Products */ = { + isa = PBXGroup; + children = ( + D095AE0F1814AF10008163F2 /* photobackup.app */, + D095AE331814AF10008163F2 /* photobackupTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + D095AE111814AF10008163F2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D078FB0E18726D2900F2ABF7 /* SystemConfiguration.framework */, + D078FB0C18726D2100F2ABF7 /* Security.framework */, + D078FB0A18726D1C00F2ABF7 /* QuartzCore.framework */, + D078FB0818726D1300F2ABF7 /* CoreText.framework */, + D095AE121814AF10008163F2 /* Foundation.framework */, + D095AE141814AF10008163F2 /* CoreGraphics.framework */, + D095AE161814AF10008163F2 /* UIKit.framework */, + D095AE341814AF10008163F2 /* XCTest.framework */, + C6265C93000C47B5BFE9BB61 /* libPods.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + D095AE181814AF10008163F2 /* photobackup */ = { + isa = PBXGroup; + children = ( + D095AE4A1814B1B9008163F2 /* LACamliClient */, + D095AE211814AF10008163F2 /* LAAppDelegate.h */, + D095AE221814AF10008163F2 /* LAAppDelegate.m */, + D08F911C187B4190006F6B8D /* Main UI */, + D0D45E9F185FE2BE00EBC0A2 /* SettingsViewController.h */, + D0D45EA0185FE2BE00EBC0A2 /* SettingsViewController.m */, + D095AE2D1814AF10008163F2 /* Images.xcassets */, + D095AE191814AF10008163F2 /* Supporting Files */, + ); + path = photobackup; + sourceTree = ""; + }; + D095AE191814AF10008163F2 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D095AE1A1814AF10008163F2 /* photobackup-Info.plist */, + D095AE1B1814AF10008163F2 /* InfoPlist.strings */, + D095AE1E1814AF10008163F2 /* main.m */, + D095AE201814AF10008163F2 /* photobackup-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D095AE3A1814AF10008163F2 /* photobackupTests */ = { + isa = PBXGroup; + children = ( + D095AE401814AF10008163F2 /* photobackupTests.m */, + D095AE3B1814AF10008163F2 /* Supporting Files */, + ); + path = photobackupTests; + sourceTree = ""; + }; + D095AE3B1814AF10008163F2 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D095AE3C1814AF10008163F2 /* photobackupTests-Info.plist */, + D095AE3D1814AF10008163F2 /* InfoPlist.strings */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D095AE4A1814B1B9008163F2 /* LACamliClient */ = { + isa = PBXGroup; + children = ( + D095AE4B1814B1B9008163F2 /* LACamliFile.h */, + D095AE4C1814B1B9008163F2 /* LACamliFile.m */, + D095AE4D1814B1B9008163F2 /* LACamliClient.h */, + D095AE4E1814B1B9008163F2 /* LACamliClient.m */, + D075B280184944330054FED3 /* LACamliUtil.h */, + D075B281184944330054FED3 /* LACamliUtil.m */, + D075B28318494DB20054FED3 /* LACamliUploadOperation.h */, + D075B28418494DB20054FED3 /* LACamliUploadOperation.m */, + ); + path = LACamliClient; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + D095AE0E1814AF10008163F2 /* photobackup */ = { + isa = PBXNativeTarget; + buildConfigurationList = D095AE441814AF10008163F2 /* Build configuration list for PBXNativeTarget "photobackup" */; + buildPhases = ( + 85BE1708753D47A8B10D430C /* Check Pods Manifest.lock */, + D095AE0B1814AF10008163F2 /* Sources */, + D095AE0C1814AF10008163F2 /* Frameworks */, + D095AE0D1814AF10008163F2 /* Resources */, + A368DE813E1349B0810D9274 /* Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = photobackup; + productName = photobackup; + productReference = D095AE0F1814AF10008163F2 /* photobackup.app */; + productType = "com.apple.product-type.application"; + }; + D095AE321814AF10008163F2 /* photobackupTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D095AE471814AF10008163F2 /* Build configuration list for PBXNativeTarget "photobackupTests" */; + buildPhases = ( + D095AE2F1814AF10008163F2 /* Sources */, + D095AE301814AF10008163F2 /* Frameworks */, + D095AE311814AF10008163F2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D095AE391814AF10008163F2 /* PBXTargetDependency */, + ); + name = photobackupTests; + productName = photobackupTests; + productReference = D095AE331814AF10008163F2 /* photobackupTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D095AE071814AF10008163F2 /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = LA; + LastUpgradeCheck = 0500; + ORGANIZATIONNAME = "Nick O'Neill"; + TargetAttributes = { + D095AE0E1814AF10008163F2 = { + DevelopmentTeam = H6S4PTUWAA; + SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; + }; + }; + D095AE321814AF10008163F2 = { + TestTargetID = D095AE0E1814AF10008163F2; + }; + }; + }; + buildConfigurationList = D095AE0A1814AF10008163F2 /* Build configuration list for PBXProject "photobackup" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D095AE061814AF10008163F2; + productRefGroup = D095AE101814AF10008163F2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D095AE0E1814AF10008163F2 /* photobackup */, + D095AE321814AF10008163F2 /* photobackupTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D095AE0D1814AF10008163F2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D095AE2E1814AF10008163F2 /* Images.xcassets in Resources */, + D095AE261814AF10008163F2 /* Main_iPhone.storyboard in Resources */, + D095AE1D1814AF10008163F2 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D095AE311814AF10008163F2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D095AE3F1814AF10008163F2 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 85BE1708753D47A8B10D430C /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + A368DE813E1349B0810D9274 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Pods-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D095AE0B1814AF10008163F2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D095AE1F1814AF10008163F2 /* main.m in Sources */, + D095AE511814B1B9008163F2 /* LACamliClient.m in Sources */, + D08F911B187B4189006F6B8D /* UploadTaskCell.m in Sources */, + D095AE501814B1B9008163F2 /* LACamliFile.m in Sources */, + D075B282184944330054FED3 /* LACamliUtil.m in Sources */, + D095AE231814AF10008163F2 /* LAAppDelegate.m in Sources */, + D0D45EA1185FE2BE00EBC0A2 /* SettingsViewController.m in Sources */, + D095AE2C1814AF10008163F2 /* LAViewController.m in Sources */, + D075B28518494DB20054FED3 /* LACamliUploadOperation.m in Sources */, + D08F9118187B417E006F6B8D /* UploadStatusCell.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D095AE2F1814AF10008163F2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D095AE411814AF10008163F2 /* photobackupTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + D095AE391814AF10008163F2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D095AE0E1814AF10008163F2 /* photobackup */; + targetProxy = D095AE381814AF10008163F2 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + D095AE1B1814AF10008163F2 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D095AE1C1814AF10008163F2 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D095AE241814AF10008163F2 /* Main_iPhone.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D095AE251814AF10008163F2 /* Base */, + ); + name = Main_iPhone.storyboard; + sourceTree = ""; + }; + D095AE3D1814AF10008163F2 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D095AE3E1814AF10008163F2 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + D095AE421814AF10008163F2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D095AE431814AF10008163F2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + D095AE451814AF10008163F2 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 208E1D70286D49129C896012 /* Pods.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "photobackup/photobackup-Prefix.pch"; + INFOPLIST_FILE = "photobackup/photobackup-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + TARGETED_DEVICE_FAMILY = 1; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + D095AE461814AF10008163F2 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 208E1D70286D49129C896012 /* Pods.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CODE_SIGN_IDENTITY = "iPhone Distribution: Launch Apps LLC (H6S4PTUWAA)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: Launch Apps LLC (H6S4PTUWAA)"; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "photobackup/photobackup-Prefix.pch"; + INFOPLIST_FILE = "photobackup/photobackup-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + TARGETED_DEVICE_FAMILY = 1; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + D095AE481814AF10008163F2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/photobackup.app/photobackup"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "photobackup/photobackup-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "photobackupTests/photobackupTests-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + D095AE491814AF10008163F2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/photobackup.app/photobackup"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "photobackup/photobackup-Prefix.pch"; + INFOPLIST_FILE = "photobackupTests/photobackupTests-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D095AE0A1814AF10008163F2 /* Build configuration list for PBXProject "photobackup" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D095AE421814AF10008163F2 /* Debug */, + D095AE431814AF10008163F2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D095AE441814AF10008163F2 /* Build configuration list for PBXNativeTarget "photobackup" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D095AE451814AF10008163F2 /* Debug */, + D095AE461814AF10008163F2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D095AE471814AF10008163F2 /* Build configuration list for PBXNativeTarget "photobackupTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D095AE481814AF10008163F2 /* Debug */, + D095AE491814AF10008163F2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D095AE071814AF10008163F2 /* Project object */; +} diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup.xcworkspace/contents.xcworkspacedata b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..13577cec --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup.xcworkspace/contents.xcworkspacedata @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup.xcworkspace/xcshareddata/photobackup.xccheckout b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup.xcworkspace/xcshareddata/photobackup.xccheckout new file mode 100644 index 00000000..ecee56d0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup.xcworkspace/xcshareddata/photobackup.xccheckout @@ -0,0 +1,41 @@ + + + + + IDESourceControlProjectFavoriteDictionaryKey + + IDESourceControlProjectIdentifier + 79DD2B70-7262-4160-804F-F637A84D4765 + IDESourceControlProjectName + photobackup + IDESourceControlProjectOriginsDictionary + + 1AD6A32F-218B-40FF-A64E-FD2FF680305E + https://camlistore.googlesource.com/camlistore + + IDESourceControlProjectPath + clients/ios-objc/photobackup.xcworkspace + IDESourceControlProjectRelativeInstallPathDictionary + + 1AD6A32F-218B-40FF-A64E-FD2FF680305E + ../../.. + + IDESourceControlProjectURL + https://camlistore.googlesource.com/camlistore + IDESourceControlProjectVersion + 110 + IDESourceControlProjectWCCIdentifier + 1AD6A32F-218B-40FF-A64E-FD2FF680305E + IDESourceControlProjectWCConfigurations + + + IDESourceControlRepositoryExtensionIdentifierKey + public.vcs.git + IDESourceControlWCCIdentifierKey + 1AD6A32F-218B-40FF-A64E-FD2FF680305E + IDESourceControlWCCName + camlistore + + + + diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Base.lproj/Main_iPad.storyboard b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Base.lproj/Main_iPad.storyboard new file mode 100644 index 00000000..7e2236e4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Base.lproj/Main_iPad.storyboard @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Base.lproj/Main_iPhone.storyboard b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Base.lproj/Main_iPhone.storyboard new file mode 100644 index 00000000..c097bde4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Base.lproj/Main_iPhone.storyboard @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ad75e95c1489eed98c0d7743c0d6d455932bb2f6 GIT binary patch literal 12617 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..54163755868e8e54236e9d796cbf30a9d060408d GIT binary patch literal 17204 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/Images.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0f782faac2032a241f4a2763c92484e73ee34918 GIT binary patch literal 25848 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 z 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

    !rS!H@!y60L7I7hYJVYS(D*> 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

    5qh0B;8kbA7N0iJ(ZS4oyZJm<26U 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%0A^JA1sHPzs!y8iq$$Qnn(! zmSN;s5A_;=g}#6z5;g6CF}IG0p?9(C3U>wFYgr7J*^xf&h%Q|Q-O5g^ zm;Uw{ge>JU0~sAk~e#+T3e#%c?4z^o8j~ZkszkpnI!|!a4#dEcZoq^D5NE> z)ynTxuVAoxlm4Z&;SD^tOqQcl0N638v~7AFH2q~u@15 z$pU^^7i;=hnI7sKhf3H}t5-TxNo1_x%DRA=%Mf9U=73bxI}j(DEYM_(6SVe7EUD?0 zMI{g;OT-ArjMFrAW4x$~Zy2g@CF$kJ2()~dRC&27)_1?U`j}$DH~ZEKOr1)$4%et4mOG zi@_Tz<2LR2aMM-}iA;?aoS{%?P_Yq3kknK$piy3$L)*I9s}?Cf#t?Y>Ke%uAFaRIp zZOfJ|&;Fk8DgAbG@~8sv`gQAH^J}kL6*2hpx2>5z&~nXG=#h7O(}9Lbqy=c1`M0_f zG!DUwi^MQ>h}dD4M4>i}T~LbZ70@zRegAbZxdkH_7Mow%N;UC&W&JKD606)o3uw5B zM`xx^YjePu7!3+uO(n3JN^(<_I!G-}OKQNDR3{Nx?XtwdTv*2iU^K@koqwa9}(Yom%XT zhalllFBg$H8j&^w06KiM~%TVGK)YH;R6}axKh4q~-gu$E6 zX-E=-fZHO%Z0U7=xHYthb^U-vB`m!wil~;*ahGrxu`%vaQl+)TF}}K3^%>V$j}``V z2D3ClJ5z(-q@Vs4$zhyQ6QTs$I+qXK8LARPjb<~eTt%B? zSsxFRO~WY5q^Tl{T=@F4)(NTZcr7}y6!fhUZg!}@({5A)Em42IJ02VxhRkDI{=W33 zFKu|jp51$nE&y-pees4jz3~~(dgcm08`s`EQ2{fK&kyD$ltbMRZUn}qA%s$2$*`dN z82hdku>%`Zf3cM2P;L~x1Ze~kAXX>>YF0(By1JTTsM8A79qjL%+A#|eArD~n(44+c&OkX{~?1abkW2>zGVb({59)atdT7hC->0dQ|*Hp%=wW z1`=5&i$o2g8TU+lPjtA2S$3!u1;jSL#9(i3MFRXe1aipGkKr(azq|Mqoue>v-Tvr&hoSr^cy)OCXec)fE`6B~hbYB{03DTI`wP3^4$PQzuTd zNeq1vN_Z@KWR^raSejo-TPpPCk5Sm!)!W*jP;9NK-HVZ?P)txMN4ZEl;*sSJcY)Ho z6N9|Ani);&>o-Z#XbkL7mhWtYA9`@g^WIbZ(L4G;VE z$HvC03b?zq+wFI}^PN{b?ur$ALcX@Ey<=y)d&PAkScD`jrdo|Lk1VeI2_?FqhEW(L zLb^jXwz)XMf+8=Ryq%Izq#|Q$!8^|swiCKg3|1|g5v>7?mrv027!2DMh@5U%8O)}o zSr6>cQLHU+=tLkED&Qs9`4x9Mx-zMxuVSXTmD|j#!3YyNk+2FCht>|tVT zpwu}*INd3y(Vs3XE!~Ws!CJ2%WCM;oh(d*3Uks4j?^`pBn#XDz9UXn+Z~jKBHNe^8 zy6dic*l%&#>8HD&bgS(@de^(pKmYs{ds_bE#tE@mvX<3@Bw4koPML@RQv6Z?ks`{6 zq$bsdqy|FE?)?U>N5<-$3H`GkrJ*uYhjG$#^y~_brn+IPQ4R$CU|Jc@dZ8+b;z1&8^T zcr^&^$AFL``4lc`f+qEugpyDOctSMM)RbETVH+U|9x!$yDoJ#zWTpUh_Y#^0Xq|Ib zen9X!fk@C;Zaf;SJBAhTvD<#`m9N~seS7nB z?zsJqFW>O+E8x>kJ2n1r)8FQWE6H5YBfY9 z2R7EM2vgpvb|)G?)zrnIa49oGlVXsA&I%obfO@zMZb({J!w9oOI_-6+K7*fjMxsCi zoF$f!oKZRaZDW%sl&PLEk!uoIU)U-~8vUl#6?=FV_v{=pu#V04?ceboKk{QgI`C;9 z{_{USns<53*=MIeKlicczT=(m?3Umw@XTCy%bHH7>8>)_bC}N8iyfJ-fR1cM$JZzc z(PgMrLuH})A8yv4stj|k^))!ExT2LSnpk>sy)8xZ^W&vTfuCnH^ z$I0U~QX)n^05j#1rAJ^Rh6W{)qyzzRiZvpP@0bVDEjH^ysc-Z$T07aUOxjY)2DzBE zhU)1Htq04Z${4aK3m5%so*n%NhrzNs3fAk$SMOOf*BNSH9fR$XOD}!%Ti-J9X?ynU zdGCAPb2M*sPC;F(yIpzJmB0R`H?6>vv;R=*D|d~F9TfEGte&{DT^_R>Ay4rAzz+vi zvLW7dkR$nq%Cq0JM45GB$5lXvIcS$%z`TK}Hd!Gshj z({U$Jh$`xtt){Y1BjYSIO4Tb?ydlrGLILHb&#<)4rOSirY(hz*xE3ZeMqFN{vQQbb z@!AcqhcXqOp%rgZ6H(-KWHKEN{m>Ha`kG;=s%fAPHo#xBos4Ws%AB!O;W9JYtc?h< zI6nXf>9B||xJj^d1kU|or{kM6ny&$c?A>zDumOAwwr=(LNAG^u+O>n_HGcc8Z=IT& zI=TSdq(gc2^Pc-dFMs(8JVl@X>YB=`$Rl1%vU5sSc{IjGA}AxN>|ER$ivStwrOq;h zox-l;+-O-OXg5+TszZguQmN>qo{Tv&4M~z;M4aOa5?3>r^P?RQ*e|tfZjk%d&95TQ zdsrB4GHI3K{I*hvgbe0-3ZQ40>#b?bmQ0IO^|t{b2(iSgtVJGj#k^&`=SIkomWq~U zlw}RJrEu;=OJTs|KCaqENG7O^#$a#WRUDQmTU}iPor(m|@pp1ZT|BqmGcgRGM<0Fv^!@KY<&;wf zKJ$~;T=U-d{I8>YyK~Mxr}+uL_}~BEuYbx@R$KtSZH*-OWmr4hAQ1eL;#Xz`HG$az z3+2X5KgJ_@g{5k>iu$DQRvo-)T=|U5h>~1}8g8s$(H_oB`q@ITSc$Vh5p8lq&(e8K zSgVa7tCp>Pj;1l|wTKyWCDdb*As5Jum3pVBC1Ti5`rtvYCsL2|IvQREhB#w}61oTl zglZ#qQSr%?(ebUdy0T>t6+e;$=FS0l41zRt76Pl!W#H5FMA3X))Vgt~mA~}F5t-v2 zPNu_Oc4F)Oz_ zsBw^lE@=biS{X}us$1UsAzx1irGJGG9Z56N$t*dLW|?l;|K;4QTZh@bPAI!WXOGqF&gZQUU8WhMexWF&|bF{n>7b5YcaQoWaGQ6s%JfKk=5fHzEb#2lbVfKOHXbqACgyr5^6 zwF6R-K#u|(kzJStpU0#vVQ`h;U*w=8=p0at`l!Arol_Ws7%AY{hT1R8C$`x~=CJET zTZ&~BE@s0F7DqEmH^?$p7TW0aNQpNIcjJIn5(9Os>pMSFlIzJJIJDsAyNCGm!`#-b zTlbs4^;_LXgWz+u{q0}>%?rNo`MY-QI{Np$_~NZgeC8F8yW*8U|MM&Aq+NgOnz|=4 z&{dOl!9&X}b+Qr-8h@$!jib#1Ca;C4u}>tkJ(cr~D4RG~?An9^T7*w>vv-YnJ}Xw)f0?Squ3Pc}M{gHV`oXb|ITq@6oWXkoICVu#zPNzPCxjdn1E z*VS^<&kebPk6=)3-G*yNJSa@}Nm&&t(}Q4A+JZ@@3p+pO?M*^wi^?U27RrH5c#*oE z*_WzUu};kh8#1sS-uCU^@f{!g#K*hk@4#nHO-=p6EB~LLe8npc?B9P(-g@h{txJFM z%YXDozUiC4c_pFHYmA`~B_ByqOPRiXs?vyA+fHKGaIq(-O#A4T%`5?d{^9fuX)rl0 zYgMm-LGa5#XeHumlBuNVBfT_XCGkORu^=YNpw*m6Dnm=Ji8U@Wu2?1StS77@lnOhn zQH8vgDfL1j167o`#oSd>ECxY;uJ!6!0kaia;x#1-I#bNFAQ*B&l%eA^7c<5WLHz@I zOg=+bs${crEkr<^V1pLv(QH|o@@grMPRxT-MJ@PU67BruhW|4Eld|!sVu4liHH2gLl?p$-7df4^7p*&z3=$_cMfd&9z1yP z4}bp;p88GS{E-iT_!#cMC6``$M9+WoTi$tSOnv-kP0j_Zq%4RY~rv4+J?S-962 zy#LgozczBM=}uY`1I7%o0^T5IxJcXa`e8rG`*!4jNtDD*PNZ4hy^gj0Xi9~U*RH_g z1!$?6Vp6n?(mgc<7%|;J-|tiV2{C-zDC=9Hm2C+Dn}vD_NYso?fyjh7?wYlXVZdcu zNsduo@)0--%Ff#3$;jqcX@Fv}2UYh%7-p0;Z&~?z8fOAYcBV!rSoZF9HEP2w5i^<- z4A^cGUS|)EWNhC#^HOU~v;LM1vsx9H1|#}WolK)4M`Ohyw#T7KlFoyy`mUpepj9&1 zQPuEQ?i%9HN3ebUQ=aleFaP1EKJ94(pLzG)cYpBxANbSvy>HL%-N$@4p74Yx9MSW) zY}xW_zy5|Fd-;#7gmd`NzLC2h9DU5mGe&VxGHpWV21j27#r1(=3O)xu*EcuJ6DX$T zXWUs(2QXi#z}&({2zn622qun)e(iF=oAa+!>s6&S1iOlg+f{gTTdf01$)}qriLp9t z&220rEpZwuKM;3@ZC_G*9ktUuhFNCA=!8T20b44sR%l{FK(+6ROlUjC+9rTPJkeYv z*y%XQH(N{TkDMw7*BK(iJ);mLqVeu>J}pJ7Qu5OHvoJRTf%xte#rsLrZW%O%c=wAh-#HAxgSS&pJ@q-yz543s zK5roQd)sZdee#-XKK3_%bM1dz%j3gs>(;U*8LWow+rItVpL6xqfAP_eu7vaXl{?2C zbIOdULX);iOA_Tmnk}JPpqnIwN{(Ifs?6%+;1C&n*~M1gHi)9Es*nWPhPHP!Mue5< z(Q`_$ke-Ies+^wYOjgS^?T`y>0}G{P)mDiqNyx`(*oG`T!w$AtP*=b#%q_VIP9lp6 zf!71chDqo{wKz(fm_i>&c{E}XX?U{8(v$n}SzD>fy_3`;oD6Jsg#8#6>> zmq9OUmC&XrdB2^>)$IBZQqWRI6t;B3))uibPGPERLfwKYtF9aKX2I`z7D5ykPkrj+9{>1~v%=1uJHLACtzZ1&7r$`* z=l|{B{`G73e(iYg*b~0)>mJ@8{>rOg_0RwGPY*osz_Op)8}1zcwy)cl3@-#V3~@lQ zX3{DZ3ki50Y3`k3qztH-#~m}h{d8!sN$#Er2q)>3GrU(R(nU=FT%&h1fl1P$DD5Ez zesZd=i>hu!KpVACC39g3h-u*qO+ldH?!bKNyzt1O$Jbym<|^@#p21j z^k;Vc+xk9fG%!FQs1pU9dubC%2gTO`wSY(y{&>j(>ai4Ub=IR4bh@4k&a8LbH@^Q+ zYZzhXfzTq3c;iQcl+wFEY#QyJ({zZqKx!Ku0dmegdPxqfaJ0IM6=bd-l zdFNfnidS_40`SI-8-L|hulnhq{K;iM!#CbF?&B`<;z5+3fthuIvGfLOwLyl?ag2pg z_EKB~Vmasz1RzUKvt^9KguSn*YuvhoUQB~ebg?UnTv7l8o#QP-3?>YgU?2p9irJ#a zbq!{#qdIa^MPHbxU!Cao#AO$8c6N5x)ZDH;v%9COW*VXa7V46}?ioO-A);o%RXYlL z9|FpN=)0ti^$zGy(Wy9TPiAxK?YqO;+8d2i@ro@*((oky9IA2WNUx1PjL2%xsSc|UoTC_@U$q@J;wPcJhE;yz|sH+SGn45u2 z)*4U|Yjl+hv!gRBrCyT@=6eojc2KUk#hiXX*Jv*?sU_GG;y}ASdg|si+sXt41UJ zw6`N^v%wQd!zpe~mwEE&??sp#*-M8rskJHjb0`Jomu{0V6StFKY+|YTRb7h+(TH`) zW^fiO$lngk!6XA|s(x^%Uv}Sc$1nh|vTf7mO;=oT#nHUNYk%!^OPN|euG{P!ZrL#w z;au3k4Zy3{(OLCX%U4CLr4km_N*c)EG&ie~hP{FEK7-;hqFWm4^)vo$O{5A-62*`? zLlTY#hTtvcu2b60{-!%tCYtvc!hSp3PtIlSTaiJ<`Hs@uNln~s5`#*h8@k1zXJa5n(=F3v?^7@Bg5@aLU*ear#Kv{4q%1O+nbKM_Hg?7-lb2nz_NvR*Y<+C|5nQ5$tOkq#+}GH!NuE5Z5G8=U2fBNL3KB(m5& z&U2?<2lrmKGbc9;8^Eh=YqeV6{H@<|^sh6ycJgQc+kac$W4UR^80pQ37@W1WC%Vz% z4KXLAQ7VD-A64 z+L(9EjVulhlF<|F(6;C0iZ)wo=obsSR;h1G)0aZr00>m0Eu<86rrtTcw zFEpMHtrvwY4guC;nUMjM zrTdZO#At(hgzb?;OVIcMAK8ncy>b1T^B=SJNtdpD{I>CP&KOxcDJ&*j1NE30`jHj6 zns&xX`xO#KHl>p37cE*ARVqb2c?l4Kh%q%P8Hb5v-nuq76x9ZX^1w2dh)Pftwy2Gq zEvKPfAvv_(%oSO~$aPuzx=CL$nM~1tqNS;pmIbo;{ul3bo$xZK^k%!kwWHFtei)wb z1b38qa7XUEZ+zdu;Q-Pq+n({vXC14T8W|b+>7V_XWjvPdrFiSTqo(9yEr}yybthoL ziD-RX@x8DpO(Pi|CehiwaL2+{CUx%_HBek9?#?2qy6IA*7uG$m^vxmO{T+qc9ky`t zzhW?=wUZ-eA##mg?HpfdBQs4|mQVw;K*K)TwW+msV(jeGCm*+U-IbTEx!~;f#tn;f zdl&68Cw7m^E6TBu}6WvH=GH#loE_K3*0qb%BbK;mE<_RL$YF2>2#HvT}40IG^m~}Uxtt(ScdRnrK8~s2ixId8(5Dh zkme|QMerAI8l?pAD^?6s71ZPGcIAOx z{SjBA=Bbm0BA8)DD6YA7()hY=nXkocWbNZV(ih2*- zRJ~6Uoh&0GrR{y7fZn?vuNC^kHSZA zWq;(Y+tgRb9-YHTtG#98nv2d||KuwsFS}^$%u`!y#&sR0L4Q~A(Dn>t#A2H9(PU@L zE&vp|BND_pGvTtl8Oac?oJ^X3;mlqTnPfCWU8}P>A{Z;#+n9>dEPY=ljpsXM87Z;k z8*cI?5{oVWifY)J0VdMK4QiW98UjT%KG;K^>r6tmetQSIXK#DQgJZ*xwz{_Gzwiae z{57BXtY@8h=9$Y^0gqKTc1J}E&?UVVGPKJh8pA6~K^HkRUH{+J-|o(}L~AW_r|V!k zk}N%EgN4aVl!hLH?J)zGT<=Dq-tZ|tGIc3e$~R;=8Mnz7`c^zKk3OiE>tk5~r`yO$ z>(-oi*4igtvi9q?kDq&Hd;MBx76Ek%jNS#qq^~ZH#jCXqkSDcV*?|EAA>ukdu+{~u zf}D5vgHe?;)4+oU-t|E_k?JVgQWL2V+NjlJA-_ z1t1o8g7z8<_f)UI6{Yz+w?Md}qCj`c-|L6?^J?49IrrReed<$>{X#S{^0FWNugi5% zx8FB9Gh?bSJxR|3s@Yuf=E0zZH%Ek-wS_H9BR=BgnkA}FsCzn2emF;SGGFUpDc=?1 zh3F8&gd1ffu?p=-6KEy#!)PuaX!V>el1wXe1ZK$*OvPVmUY;O+N^5e>_+w6+yyD_@ zS6x1F(YfuDH@4a>jflea7{VxeMYnPOOPZSiCOTnob1W!iG*L!Mj2x(lnVJ>aa9Fzb z-C!;~&e}YRml7i@E}{`x)wBrJ$JJt-DV6{j8w26eDqZH8H6s~K8Ipk6ogj*=NPCkr zNhhfWBDHdCM;6DBItvu7?$5qB#GhB&_R^QV>?rj&O4|!x{Njnp<+&ba=Wyo(qgptj z(ooIq6M(1TOC$^op?#Wm5W|^>N ztZt#Z;ZE(pWG^+-|EIEuKro;XmBbAkKz8G1RIUupo!Nb?g^v>rBYG|qq(|2wX1+@t zzuzQX_0TimqjC;mDWOday1PA`Bz`>C7$-{@rdA%{ETA+MU_kNclV*H$R4UV<0k|~r zt^4(%L%8YgHN%j$indLgH@)CE;Lp8n?b@}^diJxI>(Fk!XKaz0U=#5Mi9IyjkWdRI zi~U6xeHU@vzBCmTuR`C;C3R>z5hUb>LM)V?PE~4Ac0<)pMoEb#eNWaODyv;9UFjUy zBn9>|T+M|Tl+vPXRLo(PItOyDHr13$Z&#OZLZd&+wC+iluKT*}@ozzGQ6BiNvp;uU3jyKGx~a}2LYAjb?qfWbH!_N z)LKk$;}I<(uqHma|AmTxcfoODg;V}uY}7-TFb@8$wiEkLt#5%X%_H$@Pv zsD-O2CFAmvB`}e6xV%a{%=B}%hWhivhBZH3lUW3<#r6|TxC!ld4L8=rZ0F64+RC=a zS!=I^8*d(b9-7@f&AGXPg->Bnh4~068Y4u>7?G0baf87VDO^3NbOba> zI}=zzKroCKFS8sjz(lkMoba_d2vsf_rf5b6GL01R9693vp;4p$7ZsZf){ZbgSh48^ z7Xjx={i>sy!JUO>@R0w!y0+6!KmEVF?4`%`EiSwK@{6}#eAA6Lo%o}>HQgSz^(D>}aM68NYgNFs5pLC=LNp9ia-dYkR)^bhK{AM4D zr5a5I<>;cwN_;puqfjBhs#QD3Nfr)5*Uzg!rLQ%)X6!Mit-a!+^-sQhV(Yo1r))xd z#3U*4;2a}f&$`fp$yV45L8~RHBb~0bay--|LoFKu{<8V{dwE81RGopreOO(9~H&2BqKoJXjUWA?MU(A2;g|80r=T)Dr z50U`>FZG0v?<>%kuta^oae(sx>Od90W}{JY`7faaF{**33i7Hb{wZQlJIe^`Eumc; z^c7svuVj!}XA1lDzFwI9?I;Dx-q%K{bg=z+Iw(^s0KD7I&?*Y%6lLSvO$Q zXg10=#=aZ!`Zf7e4qEQiD6==mv%ZH>=hB0zoCx+^bP z_r&cJk3DN-0icFz8&Em+=+79|K{LYD46Pbu1ixCOT52A0GR3Z>G%KnRZO1};Zz0t5 zF3F8E0L{g2Z2!c2FCM#ApoLAq&A6fcc}>TsRXxjYJ)bK(e--y4wn45(%jUmcsD0=9 zZQz|#?R$0(1+Z4fcJ=d~`~5FDq2%PJf7{cSBnuD?1fN>i~b>YBILie!08O zv|EGUXY0MOG=5@VNEX3*lWH`J^_|wKJP5m?S0Q`df3CZAh(E86?MYWY>DS)yh7EP)Zt|MJoM#+HMlda}1eakBv0IPFh6b5nBdNK2_L`$Spu4LTro-muyr z=)nN>DVr7n>MJf9KkL+yiSgL_U>LU3OlAi7TKjQ|8lV=}Ft$dRX5=f(tubh-i3ILk z^KdQ;xJE$WUCF2~0tQ;VpW_Qa?%}5tniYSmr3D7bas2jHH<#skZtuorT@%^t>2)$aBAow(mge)_aBmSSx+I z@~SJ}|Dg}9Teog`-u=p}PW+qRzo&h87Eq9Kz1&j~AQS`z6#}Wa8UiB^$AVr0oD7p9 zZTXB`+apS#z+?i;)GEKsD9z1*$o`7uYC>lN2qv~#Wv}*e(RB{$Gl1C1F7QvQR$9zy zeC@-WMpmzRPAS=*oc1I8K5tEokDYaD_rbZ&?4GIFol~>Br+8?l5@D(O#8mC!O39Pz zIl?-J(w+g@q@l8?bj==(cxzt!!1~q)J(x-{C4~u+enaiAVyE3}sTx&r5{W`JGIhHA zhYX4VqC~qCmQ^#-)k;*IWKDIh(gIfYN1c8tTz$JRfYdF2Nf~Y{d3*i~U-;_RzGi%U ze7SbuaZh;siGS1XWx35Uq5UBui)+ElTZHDSHJoh&wA$a?#88;UfOQAXJ=cQ?bBiG=JsRER0a=Ov|B z$5dFDy_OkJH3O|~zh=MIl`GA9E($_pfe2cpX){~UU_x~TFcgIsX6XPabD|O-{mI2dkL9cr%=yX8Vo^287uqj|KUH60&Sl8OS14u& zEs-(_gCTiUB9ZyXkZy#-@cUtf1@x(@g-=xiORe&X#PNytlCR2NDp!U@58zI}WzdmB zGx+kI!vUm~xUFBm{y)F;r9bxLKR&s3`7@+$^gaKA^H01c<&FnO_06G!M07!r&@z)C zo(L^oLGMfNjN`P-0O2cLz%U*t?3Q;OcM@Wf@z{}&}WCjz#Rxy3y6!A;E0fmzTos33PRjTUx&t9 zG|6HdqR>%TQ&a-!SHN?9VXQCRF)=eVU|31p#N^~vS6%g8&-m_VJ^R@wKxuO6+eH^G zgDCXf54L?2QikYpRM*M)7!~NitaOvR2vSXiu{R)+9Jem`Fvwm;$}A&0uc+$>BXa@M zaILkhC1A+5b!u=smv+Fnip2&|G$8iz+y-_0SOlA%Ai`2fKWygqsXbgHdY^Oqq;=hg zoOfpD(BXN2dgnuPd-ifM0!Rv~qOtd$qskHN2rz}y@ksK{@aI~zy-ur{Ut~Zc31ZqC zNIJW&Fm>G#5nBNVXbUI{cS}Ld+gUkD9fPYL@Lc~s>zOeO@#kf_d+`1beQ@{g-S_Oc zXV0EJvLJ4C_Bm&Fx2@Z@b<52axX;~vJMnLN&(6`xGqk(1j4WKVtswg5SajU&~L-*1Ba&G^@d5Mo*Q?pb1WFX6ygF>&mD81dZj-#xT zGC1;Am>D+#l}T7_>fq4siMntN>r#`#Tum&Aw-;G3e~dw%A@R`e%W9^+_B*N=ZT%kb z&3CWaf3P*YY)|~Qb?equx6h}az6=3)=hVo-LzrAcgFL}BhM*ijX@gMRN_2`qRgg|g zJR~(PjxPR6gNU?v!9fPxt`dMuhdXrsv3NFAXWE#u=(i3kNwEQ!Nfmn?5*N|k>*sc} z8IWnR6VxcWTYuZ5U!I)XGSk^SEA8R*MsXXNT+@AwpMBb*-qr$%nlrO`tVPxakaAPP zbwrTNq}MT66o($Uq*p@@pvi|hV;+`g7_S-HRuc`yBLSStxd_D{ENxy~-L9Q}(Ct)+ zbfWu<_ZiqiG)3*JR?W=B|PdojzWqIFwc8*?n=HWV2z?NuAiCxbfP$lc6%~>lA zzS7t}3mDfRz`10Yo7~7y&96x3M4&;r*#|ZDSxt)=L2N3OD<$Mti&80#JPDRLaXL$f z-q=lds~J%^Bk2jsAWVdlUlmvF)&rxh1&NlXyaR z`?_IxcIVXG-u>n>il)s{hgeu;EUohDjW1QEhp6@1D8JGW{gZ&L%o)WF=||~oV~j`} zXk7}~AVU`J6A@1#5e+!^U4DDA&z)Y}t(w6Lr0CBN1+bQ@0^YW5)$Q}9&6}6yeed1f zzTnKm`90{wkfDYhbWsSS3j{kUl-IVQ^+T4bwYA@x=hrt`*yd*m4@^>1fsNS#1U(QK|K}b?jl(_)8tD#4-}#AY2( zokxp-)5OSGS1Josm8fQndXdW*AgzXOipf{-HN(+)DKS`ithBm~Hah{Zlt3f*W^UO+ zmt2L|H!{+F+&RuMb_?S9$HrM1HrE07#w?u9V>(B7GDB;NH<=fI&l)WJ$9 zi@!jT&H1s>Z3Ch2s{yE}!0w0qf{SAPWx6XRm_21z6|wJP$?lLSBU?|ays;3LJ5~30 z;oobA`13Mt6O)r?pL5P?-0kHG!1Z0#y>wgkn&3^a? zZ;Gmii$wgI=IeMB2+eFUyeXVH78>m;2G}-9C*aQ$-C(inkr!fi5w?y70Swys_Aqq4 z)X)uVN&|5iBlnJVA2PzurUig{^9)Cat>T7LHt&1qq0IcTVsJH$-F+7ZgM zp;8hIO=h-TSl3^p1cN54rDe!fDjGRAr%IjPC-5*MCq`=l;T+H~RcpJ2hSZ-{O%sje zaBFQBi++THB(CbEI%R1%%p6(|j&>h{iOz<8t9WAADsI{qWT;Q+J{HHBb}#U@_8&5N zUoab?O`UueoK?5fOnssfmxxLfx+66N zJBo#kK49wJN2~FR?%M1&Mi-W?Q6T*e$p3K>B;q~ z#QUT23~%4Qee3q_|F>Oq z@x{w%w=i%!aNxjlRlvRB^$}%xgjO9w8u~cQBJN;>nK?XG;krPu{?uimC7_6vJ2tv| zHgZcw{AAp7Dwn5_H&pLZ9SB7VSf4RlV^#`Oz2}nFf{1AvI=+hsFUwrlQpp|bsE5x) zz=77t-Q(SdwAtA_Pul8i8qP8eE{n&{I;H#QM&H@pd*>&bc20E;9+C{N@cNNrh+SfV zXnoBZs-Y!fKqr#$Zk+=YsXEJJ`L{?)Weqsjcn%Cs4$DN-i&0hYcKV9Y9n})P?v}N~ zOKaJ;J-c`R%m4kq-AC`+k&%(do_F5XZCmG~@%Br)Uz~N;Su1C64=z&xK5(ctwZFA- zU55*CSG1|S6bQGCQMmzOrgI?BE+d&s)OcmMZSK z;*&6jQSR`}uQc^En+Bn#yIP*CUI`&d-Reu&g2fYDSqwYs?Uux2ql7h!*8QX1$8eVE z2-25UYjn$o?n5qIEQ@C!d}#Kez1?dh0Y7N;CL?XZDQeN#X4%TM>E3RZgj92fF-tX_ z9u4(KLa3&7l0a-P_1RUTO8m~;q8?U|hRqff7|p?HBDYv8*t!B{*as+|_4qdF*D7@Y^P-~3 zyulXnQFF}0=E1*CToHAQwV?GZ)8t$)5_dKXXPK5<7SEd&PcJA@PjwC*u5YS-YFWgh z(|QzTI-k9o{1rp%WTLNRW~@bf8Nv$3*Sd6C;R|)84!27kp-QPceXGGxP^;Mwf z|D7*?hxqgA*runa|NY|3p~&pCS`7;oQxNjEBQJ^P$8l-7v)`9?~`8elMp9>iH~96u>a>>nBFKJFUlC_9@M zTE&}Y=herDTf<44H!VJ?`?i zk_E49Cvy#_^Cf}1Z{5Un9c-K&`?J63EP~I{N1=xSc$IFQPUnu>@8~}M?qeV8{bX|O zVZRcwgZ{+5Z*w`MR7{X2W2fid_h*Hu| zvT0ocH4Gca4^ggk2UhiT`9W0EL-;c^fB+utgYz-$6F)U88v(9P9gq;8151n3y3;hC z$37OT6Lj3C(J_OO`^LJDZo1dmINJ-x93KMI1GYu+%g3Brb&I=YF%KS!B;@)^ZP3BV zl0+7?wCdlwOES}WhpmqZ3nMd8#zAm$G{aO}G$O3ECzT%7TtDviJQ$|-k9_ssHN&gr zk+JRHxBt4&ezyClesb1XXKmd+Um;`E24*@`&lv^>OAMY+Glq_|cD1_?xkb2cuE*ONifs+f zVEWzSiK<(?YkF?a^!#iwtiMlJW zz3$huP(>iT2y`s3iW91qb|^;4T<;oAfLfQl$n~Wq_yLz(mFlp)Ty})mJ2J9YiEV9O z0H|BTwRFH%b&I>fXI^rvr$oK~;1XRtO-2X<2v;O8{c|@P;Mf^DIW@uCN_iNB$I98n zQn;o*D)5=CY0o_YtWF=1zJ93x{Ak(^9z6Jk&wru&sD5(lX{XKC$J-al))!oG!AN_s z!p7|E?2TXf%5ojW&gl_f<65Dm1Z;{puSRlb`H;J32ag;YAm1 z+umy=&j;p{Pd+*R)%BnI+;YtU&sV??wG%IdSPi?u9@I_8dTf_QFj)}Vmh=%+gVJ8L z*tx4&idHEI!kQ4%84K#lFL0Thn+sFRCNcVv7)bY0Ifog5vM1tsbQgQi_mb9)a& zjJGEKg_|njKTEAkrx^{R0D|yAWhm4!w8K#9vSQR>WKQcv-s~9Zkt@tG8t~e zZE>5KnfdY!H+)&a%UiZ=S(tsAA4Klf%H8nX4bs>A{XZ<@PA*o!3m0}v%-lRu4M`fl zPt#Bu*O=*`!55E+m=m70-Qg``n;U&n2LMx;q9~ zhdSFePgrtQF>R-_5JpidQEoc)!VW=0WF;EN)NT+x2KDCIdAs5vgL?3ZLwn=8?nBN$ zvvYW6wp$bLnx5G`C9`wl5^dYwsF9X1p-;=7baHXd61-qS7+%mtUzakgNUU|QMAg2X z1r++TL;mw{8yM|&?)<0E{8RVQ`*x%~a=`@`+<)Kw%eYIkbJ)FiWb=l(5hrL2V+?96 zN_AlegKdiHahmG3uc476xT)fv^B8R=f<-JX8^56xTqRflnZvvI84VVhQ?YV33m43o zQ$?rJLYs_w^)UWRB0=ZNwZnCxB8jC9tQX{pc5tg1)Rn}?=Aj1l(#qno(XlhObRXRh zd3Ji=?5_Fo;@PSFi&q7FKwX?RW|3cD5>q9MgFLru8c1-9j?)HNSu#er~Z0?f7cX)wr&-HF!0xR+Ivn#DdA9 z91Gmt-Q42!Y)+caaS}dJ=wqi(aPtueAs+>%Rai`t;+aO7Kr9+(jOhaYuhD5;!81-? zLD|jq3)Q4sy z<0c@;u2wWcx;mJ};FRddkM`W4;vFcW`F+d?cRTy>H$YBEz4nyDmC7mC{@ZNwl;3&6$Crf79ucuzD!8 z(7OVEZpeQgZmV|NH9gX;dt17$f?+dNWvj+`>spG*=)cc&)x<%X;7WrPZYhSwJ~49i zJ5-|~aH^cZP50LIdVl{>&J$`RoSq7vpuyrT2tlKvQTZ4;`l{Gz$vRp$f`ybk1vzp% z!j-wrqV>RN_c0Gpub=Cc#auh&Z7pdVS-YnDke<-Tp1m`>rsr)T7ABe~p2@5!R1yFr zpN7N6@6yn{TN^rksdrH=mWuYQ>fmnqdw2$4ynPsehubO+z`bR?gZVIARc3ln2edj$ z@OMlsr_9aBtQw}W|BE-Q4!t(TCj2mK61$n~URsqqo+g-2_eOUnlZe0$L&CoxY2&Ch zjuSDXL8$zWP>X&*L{Dsk)@Q;(@$vQRgJr?QX>|u+bv&)9k?uoo6RznjkhW$z8)v08 zywjIZ7PnfXn>TbHa^d_$Q!f(F?4IWAoDQqj@??oGZNQw|XEELn{YJyFL7&)kBy(PWxPk-n%dVBqn@4Kp9Bkhycb{}ibJA*TG z-GX=_7*BUgWZ&pbi&|X)JI}IILACe)PXFJY7Ide6qnZdUGBZ2GpNHEj55RjzdiO!6 z8U;o^;AceTg6k-P%e@&xQYsnDED-X@nwFwT7`OL*(3cK^9rb7|@~Pm&U@@wcXj3@A z=lW4u%;-Uiaz`!=Rcc58;fwI0NMjVv3tzp=J}RO~rF=O`ZknB2m@Ss}khit8vo|_2 zcIw9NV?L{y_h_2uQP1t!%LR`nefyO%Wd_qt)0PUdqIV6wK9N z>BgY@cBMMkPb3GBA#=3q*{S?BLx25g5=3k?4~VqN7^?E~VSvS2&CCii)5}D#4A(E8PVpkgx$~?-O$wP` z>HIz`+4baP?K-2%S(75Lzz^4l7A2&cE8TFSYEi8dX-Bw1w~DuQyKua-FwQhP&)XX6 zZ7nsIL3`7>?qlM7;mquU8w;I+7DO+8#S?Q}-dg@!|!;#MwQ2+37I5bg7ie(VzF#^s5SgUc6gW4F1v`69*3u z+3mw^)o;6|N7k+Bz^Xa6QtKh*#^Z@X)&&o)927h9$5I__VnfaHg#xaSt5>FRG^#gC zgF0ShT*FYB6Ukd7DkZ(JZG;|e2NxWy8YHQ1L52+s{yO|2+(`7ofeZt%+B(wO-R?dX z^|pF4)EkF-TSu@hju)SO>Y`!df*$qUz5{y9K(8INdN*G7SwA{5H`L!4ZjY4hp}iw= z$_#@*vKLEcaOPH85H7@0&oY%>)Cpg}dhltAMtDvtt)bPbjdFD~eNlg0)$xQE3`qUR ztJbdry`hvQjgqq-G}1CIJ=|@_^OIsTliK$^xPb-zjYXZ zhub4z+p}*(f{vpXfK)3rVXC@(HrH7rh~x$-C4!ayQkA^Ac0^V=S6Hj|@2f%)?9_wn zPzf_33sDBCVj*{0=cQ21L&0pW(LTUNNguoW9sFlhsB-J)0H$QmYyKewJ$M_AGaY?4 zGu9qEeRKCA3mEn6uBpR4jC!s^d$%mEFP%mFS?h0f|NZivYxW-+;?KkFk+4neYw6G> zQeY`B=?1GIO=yf7zD|vBssDM!6HX(RH4B0b@Xn*9eswq4qib|qHCA=JJ`bKcqUCI9 zYu2)hZHr?YkFG zAKp1VGqs=dTOV!4BImhNNkA{)&)42O)ZZ9xkC1KpKueU<32Y>++Pc+RpD0I$2nlP| zhXAFP1U5|Lwmh2)N9Bc4!vI4;OkcKYnGrWM>@0)e*xf-Y%a`%Uis^D56J?x@E=P1v z(wU3W5d)LNMeJ{l@TWDE8V2Bp6^W~HCh3neZJy=&VOe}cCvnlec;fssI6K#^i4Q$E zec=A-xqXL(Y9P4N7vkv1kpDc~9yt~8ff1=jTC1wD<;y-ZG7t#NCNP9rEgiHik%V#~ zDk_FoWm>9jOx^oxdtGZK9_@9-wVz>lWQ&z$Uy{})eD``$Uq}B00I;UkfJD347OzX$ZI9aD_QGVdu*>~cE`Iu zI-c>2XJxOgKlZZqj&}g6$y3x5`s_^fv zs`skyy;U#vt=H-Pr`YU%=zg!NUcLJ6_lNI`IiK7KWQD2OEImVx6#&bm#roZpu4wW6 z!stkMZ(BpR4FLIx+|0<#7VvdgfPP(n%x4|-v7^4g4i-J6FU*BP^h8xC^S;BMOnfF6;t;WyPX-GDbBr^SG`~* zrMNOP3Cp^|fy6M_YU*HEJ((NKcWJ~5rsk=KdjND_Y&XPc^GG}kfS+aS*IgvlGTPs^5C zAv7cohI9cqwd8O$AUHj+xn*IvRI;Tk-7b8L=rMefB6!u@K3Rt)NG?NYX*$` z`h%?vhoDQi^8;0geek9z0B$2K4#Y1hE%sGD`jsa{L-E4Fk~#@0mKOkP#BpoDWzlAh zA5ek#s%?}6N4Z}t7;!9kUoqMz-V-VSUeG{Ai|fbXBj)t9Iw2~STg(--RLTzfZ%HjuTLpkZ zEQxbx;2MaKX(NhiYlsQwi0)&IoFsBm7$_hn(=R5JsH6MH+7^+hi9I7~5qJ&~!N`I& zX^5N6MWM*ec3L4{qmA}$VJ(dhj6F1__nUnZ!i#Mw0A9qjxL-9nraB2KmS4;i zv{ltV;DSkTT-y*|s$_T-XY+>J`~U zZCUWjcU1ma#qwmp;fJ0zGl=890p^h^Lc}QO5A)E4ybnrDLpAcH*@$}qAu$PUVLwGk zMv?#3V)wCP(I|$0LmG1-o z8k*qp^cN#YNqE%@jK}#1{MXQ5uI3sPF$))(q*xzfZWuQr`^8!>0MpS8fsI7e5|Qw$ z77!V~Fn%pyFqaQtR!*GCCh$sO0$mRFP)SiPB{24tsD~>COClUc{;F8E z036)nNwhDQ#G_RxYQgs~1ICyI1!xh@-7+*W92{clT@?`w;g>};tsSugQY<5kG*X3= zJQ$;x5fWnNY_Mwzmf1&GJ%_vTp5;|)zCouqw|?N4FX0InjvYlcO*MwJG=nv0@_mab zGhStbw%nLvsRS>-tO~#?mS@c5HH_m@i1B<60v(Aes#WZ-hQ z8M_Jxu9!hD@!60sD5x6nlBdOiZ?OhWpHGWT>|7?a_{}%X**TSeRHg- z1Cu=l;4zG8MHXR(fpd(VI07blOZaJ5tkbAmmDCE%s766 z+F7-L)gnJW#CAKg30ov$p_c$J)LexTR;9cchrZQ`F`GP(wt)$m^O&+O#^XJ= zJG{o0kg>;D9SjJKCAbU(E3L)k5+LeALBuQqMuw!h3OeguQP~v*z{@%iv?+sbWucFL z3zyDBlgdAz^@QR_uo-G6f6HR`OdJ#`Fsr<8w6;aFiHR`C8v)3$Ig6jhsWmcGtyC*e zGyzJH@n|Bv4Ihs#g6<=$7zONL5BqoQY*LkhW7bC~e3b~_<<$5k!`@a@0Ib5js0vK$R*zkl!0fRXBuB0EpH(dTD3%Dt5-mU%A__*a z{8C_YTwj&I!>Zr`XO2|OUnt6M`8^4m!o&hCh^q%tvzgVFVpTAO$}S4cSTSAAVn&^a zhZ&$U63$q#nr1ZskF4zPkT{Dl0+&i9{J$bQEQE~e7VF#)L1F-=d`tL0XGJlQZ>_>e3l(y$ z#fY4tP@s1m%>WaY;0kj-JkjTB%utgMM;;;(id^kXvx|T#YoTzjT-^1#VQrW6s8CVk z+RAHsKCO?0zwX7EBTeGaU=n>!(8gTa>~8uJ4H@wZY*a ziEvrAQJGoRAjEeshdaPmZ87#lH<@_|{1}4VK(twm1OV(SC6QT0f>ksbLXLX1o1mUg zj+&yc%0H`EDI~Z^y0T@<1RowW!u9&rRGkt@aMUe0MB9i?B!Vb9O-LNr5K(PIl&WU9 zFI%@JHI3^D)AJm(hOzTMETKnX=7(^TKWuYWxL=W}UQxLd!bL``5k*XOH->>T4>I`5 zww|$86JZNmzc%#5J2PYV5 zeQJPdg>V~nK@dE5f&XPDlN|jMwp|$+RSgsxqoT;gWQ>u2+Oq0}_v9o(<5@6^smp?> zs=>Z`d|!05GaPy59hYq;vFDjpsE+&&ArXGh5PHtrm~m7L0oYk%VwX+k6bV)_@8Bgc zaaFl>t8O24E{EU$zNOBeiWL$?lc0d3UPBhqA|OiQ;ek{mWsE8o>jTIQ#S$L)qu|Q8 zBncvp0uDuGH$s&zAxR#gq`}Z|h^B-8MV5#L_3WrFYlsw%5;1?sxjwLe67dw`ybhUUH~}8}D@F^VF&|Sj z4HMe2IblBkT-FGSj@~v1x^d^8E!;w+1CB&aIJX)?k**7a(d?g~0Km>V6Nem<)6@q; zMIvC#^ijKd)IJ#ZS`{$@m48;Tvch)Cs1{Xt2-q9ft{mq*5MzAHcQXZ4l1)2vXw7t2 z5w@nwYfWI4RwQmlh=*HI@DjPIZLlClnH$y*=8}p0I{-;5yyY6?FB`GRU{tt`kT@98BdQ|L#NLL$p z9k^3 z8xb2|p#$7MC@uQ>_O_z>V5n$+JbxmmPJ)UR6-6t=F^30+@+d8G{&#GF7OTJ`;XAe< zP7E@v=>qo^d9)T>6Uup7w!<01zr+ZJv8rAO@%)vrDRF_yMp~R|qGhcRWjC?f!l7~! zSQiJ@NSJTA2-$e!d;1E{K z393;{ByoNO7}4SYvG6HmjFE7M8C_TaL+&G7n9-pmb*OHDg>0!6_%<#SRsyV@6q9)H zA(KSw>pt`b?NmXp9Eq6)3&*jLUk6lU!0u(Su_Q%SP=$&HvjN0TCcoAld{e6u8 z>lkJq_qwc@20@ObvXYizO*dkWh-_Aw1$*R1s(^6hn3ED?_Ft>E`>GPGqHUq_&ni}C zm^KMHnNY#?fB_lyDv(1YoSh%Ukx~|Ykq72#{wY5){8HpvqQ%NyJct@CP3HN<|hHRlA+- z4JAygl%R@s#LK5u{#nIJjG|cv1QIL2#B@9%ujL%Qr;Gn8(orVwivZEoG>Y&R0zdL} zkTCsPSVm<+7}ViCdPRg6(L3frjAg;m^+MFJf?sP84D$!U8H+Iu4i-Q}JY9iIcTjE* zBuCiH1V+vZGIkWPZ%rKlrvGAZRU}wNJL1{nbLu3hSb-sQONAcwgiAXES6!`_fUT0G zWhaP4G8&=LLb(_#vqOjxOC`Ttii}H=py`N>j^su&=T+u&BaL8$u`6a^j}`O?A15Mj z78N21(|pbn>!93Q`NN=>3gT=cF>a;7o_SIWfGG*yR8%BbMba^qbKbmIXqq}~c?2{= zojnyxI&vk=M>WUWkT=jJQC<}xex!?H_L1J~3Y^>}0_j0uQca^VS& zDq)Evc%W!p&8p9disa(?lXJGy+&T|a9n(Mb1!CqiH{Z?-A5Gsn;|kcTP_gtVFeh-x z0U3(6xLjtK#GtQ82YGG9a!)UY9n^@NF+d>a#439Tb=ESF5Z=fSBaPS0!&MKQa6L+~ z`W}H4rGWc^I#pZmqel}z#LP-^Mw`a#a0q*I zNxzXHCQ+Xh6$!-)C!5YcJLxVTh6{sF(v6hnGB-|St{+X`Jufzds8F$pqhONi@Dw7A z*j>c8)UtiNA(rq%O(CvS@~dT;2nabb3(Q0qI0o(;K;Ao{ig$gr*6!fn2#PINB&xK< z7!rq%lW-jnv|ERML>MyQ@Ss?Z&h94R^8CnGd1L(@Vh=Ya8-d)=uy{tiy{ebQ)6w3d zNU(~e;_j4vl`PADo=v^{K_ILO%mh-pv4 z49vvpM)LmloW3JlxQ*atXwm@TbPYxw?^$9Ukv`^sL;SI>qp_$O&7d-8smnSOGv9E+ zASl{KVXRjpGG4;LI4{2o7|9q%4WOc#e-d31u^EN9gRhr@RwP(O!g28JxfUp^0Kg$L z&i3SYeUxrmeljzBEOY%x>fU?mB&%4w0PGOa)d0&K#k{Nqukb*4=w(SHqZ%QmJuA=1 z&BHMCEy%kHd@&w8NPQv5kdaq&bqEfGc{Ubq33FV8CHEN8Uv_L4qqf0>L0Js@z>$A) zQDc%n`uL$`W@msYoFhwoh3|#z1!(|)6$w_6T(s8z?4-Kt{6fd7{GK19o0c2TUO$q# ze$<#4QfFJmVv9nFRFzO>IU+A0%#yNDu9qdnw*q?uq2kLP6sI4_^iM@)GzHl!4tIfnIo`#YHBuD*m~%Uh{B2`{9Et?1 zNP_;JXmR}4Z^CLW*Vnx+x9@4X>C?BeE)pNpXKpw9B=>&ekMy}QV|>UMznU7qrqA3_ zm1Y(5iF`>zc9bQ+EeD7eaz0?)4w#%OVKg5C3OeY?+oYdmFY>1-~pTBlu3Ikg77y2LaZGO<0VR1jaR#VFODJ?ogukz=U&76#k&9 zeIho@fE5W=kwiS-DgZamfMc%Y-c554ewJ>=#C1xDT_m0zZ4AQKI~}X&X0E-j{JFCV z^hu2IYpDqe$wLZ{RW!YnbVAcQ67hSF=>-yIZ-`jFjl}>4t9kHgo~?@#8T>$0U`r5w zkDTTdiDU5yT=JZqwju(pB+Lsj92OblULc{tu;BO`t@6fFd0>*&G3S6C07G#GIwrY+ zaj#lAQ4LEOC=#qfYP@&HJb$y)egF=_>fc7UxraWVy7!*ju{C^D%T3h!{ngvW-7vD| zx;=EuD?C=waxkjJ;mB_dUj>HbA;yA9!~q!N^c_h{R`wXLNCgpE7?NFNSZuV9j1j)( zrk_DvZ3qe>0Y4@fV{H@#L5N}{GPPj>F@hRU)z?B?@kGTxAuh)q$u2aZC8S8O3Yqlx zWb5*ib(^r73Ukd)x)nFSf7PphSQD_f*SS?GJXXHfjycCs2Ck6%rVUD0ym>KagC zT7)n%QbBvI2(F|ia0(s?5ceb2A-Nb)+7gxmVP!Y4eB&WE-IgPkaU4 zD?q+cD^3$bsOCmni&^y&?hlc(HwNH3rx4Y**XJGcS+x+y4}&y6m-jYoW1r&&*48;mw^iRVg}GEQq1hMC;@R$C)8=G z0mC&3Zj4NH#5jSFxVmP=te3=5-T;cifnDI>Gk5{yl`aB`9BoA={3}-LSO$hJhZ>V4 zGT#)e8-!$aY-<)XHAFx>1Tp16>MV>{TO`7tRjh5sP>lqu&y0$B#&aj;Wq;Iu25fh) z$5e-`*U}2p>KafHUe8X9f@-v=8RlcL3OSC000F4Pv?!0D$jf&i(;60d^FhTK7y0xo zVVkY^S4HG(`9ykIF;gNH#jYAf*s9rgo{}O|U!br?io&UhK2K~77$a_uSZgIDu6vk8 zkJjw9LP;>DR4GfvJZ?AGdjE4$0E~`4HIf|t6@nCJ8~n}E#(j40Ms>|Bso zI*H_>tC<2pgz#Zy<~82%9z#bwV6wpk!{TeuY>X8{+|Z$~LQ;|04dk^i2yj9fz$;Ee zq`5oXsVa&e7{}l-vd|z=TF;)q(CL@?F={x){+eJ)6}&Do6JjK?nh{@_1MA>ln>Tb* zm0%U~i+6^KH^;4cW$jXuQ_%YCU1Ha2$*$Xhl@J3i?D*m0w#SUA8=31zGuMwaI5DMS zsYI@*1@~okzJ++%No=Ho8v$3b%L%wX%lE^4}Q*= z7@|m=y?#XXa4knV908&R|4Qh~Wt;#wmd2nIt4T!C4yLFC#?Z6XlQRO z8dm@hLGZp530BcOdBs-z2f1fRq64c=3pBCh0&robwS%QZoNjaP|9tA+RaaT_#xX5F zr7o&v5)G^>Meu_-KLZ9!0`opWy9#+NXiiHKMNCHqDlrbd!eO2(VO14NMCk?o1BCN7&qGX(mSw-nb+sWYW#vJwF8U395|NbgC$yN<3760I6t4heKvFbNM__6_k~*A zMa`XzQIUK&SO!<$3MtwLMEJTaA)=aNfJU6T5UWJNXfp-h!RG`v&s)5ry9+bx&sX64HekRF2)H^ z#+WHZ=cM^j?|Cp_@9*txMdOkp!73V!H!l{Za`U~jOJ~5f^Uodw9+XLc8_z$t&~B#b zrOgk~ZPqr^w@zh-kEKUW5v#e`{keyJvatPeV|*wzam}@Unix`p%gIIm-i_vD&YaGH zX@%sBwH$~tLSTxueASllvQ{KIs>|k;AywMZJ$}1AA zqM0l3&=2Uc^-6g^ek@69B;VRFn5MaNuGqYC4uoLS31h!g3I(qF;>VFc=HieQtf3Syk& zA>p&otVky#CB~M|39pqM?EAa`xJ-gCC=#rqev~YH<+OzIB&H+-uwHWpX(2nXWN2@p zktuHb5Z#EG&)j@FGki3C>rBlIGl|x`sxf}e#p8x`MO4(f4znpe+n3$YS3c)O z%bIq<*qX{)6q56$@Gc)21QID=1T6zY`v9v5V-bjf$1n?oyw7gTxGZBtPk@uq3hJF4 zb^_PNOkziPqovepDwG)UC5&AZ1P@n^QS^9Ztr8i3 zk1@}S#$I32uPYL)qJF%3Ha}aCP+3ITV@S1UkP_&?nqH72rEp#te3EXfpZ}6)%~XBC zQ4jb3j&M2Slxw!=>j8!gIEsHnO_&C zc-dt{bRVOW(BT2`4dV)_Y&KH2`(^!Lr)cH>Jfl*OA4xXHo@;tIr z&{(?$EVl#8{lpYz7wAZRdRx?I?#f+oIGwADbldVs6pzz)&c(AX!Lc_#0aECP`VF)QWBBMtKTV&tcT;&aQ$rKyos`M?-UT_Q~1GVN5+ zN^p$Z5kyq4Q7l9g5R1FqE)dZ`BFxG2GS?7!HBt8}usI^}rC*;Mb6R7}>k1iy;AIR6 zH^g-&4ml*JNU(~kkt;f{oRKpgBbOKIvtzY;l;n0`x%gC=T96|x(|y8%J&&W@7sT0L5}o2DJVLWMbKr!)V)-uXqDX}5IAP?qnb9eBs0kqA85Ojc zv3g`U?%2LWr;m`DZBrz0TwqefEPQe;$`Cj1$`3nn8R_^zC5A7u z9>YPkHO^{+2nqH$hhV{Sk+7L$p;3$Bufe1cLYPQrEsY5YwW*09n$4ybSVKnOEGxME zB9Z{uS!-gqO=flVF;Ed3&mNzX^;B}3u=Lt4gt9xZpt&ZjBwQEUx8P_=CX+PbJfNb+ zdDOMksxG<(;9%#9=5)@QE>K{$XA1PkcpJS)$yTt@B@yLFu2-2KERNBIiq?in=tCS} zCqePIxV?+hpoTa}YOYodVh=M2B$$XE5<%e>k;`C*bU9dd1S%-(^fNNq{<8r;N85eXOo+lHQK9z_tusbw${jid` zmI}zAcV*}Xn}DOpGX={lA=7W1k|p9OFipJ3f6qKK=T=0j%nKv$I|%&az*!^-lh6bs z#2ZB@kG@E-T!^pTMr1L>1xLVlgL495+F78#oR~Intd~7qgxALgln21hYRl=hwP{6y zRfzH8J2}&qMr&lJpli(_N$tmoZiINX=-AdRL*hGX?RC=#qfjOS0t%Rf_o zMx?d-HP8|~DEVBV(>O1b&VaStcx`=3$)cOh>ve>O?8;p4rrYcXekOhQf-A0h$-WewdpJiVh$}jhK)V`y`PBH`k0N$as;M1FT`|EE9m8KFjI1vzVr&ES#&7<+7Ijj7#BscTh*9}@eYVHtE_6{Z&IU||7lkDF$|*ns;JYDRJ3VY(5! zr5G~peHqcdIc_K1;X1S`NPKV57M7SjrVRd{i0 z(mHXqAn^$$cVN{7U~}~j`S+`4va)SXK`)@~achT+db+svF}hJ6oVjtrmASrs4w|1l zwtF@fw|-FRXv&4;410$2CEFv){zf6$DVSBsE^qfs7*7bOV*KYrDFI#*OBjga6*fZU zPKYpaBJy(Nd9a|JiB(4DNE}WhTZ$$L05Eph&U#b3t1wsvyl^rnS%^^6T_XWWeOUEc zf(H{|zzq)pOKmrEKmWE_LjoESX!&l6?$3wxqml7tu8En;3?Fw*81I~K+|szTaqrxN zpDXWkdv&c-E8{k)cckf-Ue^=8WtQy|Ws~AD#pbz!Gv}6*1u;mWL7JVo z2V|FoTANTkNz7Ry3e+|7J==^O<-Ji_j2L5#kx0XowY`uYO{sZ$70;cRllz33>qpn# zeB18bY^~bjZCmYCTby(&Z3lH)f(fBdE+2rKaAJ^c`ZPUK8|rsfj;gxW_pYWUu1Rmh&Sc2SyfY9(Pp#^tTUhfzv~wkw zBF`0VZ=>Hh`4R-GkPwP$g)q#@bz#J`5jpe?`7uURM_7YoV1z0W;wr_pV7#j5p)gks zi2*QnIIdOjt+Ya*l^oZ{tTWf;_sCEt=iU$}hQga>$0`?$`&>w-cx?4-a*1+%`dg!5TqJh!xKKuAKYh&%>9TXfqP-GhW*o1( z32}m#LK{~UeWQPeyyrx`qhaJznb66!w-@z0DR79ou2yV3d%R8MGr5?eAdvZ6QGJ6Vhu}(7}^Up6*uD`IVk|fv||sH^x=%UhE{AGI6k*9FYMS9Tv>c|GCdwS z-fHy?xLNDI&8QjmY#0K_oawmuGTrRnO>Uua`xBHE%>#fNC57@ku%_B zqNyj*r*Czg`una^-!j+lDGWYU-110WT;wEgULGxH5S9njR#(c`GvkOIk8@kgw#?(m5f1J8|fo|Q$zfv0bK(j^S#=D^9 zGGGcqwR6JO8p$(Pq|`zOR=c&YrBi>cdG$>d$u7P#DY~WlyZ9RbI(ZL?^WIke#&m(% zK^8u%^-`e#z0ACF_$s``69zIMOm;e1*+@4FJ(3|MwJKa$K`*%H7Bc`#0T|nCwn$8@ zBC$&i<9r)!j}vJ5ZUKs9bzpJ4zpnqWRO;XCQFnK%d*ecUc(roFSIk9NFhf+%l~I}g zh^7rJwH4Jvfcw{Au$Y-(Cd?#N@V5!;HtT3_D;SqM)LXuy;`n=oTazoriM{b6aZ`6^ z*_vOqZI3shk&%5`T5w`$xBqcd9as&l$x4kcu*zpi;qPMmIA>FI6BsjigUdb`JM?SO zbiWo%53Nn7WQJfIrOSasFyXYX{a-d92hf?|@z|S6`ppcWF{MqXSFInnT5sAvRGSCXg6d1Wj1z=r#SG7dHQ_V!41b@uj{}@z)?alra}9 zr)0UPT(aCB>DPk$jbT5l>7TKoC&Arw)|)tNlQ~_zQ!63fxKNmwv(E>j5 zirYU_*#2Zwm?V<{J9h1+vm|UMT9Uz%E&erght!mYmV4kW9I~nbFaP_1R)**Jl`#~+ z{-&<~ugTsqtsRBbnJ)Eatb{nQBJa8_H+DodU2Wgi6-Upl1#xXRtO8S`3+Dd>7G zgz`JEU~$X=*KR$N+kpiwiGQh301RM){f)$%q$mkir+eAQt7mfgl5*jY5l!~f)`<3Z zV71T>3`xjePqnWD3!0HRQ=y`KiIy+@?`rrthYTcm>%<=0S!-%z%BFBR$FnN`EGgC; z_%$cpZTD}n`nLLt)^_h^)LZ%Z)Nx%*qywv!b|h@G|FLxOujc#CdcjD&+v#-M*lp9V zV5r6GiUeS=!Ty?Nd@=D#aQdFIDO|QOlXt#*F0ammv>SS9R=e|V>dw0rUa8qV8?3&H z4`bhe-L+=^M6l!1E5s@kw?A%otzAw4Ow62?8?SXcl1=?vXu={6(e9=K_jJrUJ`$t5 zIhc0Tr(k6OFeSlnX?VpUyOJcq9gaH|Jfdt0muDP$CuiDfd6nGf$I+*6(JeE4wERG- zgCenS#pwDl_6m_1U{!r@uwF%J}kQEofZNABsSB6IOi@O8E@9>1Bt;r@%=; zqg#+B#1vwWja|0meWmwL^&wcc08ELg{WrStZ<1}5vbPmnn?hSP|1HCi>p%DtkSPEj z26IZ~OsbN+d}{O}-GT?~?zKK7d)7}|=#O=ACn?}Tm_1n z@4%AnMQBXcngFIN6}cT)-dd+=0Pf0YHQ!%C#tj%Z%5Q^?PM1rOT`;Do+~v?m)Tdy% z05DyFo+8c}qU}$T1gFv7QqqT%P2qBj(P{heC7FG8_oHz?=syM7%U~ZN<}om56cX3_ zc2OsD z+yVf%c?QW^Jt&+0&ej^Xt`eQ;dEEv{UZa~fT4~KQ(DC_kx8ry$8Pr{NX#p@^B+g+? zdpyaNl#cdx<cH*@Xo z!r+s|ZI9L!30e3dV8q%YlxF|JcVM~qUtKCfT11;PV6t*oCxD$UGxot)QRF26z%B{? znx=ioNp$I?92%BcTpzRE87j(s&aNt#pboGfjQwa>0B68DOw3b2S)M}T#3MC!iEce7 zzE&J~f01(H>P6xf%z))~VCi$CwFi+GLkAWl?S_Ov=5)Ggz))l#iB%PO(E>0I6X!Ke z`_UxVN(8K(h16N)(6DUiujJ|%U7BU;tpVw^`!E;}qd{-9?|^xfSf{}>)tixYxJBaQ zbgNM$*76JNMOa4z%gES08x{iqcV*j4LMTO^GJr`>xPwofjt9mbFn6k~^+E%%%TT|e zX&-SQN%sV2jp_7R?$w}TIRLQa{Bu_`w}CXkV~~0bQ$@(00P83*&w&pMM&`nqkbYMxwnI)=2=?^}Ijn>3#?CK2h*qwb+klbbf#!+G) z)bvj$dl#Z4IDN8Py$4G@-o076az`@r>}=^nPf7EWka`kRIl$v!9V6y>u$sDve*)8w zK#Ef88(thSvgjHL8Q+fv$wB*DeHc?S zE)tu^h;;$1Mv7g7UfPFJ>Ybp|&95vgJZ>;jV^`?bbNs6<^aD#yLDv-jlHP&UG6PQO zi=JHuh2-Eytx=ZjL9*B3|p}Zzx@kr5Y!$7H&<-9~|$Ly`o+P zO{gUmvc0jc2LX?ZfWzPnDLkI%tw0_FuB(A1y#otcn!KWsO)U%Nh~|f(_Z|Im@#OeJKeT?;x@?lW zfY$b+aU}~XmRi^j9(+5O$n7`*0KN%z%)Yjxgqwkn$D2U=Iw$~cB2srR&@CXTR^NbK z@qMyZ4b+wX;2CgD`D>zSxpAj+wPXNpX$)yerl4EDUl1~*>oatjo8pMF55`$LB+4u1 ztc34B$AC%s5y!#9FOGctXLlz1zHBb)BWb0}vE<^73x)C7L@X@3Q2s}7nAFLD4ayt5 zQ?k|bB2QK^b?-g81&>g?EQ7I!%5Dz1x{`D486*nxYYzOnxqgq^u)XDx`CC7MC!kHt zR*X11Gb=swS0>8bL*SF+5Sy28`q~IsFIHffFjSbv(7RK&hvqhJ?@IDbaJ#Wm=aeqT z5{m=L9?Anm|9UmIcu73Bd5x=Nl)3moBe#XXN|nY z$#gde!2T2XguDCrS4smPC=5PX+PH5%-VCHL)7l>zx+=Xz-<Q`$a!$+Yzc@5Q6xv*CJQWFpwOe^3u`QGzg!ACLZ%GV{qjtZeO3fc5ZU;fmMm_ zinXhdKB06umQ1{MHaAyHinB>?!pZ@Tw-JD68)m@4k#;e&%g4YxLd?rxDl%snH8`>pte>m+C5@Emhf|MX%$Wd^(FbPtk#yp~ zY-pOtebMbdaXEqpged~keOC%9yzD->%>(HK%ANsB#Rd?#ln znIDy&g1%X^1-chVztmT3ya}t3rN&y&J_(smV&;yA#7BvF6I8=iTW;C5nHfGxHz(6m z++MXvOj>2Y-C4ze7Y$>cjV!SI8gse=&lH=}9x(gg$hct`n*TE1b7>g1LkDfpJ{6`& zoS!vMy>a)Ek8Vts1aB?rx6`DkuFFLoN$5X^w4ebm!e(Q*CU@K}+xcs{VSsFk>u&(< zX~;Z{nH%67BPIpoJ1rq`2avs>Ujt`c0eOShWU*F}xMc>sjDN zyfHRyzjZOe{IhiWi>?Ys`#P|iV!*8(>=w|rVCJ)s83Owlv5tUwuNf%45A-i!r@x<9 z8J~$sHNIRg_#*M+#jTHFs-tB9UUY6qDjMg_B?6_YV`MHor(O1pDG!@d9%**JBBVlu z_U6zk(?{ja0mtu%#tIUnTdSa7&7yPc<&h63EKcdHH=Pk%o7UBPyg1|elXHpc9ZN3& zkAhRG2`zFvu%M|L@cdqgE*-X`{*#co>LRgu7|f{#UOHHC^Z}9YIkm5P8T3B%O zDLiiW%sW(W$ZvsOtQ}a=R~z)Xc7whiv~w~R914#U4V8aZ z;W1z=1Fhrbpo!PsMekUS*BGs7q5rs_h`xDwX87Xt);+z6?wP%#phl7wVLW$YF6o!( zTrnM3Nwk|m1EQO)EjR=`rT}~m?B_`Fsv3@JDYB{qYsH5~0XWzU-R+8|h^u=Wguk#ZW37Dzuabx^#0f*uz~4!dRV`7mXyW|M z(v>@A(yt;nD_Z~@tqDldJFt>&fvz26;1p`^z(T2Bs}Pr88O4AV08;=qJlxgFJlJQz z0tV}WXG29H>%=R!%wocyJY*gs42g5G(`rOwezMA({ul(p^aBj{z`Mujo@1L{Abowpz zJ}riD5S~3g+lCjC3roYv6hv!b5^)yfNGkx`)|T(tCIOfp@~>gnA7anXV0I_0=uyM0 zYQjV~U(YIx9V@VFb`F@9E|{u&N#MI?z51ncuTzl~j=gv`3-i^NI^ONA@ML7XzUY7SVIOMKCC*{2ilc?2C0eJ+Vpo7kiG!Q6?(61}my+FA?6&PqC4EE*M7KL$I+ZJ$ZMtF6EAjhaZUJoowDi2uqIOI0 zU;+i;rm#W2>UDb;95bYO2Ha}a-IuxEjrvbR_5xVPh((dO!7i{(psn+^hp=p=I^pSB zUt3T-R$9<13|H!zf~)P_`OvNPqC@oA6q!oHWXhmlr_-8;yIQH%r}x%hI_o~Gr#B|M zyVhVq8%w!PPb%6NN!uTBZ!y4Mk-UGyT@Tt8HxKE4rI5VFE0stDq_qju7iSmFi2>@R zpc4??l-7jR)Dmqk7<6!eM*U>xqUCeqeK z${x?yQz>UMjWa25-2SSjn5I*J;XihE{hpTe{@Oe8#)VGx&b2K@r|mZ{EXEgmFqL2WDOlp@zoR@ZF_{%<7zJt#eisYf9ti;PY$8~{&%lWVW-@l;z< z(A9z-%ziNTqwy$Me@t@oIq(I+V`T=c!WjSaU9z6*-XUE7kFe*FWa+8B!PIVB+LWQ* zy!OQNC+3{RQFku@UIv?P!99Gfi^nb)Z}C|2dY&TRg!(&v3G7S6{u&hC4=IYo54xQX z^90}~NMUCi2d=;?rpji(Oa_Jx3|IaFS#_30S}J{JUqVMuuYx?a2(9z#Owz0 zn1{qIt0Xp_f|lEE2K7WYlW6&#X@vpHkJ_zg#BHda7uWRh_z@3}zXzqiCUOMoI~4#c z0G9i7xp#5&Te$8glfAI8)3yhS`i+cw|B{ZOQSS={3*$wbcJf90kB*y zNO$aZdt0De=)j`4n~ZD#rbk`7(y-v-UkZR#REtDfneKc8*MAOV)M^6Vh->%D1DjC{U(`x28yE*xLv3e^l;ln zH>Z=-p={#76Yjujd4W|sloz3jqyU)4)ay9;{luXWGmbT=oC*^O%BP+`sQ|cY+y&N5g??b^ z08HUoKPuf70CiHjq?X4XYZAgJ_e`Pwl|m5dnqvrCcgMiPMwEDLn0bY z1J1PF_IPFyG-0*518Y`Z3$*egRFNP6&-LdnJ$dEI`>%cfx#4#3j~xXy7A!GdJDZy= zsPma@ocH{EZoB@U^NsdvV8P-@LDyE_l-_|gCC`w!N6jFqNTLQ@=q}!R->s>Q(}=FW z;qZm&p?9aYzppRZjGu2yv&p(g2%1yQeLvZ}p@!n+2-c zIJ5>dD}j%C3ux4Z(#5}OtvnWO2NtLlv}#tH($s zd%-ztOlPgBjiuC^I?tHSIo~~}%IfWl5`eGr52QSG-S7H7>An@?0O%A*YJYfc*2JMJ zVE-za`4FU@@Wz*AA<|-Cz_nY?7K8ajRR*j8ScNPArqFwrLhqJIB*=CtpqB-jnJArn z?apKW!=|JU#=0+`o;1Kx{r&TbgYV3lwuBqa|Ne{Fix=(J-!M%j=FwzKxV=Tz>59pO z;gUI*98-$yyFc~tNH$!LjOM)Y<=04QEoe`ANW5Wwx!KnGf#m|QQ%{6azWpLm0IXs$ z0dTRWFt&SiYRjY*X%8f%+%iZ%bEkfAXU`+6`_?3DpMYt!b{33_9qN6ZPsqwYfBIAD zU;ctVJBzP=*L?2z(&59Vt;U*KTUmNl-eHC!Qvlvt$yyJ9Hqx-~(-=4hpm=~3cY^*T zrXGV-cT1o++fqNU+$OBM;M88)Ee7$gYN1llD&_^i`89L*_KZ$%aP@G|>*jK>e0dV6 zeB*uG=qJ^-@bv>XKJ`y_C4D^h#*#Lg(k7HXWs7lp(mHxYj>vXzkNfs_cazV2I!(8! zDSY{r(hG-5$BtQ!YVew0%mAJP>zw~#Ybo9X`#oZR0}2mB8>3>lSRQM06|h?D08SqmI43+%2ZBk9__oGjtmt#}{96k@)SmEmT6H3mO!`-UjP! zVs%2{QAj=2aFMt*D3UGyRcjwMgC<=BXj(B~6$=HxcGjHQH9ooHo}IC51o2YzK3Tpn zGkVEq|D#49=rB`H-X7dG(~p0F@Q4Ko+xTMEu?7pq6_to?5rCyUPbNcBRr=vpuO>hJ zKWFGRI_f-su=L_f=IPUxI`az~Il%8h>3akphV<~M) zC8C>)D|gKEH|0p0^!C=j)vjBo{ld>@={7RrP(n;;@%i(rO1!|)V3Bxc5qJ@{Uww00 z0kDd;0dQ$mesA<k9 zC#30bGzGvRYZ=u4k6)x)_R=Ma#HE9WO4qNeHPk|eQz;U6K*2XczRxQXFZd8}ZS9dH zI-l{kH3&c63X`e{^#%d1~wJ}2_q8p62hl)4uNvp-S2Eak=-mUN6-SNv` z=s0)Yevu;a!P3aB)-RYfYqbCTpSsVUwdk|a?cxQySW?|zO*77*Nc;q(8XtU0hymAD zfmR(@DwYNSPY;ggHqPeO&Dd$X+`H^lSZ|PaW@@_hAq~qvl8TlzDgO)EeP`o?cLsE) z&gEr&MNfSQ$?CQwqED!f=!QaK{uwGWNb`>e_UaGp?f8{{-f{Yj_2NtBSH4ECeyk}E9K6BPSch0(a$yQP!jmH=`&q6CE+;SQ4FllT48I%))iX;H| z_6KhS+2i2z5uhzaU(Lr_QlYuT`x^X-hrJY!*U$ER;QC;1p|RfM9N2%R8NZfzov)mT zzN|W;>#bC5JovUu`=h=-nRi=ynLYHNv13sG>%VDbMwtRI`=hP{i^rA)mo8Ce?D1oT z$B9K}8yRpC9avL}0jrP>zyY5Kgsc3fDiSm~S3WfwVZfOI>a3O8KfG<*_?o6(*m+BH z4x{#X@`>opUPtt#N<`O+H!c*$XJxL3gdt!lL1D7BKis#k*1K;Quy?Ocx6gyZ;{+os z4}j5~L25SxmRJMs?Nk7)!n^qd*=EFWrYPuV)+#jDZ?n=Wndi!HlEJ&{_ukkN(`?lU zH2#^UJ>)?0?9{s7ljcww(beObk$4`g^OY4meOES8Fs6`u+N+!ma#U1^`G?AA6bz6iqPDhPfzQls4jgmo;fxv{W+xru;V~$>Kt+bcx+6~ zB_|j;zzbks02?aH{q-ImZ$-_|U9a(oz>UVrv3J0Eh7_NH;x5pi@>IR%(+D=L7_bWI z08AlPBjsjhqLnXt_dy?qwepW?OW!fPY5T}V-IfesI7IAkYx?Jtzpoi+-JegL>Qe7I z8~0}H*WZ<&Qs&)lBA1q7%a^#eh}#08Aa8T%k>RP%pTKjHMel zRB3qu;%>u5Vu>ACE&z8c0-(YJU|kP;uP7VtC}y_b+%Rx^z4Tgd-LU^g*FK13ed38| zS03xWf^k8~W5I!A$JHtye}-@m8hU;c}ZGiR)WFPapIZ{Jo( zya2%#i76+3C{iSzmI}bDln{rCfC2lgckN`~){*sV#{0=a%~=${{!`ufH|_9dw>Ol? zq^{pdukg43nI!_ixg55}5oOOHISHMO{re5N{p)|#aq^^f=%v!jFPo#P-MZ}&6p1Mh z{*YHB-sx53z#RgX_I7lnNhYPzZz?pSub@7(8{BF@X2_G9h1^;o+L z+KfSRD|ab+YN$9gnuw~CbO658YS+>O4=nr;aN}o#u7e+W#Gu=+e-VxzvnU~c>7~-- zR4Xs4>-5+S<@?m)g0E5}UI*Hf9w(NVf?n0B^3N(N0Q`|RKOk+WR|+ovrJi~i$=c-a z8XB5=bLo>^uBGCNj04B#5_;xE(gMA1owjz3%mr3!L%=PZ4NZf`KV;DDH-5eI@DcOH zmrRPpvvW;+D0c7Gzxvlbf$x*@VQei%!FdLX&yb=-FU8f}D*vpa0>Fz3eQ6Yn*14~a zKKv_d+wfZ0Wm6u!GK;~s1J9jE41i_pP`1{AC71rvn4D~h0r$*H2=j-YO4030ztQ>T zo8}9LO0T|VHZl+Wz+M9)zE1%jpFL}zRl3&A#GINzQjr7zFRHz{)Hix;{c!${m3j;I zJ<$#fc8kH&x_)cL6@%ZnkiR#R)C`PVFG6TF1ueG&tDOuu8tF9r$d9DxRxIN8e_(#? z>&0Wot!5V=%7<|=gi_&9vFHH!R_FB5^~3kFWlr|i3pcmy?&(|84#f;>S6-XZ$?VEn zmi|Km@{^7;S;yL&gB8<^m4y8f2f$J}UuzS<(mSx+E4KMLV7h|r?oI%J zYo-MD?ADEWFRgt%{`^m7_TH}mLn;8PCM*^H8h3YoVHoiB?uj$2Z{O*1Ev{MO8{FRf zZ;rk+vSoMAnvLyfl5$pA){cVlUUntC@y{fgf0oXGFD{M_ESCXKItzO${BWnDZ#p%P zUFPR{TsbgG4K;?Of})+ZCvR^kwRJ6)H_TIONB;YL?|f(LrMsQ3wmDX?mhhYAeYGt7 z&fgB%R+~Qm_Lic(Vfk0R60$!^5P&Z&iViHtX=M&rdP6vk%U5iyxg!hiz_@LjB6-yT zSk-`6Jo=jV=P3F@duqUz<_z~tT7Mp57bx*X63O@?_kSOW$8YBk9~yb;(*teF z3GB&{e782eoR_Ex*&ihbz!w%r2iD{yA};W$XfP$<&mhU;{mmV=roprS(0A#Q{lcNr z!9z;sS`1YKUg_xjK+aiHW_k$%Fy*@=-ILdQCq}xaZQ=l1;=&pz|tAoI(o-?+Q| zeZ2#_dfJp3*f-}kWEGa}Dm5Ydql8eWSj6GsMUjGTRfkgkg;ci#Yxi!M$38ypwD9iJ zeb~Lb<6r!8$GP+N!GooPFDjYq3IMAb@QMu$&HXuNRoK!f8Gs9hdAD=+Zr9A6&gqG4 z&Ii$eH;^irR|aCuWyWRZi_9M)y!y?dpZb-(nT~d>95F@D(OmlNZm^bNQ&vLuN8&A5 zo)_WeD~{9reLIm3tb57oP|8XPNa?@|!ZcxK2Iq6g5A4++*xT{1{$ zfLCf5SbA^{d$?*A;c7h=cdiq{_gt6e{M&6P84=J?qJgCE?9QmCkfdfu_4!s@WRDb24E8D zz?xVv6%HXT)nveOJFr?f>R8fGKhY08XwdD~zX&HzS}z(^$H0HtH^IFiRA!^gA{c6O04i*LYN;R{4qO|&HUNgyJ=kikJ@zYI;$-6 z{#@$a&SiETJacR*<)7<p$qCt@qWzGW7cOT4V-_ph97NqeG4dGv(fZG`16v8!Deyxz3Tym4juC`Bh{f5cyI@Wz|wo;|vhB=E(>(SZdEszb@G zNZQ+hHQoXPZfwm#k@%^nQgr*RFLl2DIz{5rYp*huZSs)<;?TIC>vF+kempVDBqp8FBq@z=LFpmssJvei;`uPv*l%VvXUuOvc&A zX`NJ_6T&B<-*u06Z$3ScnJWuYIz_nr>$vZ$M9Z}4+}Q&qu!(VPnG6TFf&PN)TJQYQ z7u-4H{(j;r5AQMP*ZcSAJ9p|Wq^4V&L3(h%e4dfC7h!`@y8tY|18V}4K4in-WUgO) z$$ag*W}&b=k$6pyDgdvbaQZBBe-8Aj7qbh%F2VI67Cfd~X24i}#Nml=fWB<1nJ}w5 z!ZKj3l5i#r0MlsQgGz51~lxv1Iq_efR0vENL8UszpAJfA-%BpZZjK^Cp=Kwej)R7_j8UV(py# z1G0cg=$bOK-es+D%>y6n#JPVR!R?9J}XG7+!EZRt$KB zguSt3?atSgH#Yn#ug%b0A=_N8^#aFt=_48`V<@QGoN!hL-btdXz)Oe%j{ z=Dt{F!`a(CYfo)X&(wjsYrpGs?bWk`?K_*x0E3g3pKKv!X7Jc?D{%Fp6nV=QZO?Aw z{`*~iOtHB&2@Xn{|NVc=|A#-xfAA6G>8H~_{$pt=YRr?9EiquJO<2Vu4qdMaz|sw% z#+?N`WctQ^=F@4qoqXH+g?~EJ)ZYr7@N3s-ANjDh;CQ@F0q{yd-)%)}upzZU!$(}7 z0V5B(Ebd~ZD~hoF$sYO!j8>~Ws0SEKD%h-*5juQ|GJ2QUwmzBde|uBM?S|Ag>>OPF zqVvF4G_8$*NHG}GXk1y6_2695dHHm1k?$nE2(Mpvu3s;`{ECEUqZgHr?qB|L{-YmF zedg)(Q%|Pmlb#(NTlgCAmG|mQMa%8Lf&{vtTZ`J-*D*L(ZxFmH9-p<&oV8mkH#8Ee z32T`Las6%W&7}r^-LTo5vp-G5-o+Ti4X1dSc4r@bS+Yw=h419M>K|mNiO$w zfE0|LcQ$ri>34r_98oUe(C;}re!Hz#vprA*!!a&)F1h*OS5D^>wLeO z?G)htGna{U!0_A!UUTkj%R%^Upq6|xT;1`<9g)}<|F5vOqyN(aW zt~K&`nRvWTjUg@L(9nEe-dWp3+k%0WvM$@(96q5t2jo?7v64*ofU`VDq=hY4gu!5~ zQUeZPIQUzlF$~z$I?k=@JhP5it$bofzlGfo;kwVYb*kOoU}DFtajuJ$meMr+KPAq8BHeZj>XZ;SRU~e02w1uoVQpCw=^a?D@%}`#N1E&gnCBq)`t=&!KKdhN zJbvF>Xa6^JV^w=kciJ%@n$vHk^=nyTE_N#X+=)3`$>2xdM7~J;^rzE5^iA>Y~Mb~H}}~&ER~u+YT3(<@o^k$G6^hEi?lTf(%8tPNLNNN;N=!h zk8SOrqYSu(zctF#mqMcxs#E#2Skua!7(-kRg(Tf?bSA@$A@2I^S41m6Sf;8p?tZC(@=yLxv1!&!C$SN4dnKk;EwNOap z`kOwA*Z-Zf?TZV8f6gk)wU^peAhWtQZfFyRHf=1PF0G+a^HlQoN6RB7C-LjwDAH~F zcKs)wPJjAS>CKzV7gdWw&{BthYY$~*(qz|8*$#}&^M2{1ADY(#mutl_IVtVKyjB75 z@(O#PXzeP@=NSy2=9c9(Dh*c1DmMEt%-AQCF<4_}e=9#~-Ua55A-TEesSL$k?R-C2 z$)e-!uVTk;T=&_9%m!n(ZPRUA5iH`uFa_gDgA^B-7Pm)!a+$@@ko_P2q*5gQbo%2z zo}OF;I+V5RpnLAub=kM~vBlAW)$BO9+*SNjLYL zuVU56f^*EBS{eB;LBPHYQYSW z3oR+_Vn4am2V)Cq-e-=(#eeQR@K>7NwJ6`YF59NtmZIA|KBJRK4~(Y`NUTL!(cgs) ziD+b+aP?~K7HIiP-qAKXF;t6dLv~Ys401cLz!S-m#(>of(h?7+*S7Yx*zjB(fLRkj zfM^4e#^7#{h4!w^4l>k3uJ*$1#bR8azl@iD)w%yqG_sgybcy>$La$dab|@GdWJ;$< ztWE1+NxOfw-~LF25SK68Cr?_t_vrI)41VvbUBitai4H7issBiN6IRpt=Lck~VasH| z3V@ez*c(i1PY$GB;6|zfRLI*KMPJ3mU7NU|d$$8ex?s2)@{1AWrVr!Q-*I;S?h?uN zXs%}@^jS8zy~=Kynu76^3(2H3uR~aH`y&+xo-5IfezSR#<~ly@DVt1e+oo$xzPGh~ zkp~AgErIL36(@$?1qNX0Y9tcpfK?atQVrsI0l3La+2J-}!Eg^;?LosKy(V;+kf~g7 z6p4$vTg{zK!(uLZuyblb5*oGH3kZx(iB#Q87(wLrK1`;DJ?+BMT%wQ9atI6krP47k}Dr2YFA zF#}dz&`a(n+~Jt}=CCuVJ{`lK*)=r8hbHjBN#{zBbz-e^y?e=>DKl%{9UL3ode6}v zKWXrT12-}q`hD;3Uk=wn3UQ6;tnnz61KO-kX55-FpMr zXI^;jxm&kxsn4gZxO>;3+YeqZQ{+?%cJ9#qthEoxjf+v7Ib+f7_x~{W;U9MOH9z_z z>2ynrYR74*1}qhTr>F7GohI&m8#ijJS4-XjOA_VjTML0PZ)z8n|{=6nr9 zFIRtkb6(sto!&F;jAhLeYpiqqSXu&C%v@h?boERmi4!5LH81}%}&}KE)z6#(UjwaH6)z{a%eQ^7O4?XxBU;51x$B#dI;J~35 z4&J?cSAA9`uO>~G`87-a*syJQ?Yk!3y1R29|8X}p{ece{q#0})Z;AQI^&)IufgI71{@3^`Jx=gx`OfXoA|L?=EYv?OuseM?cdfH*9kIg z3c6!k@8x=Vs~={);@J4&SFV2cf7!EkcgJ$QP%!P_TBmG6i9lxc@gb6s{ZVgUpZ^UZ z}2UT`@5ytE+{@vdf>9%>Z)>M&rVtnDnznU)q%g#Sf zDj%ek7do1I^1hN%;sC5^W%EG?%OghUSr8%tl6ErtCNmFAIz`>O&}Uxgv#xhboReuW zEucHo>t-f4O-`+!cC@l^HO8TD87hiI``wK5Yed_>0vze-+t*`1`*y)e;8?r6yQ96f z^pPi@dWvqp{eS>ifP=pmBDS0Kr3~B$5&@ znWRWiBq)tF9?u)wd-(Ru`gwL|=FP0vV_DXE{dRnf{MM5FW;~WGOSWZ+1PDSPzylHq zk|1#sXdI13-}imhl@Xhnkr5e@S>5QW%IYqF6$}=ts;jcHva0fn??1k8em?&z&b5(h zk~qEo*I>N}*DgIv0>F0!UV#-D18&Fz@GZe-ib(v9Wm>X9{UbV3l_niF%smRJtv9I^ z8R)L*@V;pp$BF)uL{FKC#ALE?Bach0D7H|$IA1?+UK$#q6p~{uQt=qz6O(PFiv@|V zg8m;99}5fyIQU8|8=I}HT&t>F*Y(dn@`ySA^Y4A-$8ViDbm-uZj{In4c{%@+&b1+u z#1B6l+W$=W(MLlC;UuUzlMC|KS2vph2iF9g2jJWvq$VEUy^__Wua5yE2g9()1T z0Kzk>0+K-|+O`F}Z6Qp+h-ze@gbbA!qeW!AD0rtM1ksY{a@F!u)zV`1l6hT#uDQ=^ ztV#cjEkocvjt$zxv)9Wif*+LuZQ&`Y7{0Cg>iGnN@CHn=t@Z>QXVlddnxDD+1Y{N zD^d=fmjtsv>xTC5Xoq{hiNRmd_1`k^1Glq1{lSB=EN!`BeE=**VPWCZpZ~l$mzS20 zA3t{RrI+7)V{wUo|DU4fY;MBOJQM!H{_xhV*3Wh-^$>KhGFSrxMkJB~ z{smrv1^E#4$79eCO>_ohu8=YSJLz8FmJGh2<(s+ULcH@xve>DA90Y83wVz7AtUpbsB04zpvN%8*Yo-^md z!h*?$fB1tRy#4l1j6`0gE4Rk*2>HvuikNfHo%;U$;lBP&ZUWx)_%j4*2Al`r+#Hs? zbJsF0*c?RAWt$_%0g2(OjeLZ&UKbh}+}Z&mWM8omm)&pQ$*w1ydnhT0({S9d@tD}p zCB%8UbDaY`i+B@~1Wj-Hvqn6S?(8&zj=7p$vvtXiHzdBUX|tfccpGByho=+fX*5e4 zFR#e7o2#s>?1dK(m~(b^_D8QCdHJQ6-aUDekN|eLsj2DF#~!qr8L_KU1sW$O2l#IWage$!;0~kBN1i zVJf@sLM(NW*n(&AcutN5goAVwIO zChJY|CXSCG{of@}#clG9ipAhZM{f-PmQHZNv<~mtyT|!L>-wF0_L%d_Kl9J+V-hnt zvB``toHyr%JRaXd0GyHvM>Z<3AUGY$@?u;VLiuk##l z_I)Gm)sb}>0@H4k__wqLBEA&YOMJPQEa-Q67!0PjtDe>fbzYu{bw z*sIg^mw7_n547*G|jsY4>pQiA^toYoDOAWKK`5o#5YV6 zyEmuW*+1fwaOF3M4#W)tvrCq;C7{461vj%RaN3}5EwrG*ln1UopzdaQ5xCaFyZ~Fq zU>9_)_GHmjWF>dq+H3*dyf)jstX(X|EhtSZ&fNhKTl8mIJ?D1 z*#O{67Xtv`9Xodv7N#Q$lN67C=ePgQb7#-x&sM=;*bII5y@WZzw!6j{wrA@jg8g7iMjk-Jo-gLTIoiNdthH~NzzkHRN=o=daop2a4?A4yk zT%8cH;JDxxEz7Lepl;uhQYp=!F6yYwe=y?u9>~Rf7k4F1E=GS%SjBSi8M#ScL5JDK ziy{RN-`I(1nlzSPMAv?MVfkm4_dZsK@vX;&kNhZ>?c(3^^56mZ^5x5!Z)ES@y8_u6 zhQ~!k#rd;UwoZb?5{fbBt4ExB%F590U5p}Ircc|qYehxYuP@aS==a`Fy!ZZMWhMIj z)0W=#o;?BJaDYw>_4yL$O&k~!TO&qmu4n$?gPq4bW(iWYyT zq3=fNG#rJCA5Px?RMU>VRkv0|I!p4CS!KZEV`H;3GnsEkRA2=%CMPHJr>qSd3k&qr z2MKcm|8=*u;axlRkJ9Rcnw_QJ`Man&w`|eAus{6lGvVgu^gE*h)S=9Wpf_vKVk6$Q zvSHfJH$Un9J?;A%p?qoQd@WoYwQyLff~Haq7+fODSS%In7FyK*A4}Nwkdh~B^oNgMs=h233 zdn&PZ3*hkZFgblX>xqquihx}mF9neRhSe*u(md^^UYOn5+G-d^-XJ-E(1wjC&-k;iubA`Rd-eU#gr9mcT$Z{@SAb2xc?P^G!)USVt#ZUumq7m?kdGuDh6hxFTk8Hb=@L-{lbO{L+pH@jiM-`hl+#@HM2z@D*gY~B3+NTZ9i#g$ z@2pxZP2+$}6fVA7-#=cs=)re$ewD~Bg0Ci3UL92p;em@wzp{$Tmh zM=ZVT&pez$k~lyHoX-Jo^h&OPBzCWm%0!OrkFf+j|DMdA@P4CoN_l*kb6HSXED``| z(@jSnixXKD2lsI!@+?=K5p^iP3ogMWxcZ5;y{gkq*zrjXQ zFTxDRl^_zpX;)w+9qCybn@bb(;N1Vf{pS2nzx~_qo;-Q@&?|>ud1ZEXHh(J2g%OX# z(br<;tgJv!J!R=#@7;TI9L|TJZ_z>Oi!b>X|WD(Zcp$0UQ-A#KX>1?l%&;88*HOFvcR?iygWw;?EdK zmCwj+VZsL9b7^PwOls)66pEdz8NOULK@e3ayL<&HJ8O=`xp>c!h0xoL{bfeQL&%(! z4y_j^%uwzuQw_807bCfw4p>asE@CkFGZRa=Jh2Y(}oZ-^## zu8`Us5~CgNT|E5Xd!r4B8dBAKy`f+wtN<{wg(n%liAb*|u#fl7_**#cO3Za!8Fl#z8_dBZJd45;KS>$XIp{i9YyQDPpZ4pVcQdtB0qDnA1z7?9Iu zr$tKQJv}{{ZztFatnu;j3<22vX>o((-~H6OQ5e^yB3J*MZgtU zGwFq(*9s4K43ZDN^z!S+kLL}Nb7Y)8oiJz8B=J;0MOgXx^T&|*QbK=naQN(raXAc( zG&j1S)$i&Sh9F9+d6g%o1G}M z1i>C!=?H8{sv3wFTVZhT0jcoH-Hrz3E(vTPjro8G~z3rZ-JI^|GABV%oo6D`vw8`ec`RkMjN6TV=WY+l!hlMZIA7cpxi zWJjSjo{J??%-Mu7>`-C1!dbc^^0*9uWfIKltuT^go-u!}ot8!fHC<_K>}|Tq(gV@) zviaWXiJ|gY!(28n+~tZN42dyvmjd$ofXIDQe80C`XvM~QL(I_;nD{4ZPV0o*0p*`S z<}CX}w!rx1gRzbIN(Ej59Y_z-?#|8tE3homGdZ;ZtCQlA;^)5b1#>PeEd1!m5t9>V zZjg*I4#e2;V?Ji*==c6UYR>jH9E}BYPR}#oY_ZG&ViDi!L{9D1)X2)=|9*X8I_8i% zQBj0k-y>`1u?5499Us&BQe7*B2@?zX>%02Hb;O!v>5y4I7FJ6O6-FaN75Pu513HWk%0010p z0vN0+=(GSh-2{1AS=krA^d)o7&d$Da=+NOqucU2|{BytX3%~fwzjXfG`EzH_TsVK> z!uj)gMUhNK|3F}?nWm~d00%#e=9tyvtL2t>aL>8fSaZ9FS+3Y=DQ3l1ig2V`jm4l9OKnby{gKE?NA>zE zEj2e9k~SSLl`c}ebzYpBvj{n+Qv8{hhEd7fkS!qC&4Q(Z z=I*8-mTItajjmjS_se|16{)tZByop&QHhYXQr=yXr-76OZ28KC5;^x9@n)LrzE_!0qwtH1RH{$ znwknktzyrffB|@FD&-<9jB#mcX;3>*TU-0De*Wjo`5*tofB5cq{w|ZXNJC?zIiL8c z&*kxWb4Q*52R1aC*b+5bqPeCM99=e!e%ng=B1odx=pwLh=rSvH6Q#V^it_$o3toBF zmJ;Vrz{AlZz3h1*N+oR#>f0;qa&zT)QdB%qRyJH+JW)=JO-Qx-~a}xjVHE8XhF^uI?x`}_vcGvXjvc{;xSjz zh%4tk1qxXGw5;98iYH@9kdni(bzS5ANS5ldUBt!VlZs%q0AQlRzF&fa@1HEN<$-PU zRbq9E6AkmgtD=}wlfn9riIa_%+n0eSr`>{TVl5ps?s!Nz5+m6~7^||HVqpPLHdR{R zQy#Rr3}0U~r+NKf_TF3AuHi0G0v$M%LT}3&4RS zfOqfi42Q!3190j%6nJ*r{zB4}WWw&e^UhRt%KUhI=8Tg(e)r_bSS$wl5ya3nR9lu0 zL1*@JC?ts;Iah4uEDK)4iEnCK<2ALR(i4r>$BGu*B4Q*dOUkMkQnFQd0L(@#z%G=E zR454HW#nv;L9#2KAyq9=jmu?tNG)bePNE1}j8+98PSD#7%|UH!9-b13bdHwN69_{bz%b)iH8I6@1%8PKMALgQowy5PKn1f1UaWJ9R1YmZnG+yxe zfv!tcqi1Rd6PS2drBgV?RWU^0knR=gP|J2PQaG%0xK#7Z`m$b^weXB zq}E>{S>n+jc?MTUnsQN0MBEob;7*WDmc;wP*5lbKL38lWCo?JE;EqoY77|u;hFad= zqQwG;8YRMso_lXBReHsbD6hWu{4#}nSasA}>`iBZzKh{qJH4&Jjhe)zOB-5)P!XsZ za3DQMfmdKnr4@$FhD~y+$0yjeNZw2$z|oZNK}wsw)W;LsA~c+f&2@APCH_#O*S%}2 zu9?QxrOJ~{y<^1-z-yUhEe-N-iep|f0grSS$QLWE$49Xgi9J$PW<)vOj|nt*{EF)tQ(#z@lPN(y+qM2lg+ zd0BJ5xo{=`@1vEnNbh|&Vns0wK9;Y>yDd7hClu3fzv z000MHfi<U%|V22_SiRAS%g9PAw0yqtLD>bZ`bB@(HyDNitaN_G4tnw=< zixvJN=LJ7x2-I2urB~C4bsDS569SFp3SlAKDpbnW6MOf5t z-;01M$`-OAp>vl2S1g$~AjzG{=|6T%iCqd0+{%yw?=k{z3xQp-j5$vi7%ZBOSRXU8 zYrIQ(tIB%}8)TuQ_jstkrp(K;)I>2yARnraZVzXK(NI z%;lf5QGu176;d_;IHd&edMdE8y$UhMzxdgoH9yuL9XT>LHIzIU%1i_g7M@5VKrKOq zz!J(q*ZnK?mSKU9>^e{9<_dzv5^#C5*a9KR6SAV5%F)pTzBQ)lA)?%TJ%Pb3@OHe7 zS@zdHo(1D2HK$rL{Q_Y+bjR>y`xH_w6y-zU_C$##4r{yO)V^V5t+tlGF6y6)Y^XnR zNlA&O1$G_@MguDg6<8DF>4aXhQ3c(dN|9O`66gX|AMfqDT0457anNK< zqQX}oF=0X+z!fOjpD|Ctg4C)@h@=V$N`o(ODwfEuG&aRh^&fsA65^&Il9r2nSe{6bFu;p(w4yr8h}@pm-}w?rE(~OG-RAk zmjScmpzAsl1BphXuOB~t_|PlIk01Xy$Fy7W0DRMdz{K6emT2xc`1%Jqi4%XWLF!A& zos*5t^HpcsZj6=Au_!d*;afyNcR@nxRT9h+z^y|@Y7Mi<#UiRGSRr5QF;zvcKq{&N z&Rhw&k1;qY1dfi>N1v5YId6r+h{u^lOOkBrfRuf6g4j0R;}yN^*ftzZ|EEF`o!B-x zwriBtZwvm-zhKTj3n?;IutubiJIlGTJc!#!u)HX48f^&;%<4g`Sr z2L3iTMhN<*pqNTgZR zSOXXI0P5CZ$pDU}Y_Wqzn0F2F3NIqU#e|n+E1w-#xk8GuAlo`)&lvF#I+pfd3I$l( zk7`9nJG6!Lcc+Lyod&pOR&-x%C~t|8cVbwTQ`W~5TOzb5ryWG- zfE{-351^B*b*83azT#r*z>Vq&2QC={cC{FN<;ntU{jX1`@-Qsnvur-?STA4)BZ(-* zmMnyUr)G(K4{*O>D%EZA;w%Q(nCXXb4Gs1*#&p^A`x^p}`VzL;1P&kWz*A`|UKzSE zx_fkb%M`NPj^*e#237q8DRBWu-IF@|k9fMhI1#Q*Rsua!uzaNHih;Deh@&Z@2KctJ4JIuC6=U+S}HdhMb$58yy+VaDTE~cTQ7 zq*KsD5uMsHHNJB^5l&#lsWx(87$FyZ5nmZlq&P3Jd#Ek!#XK;~zjrKD`(JAz6g|>> zC60|q4EIeI<-%8UpugXoM}PDqcC)Csc-PLICOubHBP$bwS+B%;cqh+4#) zh2#e##FVQsQ!0VF+KnU9y%f0x*m7)K6gjhkVJbAMMSqcSKthhop>rL$9yJF)D+C|a z^n)E}JRS6%ZJr(5JsK;HF~w2#(9z=0bwro({Ki^JE{)G!aI@v*VPuN;2qhd(@f=FIH{!1)BQb0)WPy=~DP z4>&u-_Y5-rt461@c&5b^?Hp-tnW@&n1E?U$Wl7jfWqbzi;s=Uw6$p(E{!EILvMJ&E zC#z*4kgOdfa!h4P&cfX`raD^CQkRrM)-k8QHU`TyR49j7JFpmnA9lcWYF2z!+w zo7^!GDPLg`bruG|h`%Cos?{)dC9lOk5Wbg~mw*xCX3$MwtzIgrR{M)g^gY~uX<2iE zAe$0@PHzm``U;E|CLb8V3l0}gr>GP&^wp}wKv=YHYm&1rsT4!-pAp@RoMx^N-GH)1aMe(g7Y zWWAi$GblyaL0kuVHRq<-8L^WZI4N+#U8Im^3xH5(F|lh# zv45BkM~<|0o7|U$<>w;$=xo6)`ZJ8hV&*9|^Rue^ONqx{edVw@E32xW-M|0EFTS|% z?z=N&euj~9Wl*qQgph^?R?x+0=~bcyrBvG7-298b{7dHS@9TT{rI-Km8{b%1SV(VE zJ9h5)*Z=nCN7*mFWjFt439s~A!rkEjh2|v7|Z46B4vkrQR0s@I=G=BMqTh% z5oD%F0bP%|p{R2fI$0hL>M~~4XhFCsIALVs;5U`Tq8;67!r3ZF8D-WnOn5>Uxy8nE z1C`uy*2yx(kV8rO~;q(=)BpiI4%HRCQtL3T7#1>~u(D%&WkJ5+}Rg zerwo9T&v-VI>*r5bV)S4b=HkPPZus9ZR?I`3B&^iSB6R;JwY9gWLx0l@y$0QbbUB6ivx)s%wr&Y_2c~Co;yAQ#+45_@@$29EAO9n50NyLL zN7vQWJ@)wH=1hXe=gyChk7tbme;h;5w9H7fL`hv7ZWR{_0>(dK0czZ;>bP3qK}A53|q)@2^&G=7)rSPNUG9>lJ1r67w?KD zaQyY=#JTEqeIhzpZkd~FpINS1B&xO-+ifh>T5H%rT+N1I%mG*sWzqWVSS;H+$vhSj zc7(5YPc~xSXf;|g|7PpeC^kS!nO_+xf&5r~+~~dWCx7%u=Dhd5`wo2Z#b=&u|{^>gU8vkMsBJ8I5A)hMqP|F$4J?&)MI8f z8vUt8dp2jWpi@!X%t^2!zbyK8q;4evx+2{xim+VH>MhpdDNyQ2UkNtsS{U(7A>I&- z1kI&R@K!Ndlu=j)U8G28u~#vMi=&6z$n;G@xKt<6riG>U*`>w>1Bk(uIvBLGa&E-4 z%M554*6I=xvwN^q{6)F62uuAMtb2+Ta@(A~C|0n2vVrZx^{R>Ynr;w8k+ggnn<dsd*WR{ zDjWNV?jyhYvrh@qc|~x0g((MqA)Ccqm@xA00D#gNB}+fb`%IR#=!wSo*_t)->ZB|d zZ(WEs&qwMO6DC!*%cDeBw3CFkmw1?6ijphLS+9-Cntes%AdTe%z&oZ0U%?oBS%V4e z8EV5e;GS#l@2(xCL^>??al1T{k3Xk2;_>*AS6?+}Wo6~F`}dnje9yi2u7y2m=}-pi zMHr1nZ}j$N17p?;!0D=RWJ3`9at4ZNnx=Jjb*1-QKOX8abNTARkIb%)73FO zacXPNaOoWDrDoO9>{B2FUCqaz(;c(3v)}&Kx6HX^>z3zVIB?*_14)y_xzGgMbM0D6 zgrzvA=Y$3>~u}^d=-so&hnx(jw)iPIs!EjS`#7FD+%2a;W1Bl2@??wJ3DJQ z=qTWphyVa`wJqJeuZ_Msb{H@VK5P6=q&g6@_eZ$CCX2C~yB$jCd#`uQmM*@({o1k~ zbDFG>6cr$FW4tI5&+F-AI{I$({plb7u{o0_iPKYn3Kn=$ia%$g0&6Dy5OjIqpV#G7 zJQ+P>Z=ePhlN0Ggq1$S1w*)XPHH^AAsgK_pJw-|1LN7}E`KD!jlK?OUrJ09TfJoeFi~ZCB$(2dYLg@fUBcLPY&U- zIGD2}ZvKT4Tr0NBP}XpmsACRA+&3cc9(F0b8+K-z8!Y3?xG&6qC5Kxo>5`4&tRGR^xu9k9_Cu%MCAW4o)4(^G7IoXA*h^`czYcb=69G^3Vx9x$6Z4w_ zq^TS{xA)Z$hKV-kZXB!YYagB~S+bzluq(4{)=!Z8t`i3C9WRFb2>kcRlf)*AeRlu; zo@>|A0iHnBxVkr00}IlvM1w4ZpnH3-XJVZcY<4`oz(DIQ4ZxU^N`q9#NloI`<-0Ci zd}I^fk(^5j=%PB;#WhL6N~yGx(G>aak&w}Q4^4u&u2P5i6X7ftq)KMsZo!h3p-Y-o z^)6>qgvXPSLv0BwKQ=hZQb+qjI6b&Qav`}z94;(Mppl@z0&7|7mtEH!eyrKlR%6*> z>)d|Cq)s;;QgS|)1k};0C#Uv1y<5Ao>pr7R_sBdMTAr*EZnU~z-^KBN-Pri>kv7< zLE=^^1F%TeA}LrZNTtZ^dZj33c=qG#y5Fh-$ymuTi+U@K?-dfsUe&P4A(}+mJK|hY zkDDQJ%A1KK&Sem}=EVD#i{B19s$~O{g{y z#fBr4%|yOj63i>qE)C}6&$&AS1fKEn@s#i{8-|Q&_uZCnsw0JG1XzLPFyOS-CAUfl znidliZb@Z=RwQWnwz9;y`0ms@hlcAWOCH7B3yoa-ICQ~T71c=zN7wj3=qgNiz39ua z)2)K$#8;-IZpIf&D+7?)r9c8~@HQVi`CXd_i-yPP2+{lqRfgB zU#kMkro6i700yTYNt9AkexQhGapr=WNWxu4f-z43vx@84WqoM65b`5eM`>BvrqJJP zRA4~{ZAgJL;7oduf~~+>TwF{q7~ZrE(GWpV3^^9THBpw98nnzH=4E+;hHgV2s;QBc zH~w*8^v06;-Uf>2{;~DK<73;KB4xR@pSq_RmwMnaX--#S!|0}TmwHR3{5Xw0W)FgW zYVJL*JrV_^2Blfyw8%yiVe`ighS?>H=sL8RF<|1>nqlgQOH;|)SU4*PuoQ(?Zq*gf z2kTaUl0%Uhtb$ku@V7@m0$mQh20zZ2FDwvmhQ2tEZ~x8d@jw5<&u`zpy{qevUAuR8 zc6D}jb=5aCqzgGI6#ZtS0?RsWr3Ju&3t~fNww1wV$J2`n6c?k?2Ui^2P9PLRCK96< z2ImR_`_j56+wiATQcP&EL5s{wV&X0h-_9P)@hExkMOc3H$7%ywFL(Sdy&NSaM z(~?v9JY5Dpk1wzKZ}LHsl}0J@9mxAvnQ;nVLXpZPTvrQG#c&Dn)?tjfs}gYgVIs+A zQr?UN4!(+*GvPE{*1~ib8ytf%e>qU4p5@51(4G{;S3Cf5S71;bEJTo;^=Pqo!E(DA z2V>UtgoqRIHuwBuKK`8B!$=rcE?+UHxTmtJYWMElmI-55mkG){ckNtfI5Z{Ur@eOt zAAgR=<0%<2i4l1G+1!nhk&z4lIM52LwBpY#Em(#?YR*+ma8VpV+(u&i(!yqT#k@a( zz(6PgutpV#RdQl&-?=QHw1AipOA9ENmnP`mjs$nrb=#@wcU~D@o{uMEg7kx`A(IV1 zI=-zUo-1KRT_L%${lZ#h&B~|mxk&+%FU`46FbFh4Ud(1FD7<^!3T@sEC0Aa_28bXP zFtcDUl?c7#G%OW7%T;FV>5P49o~~bEZYUzp%x(c&+LKOJ7;`NSR&@mm1`{5cCsHw& zRe@!=F>TBAi8(FW_P~sqn~xuzDVSc&%TL~7V|Hfd-IFKHDcE60N5}52PKzO%klfYT z($bQg0jHAnJowVfCaypK!hx+FTZ7v34C2p$3u32Fe>GjZd(T8dNM`w<){-qN?7&0%{I)(_w3l_iMdg1=*wmowVxv<-M z60D`Z{q}I!WN*y-N1@0eQB0xa%o4hCFd0eDLK5W5<_a#fOU6o~z{(X{<~&$w^W_;| zs=g5AC0FuhT^|K(_3|>o4Y%=^7h8$Mu!dDvU}<53r8oig;5~BHt+8E}CCnt)66Ab; zp8nqQgAmeVk5>*JV)vAlmF?~{(fE$ePV*m=Eq4}|6ekjitJkisf3qe*{Ifs#Q*+*Z z&piiTJn-!P{U-C?6r_P{lt5=tfn^@ga5x;`&ZHNXR~T^S135H0|AX=mlkdKBc=+-M z(?DU3w2U}*2}Ith>FX<=`RrszS$vZXUUh}#(j-tzWzRvB42DJjkFZ6Os-k2SNx4~w zfsk^KSlLkt!;mYA>AbjKo9@1xsz&`=&Ug8#wRR1kFEVj`S|l~ITjWcPQ8E1oP4#1+S;v=o(<8N z)2Ge(mH+&^k3RO;3kME7{<$a8hM%dlg`mp=??K9y1Tds4k=D|z<-h9L9!7$kd2`~! z*GCgkf>uXdnN|U}pC2xo|A*G|_slfkHP?jcrsak8r27gIWAg}3`8lwQvOIAs(zmi> zpwr!}YDt3R+UtEe(Eo>^#5 zO0v%wxt2$+FCiz>GPD!!qKJrhnZiogFy^hsQr6j=*G7w$qe1_M>)K#qApg6~sX;02 z@9Q&XCL1vc^lL|tn$x7$&+LEpg##~s;{H#hTBVg9(qw}HU(SF52i_i?UJ+JnYhF7x zt3Mm)J~j2;k&&gjI6|&304P!sO2IFck=Q;hn^gGy>cMNJQ=ggaXozf1xvRd)NkKBS zSKqcAMy685vcg}Hg5nHykV==`B4IfIE(vi6WlAJ%8RTHKO?+LH1)lwt>adh{CY1zR z;bprFN|zNexy^%!Co`strChHQc1DUV63B+c)GaQvDV=)XMw?kmGnBP%kmO;+`*sw+|-4BKbsnefU>%@=nr)IKoX zT5N$)S~#uM7=d zU64T(l5$1K#$J9f3y@!`(_(8x^bV~>f#_s2{$8ur4?25Aic%U z$VE_v=_2*Pa&B0PdJCsQXCs14t0qgbbTg5NE=uMYq1hPB3at@UJS5!Ex4nH=QbU4X zk>bT%C<9>$dcg@&M-=QF$JqxAfnFh7<5h2MVTtUbQ+yLOWJg#%Cg)aO=95~t_+6F39TBmo?F1s0@Ln^swY zN=owbu|bZR@yL6xj`V&s3)Iy|LMpg=0hnEKrYkEVYm&U9pu!16KByYJTsD5+OzX~t znpI#un*i($qst5!NrhShiRF4I)vZWaI{;N1g*iv0pzvT9;HkSXp+0*=Ig-peeO>uX z%G+%vaK#+jvE6{A@$@i8E;q#jK4nJBBhLuj$TQ0X0|#9MUg}JJ%MsV&0@NyN%8^)I zovAe6*_7WnD&H-CSeYXACW&Or@@O|Q_XiXV5^9^s?hs=54H|ezx;LLG);RLKK z+>BAc~fF*Nhxt1=YE3HneiA{@R&};(KRXwk}m= z?>*|^iz)+EYpEm*7WYtz$$=_rA)1AdoFVtwB_XO1%Uj3`!w10B`eUJ`LDyI&#D8)B$WNxVySCkXIFmb6+_&fTRV@ z>E7&g9~G@n2Nh$q>wegEKl%9$hDZ|MweK#I5byuObLn~h zK=2{x3>a|m6`m=2Qe22+^L*4`4U4N+O9cn%^rN{o3?l@tB&#K0-H zO9Ho^S%GUt2m9z=;az4SJmeZ#tpMe=8d z+iaXWd(NC+{k^XcBTb3m00DSuX=!jE%}%*&RA8mon$_NxXTWKWnX$+R$3}b3%~F~5 zg4?Kxr4u!%Fzf+fVYBOKG4Zsf5J?pGAYYlZ%7!Cv*7csM8oG17p>w{jfCTBPq=S@v z6;=mc5e&(&K}Jmks)3+H0U>GnQx%W&pf&XfU{S9@(`r_SlJ13*+{IjG*`=Ofc@2St z^dOc(EH^BDkZ#Ob&GIrpMG}2aqRd6Th7$-WpDUAk_AK0lfY-oe!Nh!aLbpp{t zXzwGi_Yo9bu`u|YiNxjyrv3sQfy*ZP4ffdU0webQI5mL%{R zW5;XA?peYt8H^lOYEB(VMP#z#v{adlcx5t@RGksGB1RI_vlo)9(hya3E_nVAa7J=yVnVI5;oN!V+}flW^aY z=*D^S^q{=J3FV^rpI=T(y-xy zSXp*Z)?kn$(gJM8_2oX6Do7 z=qmHKrn#IRA-CQeN%3kg^nw!N@Cb7vPwl&$B&Uk*rhmnt$fVf^>hf^mQY35HqvGJr zsOi*5rkf}%ff~sj$UK8LbfAU~xc~Du9vd)rgAQJ{WJB_JoU7y4f8|%L1zu6nu3fu2 z@32f5JG;!;RbHNQb#zWuV3}ho_1@@w{Q0IMHUZ<`BjbNh6TXH=L{V*jP1Q){`0ml! zEz<~75A9jdb9=v6_YTIIU z%Swfo3KHwmJ9kMWP!)z{&PtPzV?X)x-h^Z~k(4C%;WVt`(&WB4Rahyz!?Mv;WiQ3M zz`Xg`2_~!j)#bkYin4vGmX#%Ay}(MYi1HXp)ygmRXb|e=D$H@}S~m4ai-HGW7`c`y z{cYK`mV*h_plLg5+79O!PzC)7;#=+j4yB?6OdKcSpxIORrR=&RXZ7ax z@byf$(7HQ?2j}tlmKwwMc>VZsb~79f?=&fLr^SygRPMUNqmDT=G_ z-rb$MgMAAVskdgex93Zsy(2LXlP^<~^+rcO+4>;<3QNNYY9| z-ew*aSZ2`6VAwz|31*o=RxRZ8=5QF+B!eFh8%CX;`syJa}Q~a#BJn&7@ zk)DhJcvt$*_2BW`1o^!$&5K=~yZd@?q`mRU$;oG* ze!8ox>xCB&Joo(b_4W0^FyPetfZOsFSQ1VQUtL=M+k)9w5lOeUvT&lb>G;l-tuu@J zMxiv0<$N&qb|twC7`tZh7W#6tR|dn*rCpI@La)W_u#{KjcJfFKN&~DbE-l7vC}Pfw zW#i^!%Hsuf(bBp|iFsKaD+=+_EuE@_6`>@hSd@CHtx8aD9z90Mc%!I+azoGpW#z~% zRY){KJYB6aT|h||5VDLV3qb>R-w4RZPR#_%D2rDAdU_0k`F?f-u8p@$B9@x>>f ze6pxGqo`6E9?5O_CSY4hqx;l!_XpF9GqH-v@|}^bnV!9FR22?XM!F{AcTJ$M;fY22 z3!w?&`b#qU+E)zXwjTKt!yXv+=Y|2+BE(6N*t};x6g6j0sq=LzOB7T`i>u>By2n6) zdVGqgD}s@<4HqS;e^SsFRsm)J*GWmJH@iAqNJ+KIqL>Qj76*bT4NI=suu7$oZ#jTO ztuntYj=;LyRMqtgQbPjD7lLxrV*V+b-vg2SpIOp-Mhmm}gD}^Mg#p)B)6C<~o(ilC z_b21kKz`gFgAihZx;ahsefslHKmWpk2OoMcy$bEg)cb%x&P7@b2N+znY82TE(!I*pV zm{b^31%?T)rIleAV7*8zgRv~pXpRLvYR-O`(dEZc6+Gn_ECt1pm#$88lN2h0!g0ah zAB>U_M&xo+koBcy5lSH;f|UgX_29o$DJ>ee-8LjuGy|;yv}-7UP@f4UrO6Kwh!2uD zJeb&Dk@v2a&|8cCnj_=*^GzzS@(egXG8-$)%m4I`-#2G{L&Nhgm_YnuN=f3Cm6b>& zwa95f7&SC}`~Y|v$orVQg%3~k429?AGof>;*fv%=UtB&JKp6^(=%Ke;LKkc4Cr8ot zIV5+AD(i742TYI>g*J$TtOAS~MP~5K@L0gG1HH%yFRDi{=X4nbkD{XfELix@~HbHWx-T`b7O zSssp_1b5ZJ`nk_P^H<;e=J@!!Jg}!yZvt*>$LQnZL%0a!O`Q0e7W;)z z;!iYk8a+I=Rp(~9N)ao98+-esMZwib&K95}ThKr5z@w$Q2AT#shLGk=^SXJDW*1Jg zziReh*-bGu`@tG}y7X7fo@igP>l()n1|k@_rnHWCt*q%JJpjQ?ObvCJsCmMt6jU{4 zmC}K#jDdXpHXeaS*?-qtwFOGDnSxJMO9elXJ@xFC+}fMuefy}hLXN@r_9RaTdD#JU ztrZQD9%k@`i|keGpf7PAZ&2u;2#?K%vinoXLJ2e-&a?+9qYA7f3^)iE#j+~&^#5v3 ztFrpcaeDSR%#K5T+%7{-62EZZz>`lsRa~-ak~ob7aO=mwh>#8;a>|^TUXom0?NJ^t z{KQP_yEXk@&VnEjGJ5Xq-Tm$kEfH9boQ@S6-`_^JEQB5!)f!gZK&OznvQM@quho!s zrgiu(a+om^31F!{OAcAH+_2M|L{&{`aFU6lTZzgLCIk~fVa7gew1qfmGVKexMhmY} zGwn^s0Ubf4xveAP?3>fdrbgzPCz>6~hY(yNW*4^grp=W;?`Y+Mb zN9p-DVSWnoj4i=Te*K~2+q0Gvh>@NHUw6#>$X=!ckmfau6tq;Ah_ zV_(V4XyJl5;|d^%mPY&b4Q%^xD+HQvBYkDjzA}CHY{5fgxGE~JfT5#RjiuS4M_!5r zav?MaV^5hh5wU0{;z67%N!%SIBzEz~2gV*WL<$2&-aZ-dVUrS+_G>Omhy`*kStXiC zEpkRwjq%b|kl!N(f_$(o7inV&jFE4H$&@v{5dirxQSXDP+MVpKHspZ13h<)S*9k=1 z`36cH6%K=-4|+ep#(+|v9Utj-)7oLCqz=5u=F7elNDzI)R6%GSvHzF4>Ifv*~lf$m> z7X$6y`vi1c`}TIGhxWK`qDs3$N%yh-!x}uXQv6n&%f||eM7_abjmT0Ew?%Yo(FOj(aV@#LF6)mXgb>{ zj~CuQ-S$r14bM`RT~2mP6fPB14O9oe^AyD{RK+e<7Th&m_~~&}8fQkdPLP?{R_;Q4 z1KYJ%;80tK>8da+3b9nUOg)op*FZ`Mt;vckjnUd-e zL{$+e4^YXRmD1J8xSyMYub4~KphmJVh!j!=53>3;jQr-uK5Q10cQr#wkwP{o*Z3s- z!?Q!s?_Mq4h~HJ<3^?-&thS(q1A}0|H(#k>jCMZ&yB|Pb{4L82%0%LeKY_>+ zy3O&mul?a4{K10{Js6FqTr^ZQgOYy)gH$nn^ zi3-D*$0-FKi;gC0dDjYe2S~<=!4JluBwmR(1TBLKdnR%_fI+qW5!dM|@dqlSsT#$W z%dh;AW{>CKY5`;q*L2%(lldqiSsRNc3qgA-tJk3ZD#{8K5%(R5pgMI6wy7QI5QmaL0ov9;MwU;p`jq;@f#I z#oJ@pN#Y-;`YiL*JpJ$B+^&(a_Q}E$eMhMJA-V;sDXIcgL!lZ_6@?m#DsNUAo3#;H zO3Y8kX2v2@qbpORk*SfDr8z5+bYSSt+GxOuu!qMxez4_yTry)ew2VJY{YmGZm9cZJ%eTMUmWxP(}^I|%f}Qb87_O4!+L@s$*a z1qkWjM;eY#)RVaN=Vz&c%8GZ8r*+ul#S89Y!5{Dy44AIj1jHVLe8B7}MFy3!`?)^& zo+WP+q&_pMR&gUFv3A+XSWO17=maapBCPg`hc8ZFFOC}<@>?tlJOmwt4MTt<770cO zI*kJrDnNT5fxVBQ=nB2`w#A7rodhEe`SCFub>ND9m{7O46enZJ`Y|w|a)3%3i7P0o z1}Nv3RrX~$uVqQ^r!Y&GqS57r#PVW%WihcZ6Purk&P~PUr=#&GN&1wtl_O7#?Fja0 zlqABRp4fV#u?M8Hs4Ev0B150L(f-yBeI<(xATA8wHC_0?I4+A5Tg)GeB4=3%%Y(l5 zpI9cC#j7>sgrJ3FG<%rf#Qy4Ns!B;JHPSXT=&Oo1A#W^NMor{tEAq#Tkr&>atx~m8 z$y1Q*QKj}iS*5TvIiM!(Kk{pGp`?%C>jt^8+AThl#kr{369wM21?x2uhLlbdH;-(9 z9fIC~`sejVR#O@)3otj4(H2ac3%1ubs1oH&LInjy=fpDA1r<`+pG}^GK^ER7Rt=i3#^(??FT8$J~qY#iu4vQ(2SXbr@ zV|u~*B59>k5ojDp1dJshF(4~ImZ`A<{;(!;rrLzzq6a3likOoPWQtcwVVDCj z^DAU=X)4K85{}{^abl315M?k1r1YT57_6@$8>v91=;ifDG*e&nSkkQo@X}JH(^uLu zc#4~l7rZ?Z%~UOe@?>`XolVIJwEup|r-4fDzbILrSiWD8$yD>tPRlQK%?Ay`g5>E1 z{n|+32Avy%Wx$zhUWD31Itv!u1_4Bd9^~QwEO`oY8 zeq>@>;K$@sV_PSR7MH_OaneN6Wvnzh`fzX4iETKZ{o*w{Hyk}%9qyVd{M5Ks6CoUc zy)bMzplcw7>AK9Ax=s&NzAkC|%Y2ur$zWODQCA41Y=X<-R%DeG`(EiWvIsLpE{g?^ z_9TE8%DB=+l454R-j*NfeH3}M4qZRj^>#B!5pwsY3g*b;)#x`Xq>{e^D4BV}%b^Vp z!{+7tJz18Yl#hC#08B@6E&)sfNdO1lFkM`Z9(dN;y)*#0#dnBmno&)29*=VufT@Kd z*a*H8DODY7b04-|YPot(?@g1_pQIq!S5yW5&HJO} z$!ftI7i8q^`W3vH2=`*cc>v+79?IK)S&7~I8y0_tpuGqK(7+110&8I=paYdFnw_Za zW``(`$2m3{Yz7>l(vuh(PMK9#OqJD)RZcg~CM6;6*GvSo5&f`g=<)IGf$x0%a@n5A zrVF*hC|Nes94oa;Q=jgyd3y^Y8&{kbyIO8ey=|%R{t12iJS(t5RMNm=v1M>I8whg6 zJrpFC?1w3K!B_w=P(`0hhGZRi;;J5`^52vq+R6P1z~=)@SD~Vi9AXhDsa(00DCvI5 zpqo^k$`87-ss>VdutL}6nr-T{R1!OY+=GjF1GrOl#qAa-ufb;52PVU#*_NMFBCA1y zJq^T$A=nX=6GK)sJ5xVgB^6MaqI--Vj|VQnpasda|02YrkRMqdO-zcOAcT^gj4}e? zbe`4L-Mmn{kO&$6lXOz$rQWisskx?_Xz+a*cTKj8m(EWXEeh19R5GnWu+%UQpYE%9 zXG1aA9EpLF#eov7CR*@`iSV5>6jQ>RfGPLsA-sPH3yDhPa9kp)e2|2=a$}Mde^%%( zzk-##Q;?WI^+jP`Dvx2 zOYX%I2NZcz!Xh@{*c?$_z(rjhF+ zTLFl4ns|Ko6KMahpkMxDwEgZpI%a#604il#d_0sI!$wdWPlEwl>?*FeUQYf)6Bd3R zesa<$;~dNxh@uAvw-*o{AteV)rMA`Orupdy`)owo=wVD1EFWo||BJ5JyY)D#WA5dk zb4Lf}&7gDJh7dDvK<@RB?ie|^SNL`Jif&Ko9?SS434aBtBS@v3NbaUpA&9DwOp`i6 ztR`HwioT>jeTYT=g!b780`6zxE@jf7^xIC85nw< zj%3LTwVWE!B)QwQeT2YgXpej!6{zIZqZ3KhNVV+dbo)W&t3eqgr!LQ5#2DGqwbi^Q%<2Ee;D0|v?p1h&jq?V4(oUsnVO znJtZZlC&`Rp&L{qoAyB(D0;qX{;zi}eRsQYwL$}gC38a$5^LPPP{68~As-1A_k<+m z#tOBBWGPds!Z0MWz?B8FsAAvia$K}lKM+!v0caI=3kc9EMB1}?2ThS1>u?_?RfNmc zUT$Jo4xEd01#m|y(H|_Ofe~_FlqKX5Pxp$=8C(x*gXAZ(gey6yfn^@oK>C1-Zk-CO z)WQQP0&q<;{x{!1kNyl)*5{FRy$qN-OKQ#Ts4;*3F)b0)#%0IZl*2mFP)m0+!(G?c z7IAZQ4;waDYoW;5s^KghrG2AqH7h0Z8JSA;!-gFlTy9*L{LBp+GB)=EO$?MQ{cy|t zH#%eQG@zxBu>RGMg2d(wbIW0Y%^^+lAx=huU4Fc3$~KwqV{+FZxvpAr^h2Ju-PNCk ze9l6ieF0Ets1p39l8b{{8|`)$2_a}nNQ7HMkxf%&LJyWkZNcq362|S64T1Ts`-}L#kvLg>_^1O6S?(4Y|qbC z4)0i~DUS!QQh+J?^uUgzJ3fkHWp%Hm_pm`XQCTnRjno5%}SCE1jGU!qt!A;WR zq~a@*U|SKNQa2=u)7*P_7m)C1ay6=Wce1xz2_+Hn5&>$VIwFGJh`Jt7BFO9w7D!d; zM63gdQy?b_(@$S7jwLo$3O3j#;0$|^vQdFWNgzTTC@Uq;fk3*^ZtG28d z|26(k|A2qxPtm8o040@qti2fkV|L`?nn8k#V~G-1dj1(%-JvkZ#ddO z^v9}~QGlZ)R`98T?Q%?AP6IQMe3QLIDwoF|>oH1VIq~UI6uVl!_zxZP-|URNTaOk( zI(8xon&#w*S;ZE{YTXtOQ?rsTaV5m3?zIi_0Ha6l$}a#(CBGo~*;IM3Mz4oD{vYB^dl!=`!1kF!<2pgE#Sevqot!qg#!HsQe# zEs2gkcCGFw9pRax9QmNhTzKU~qd7xc77O;wgm%q=PAF0}CRNJo(SP<(VLdr51%(~D z6mvEC3m+ZyHY-uZ=0Qo;dbl!5u4v`=O>*6N<*H_n@ey({75PvWWCJL%Gd#?Wq=-6| zpX-5YX+=W#pA3G8MK>7h2A)~et`2Ybmx&lK(j%&Ys2$|A@pwBl+j!it74-1BHZ|It z>CR-O0xKXL%3$S6%rVwy>wu>?0p_%=+a6GFSeLPnsT*UH z+Rv#cw0MD)0A375VI`QC(1K{2s5{ddt-WG|6GF`xh@-rS8Gn#4IF5}s8+x7@>cTWD zDYFL#x4pLgqxo<|riS7yN;wS25gGYZU(H926<6wV?Q^Fw96euUPF#|(khp7BZ&`9J z4^VR4l~GwwT5?Q%LR3jpxSpgiT}^MpZyZFER6%>Ro#mjh+?+%?j3a5$BpWtRB$+5s zNu7!+G#4Q?IG`RZIp*wgZmRf6IMEwk&cN<5+5%&#zr3iv(w9bC@=xCkQM+6YEixZbnDosZ-A|SY@N6;*Q>|27m$j<9)DzoR#G5I%BlKfs5@X$DAPv3_A5hH-K{6Cd3hgf3$}wT2NIxn9 z8Y;DQ@l_SrWDh_{L1mEIio~8!4-FS+tMXjL4u2fpv(8a5-)&8ciN|!HJJU7e`-ja54O`KDPoaB%3MztF)h-5Qg%orZbm{<% z+PVVXht!L((u@Rn+!)UurDh4wskrS1oRt zF}O0IL7e47QVc#{HPW9kLo4sqnG+Qn;cW}yUGw3N1qd4mc_9xxIbga8NABk9Q$ zN$o($RbU25irH(LvLC zeT6#}xFnbbd`?{}xm8av=KGl(q?3bzZE)-cedRCc;cuYr_gJ=lCWR`X+ihF@rcs zU>Adt8+sIo_(ns|^8bkUNl~o z<6j9H(V`bC%?X%>+Lppw7DHPX^~M!T=L@+Qs;i7&U5xv#$mLbk@5Cp`OCjmWq6ZhG zw66G*3!=URHGr>Am%7#n)aPIWuVO;v*H7o(>BjjMN3pFc5`bi}9~pk_4% zOn&lIPbuVw`a5H(X!`v>LVF&Dd!ItP?oUT{FCYdSa2wLX45U#26*vHAI|RKJklSvI z4?K$&XXxvHOW*olknam&^o3uC;j47;vbmm1r`Rn(6!8ME#?C*B;Yb1#odjUCrw1pU zhZ4BvWc&EzJ&x!j$6(~dd+qzAlVns&yw-H}xxqU$nia>uU6YOT1(BYrarq1^V*%nM z%G{-xZVY^?xAsay_2qh?2y%ZA#45JnEkI!sYF`YuFNIo{w1yR=Q5`&R<%3$~X0l=^ zkUR=ODjW7jj5(CL8Cj32hR2SV`p^-%^~@rqOhHNvtE}2}FP2z1_~Oq<{8e=t_S9`9 zFP@Sr+}LW;u|MKHZKA)ed#GrBBjqP=tr3sX)5qZSFyM4>$kH*qcX-QU zL1e6W9(R^4@#nHCs0bRU z($EOY8A;AHS1MAsbf~}*4B2vrb0&#JY-W4~2D`1xEaS0<{xSd-XN<|h#b5wfLm4#! z`xiwQ>tl@zD>X}O_bJciVP?SRD~i5RI{iWIzTx)&h{lT=qFBt`Z; zJesOKyMmRv%+|+5<-%!!xL%zN`3)kaQ5u0-QKjCQjP6RCQZbvdRV3d-$FZ3!P)1_- zTwfl5QyJrZ^zb+7tAB~M?SuQCMteWImZM^BngOR#fz=Xp{5d_1u;QFCrC4LMcCPxT za3O5ni?;6F7(8wU%@4rh+&Ese2m!4RO%fbS1P8$sD&J{OJbfju8%`}WaY(RRvZ|h? z&sUF>B*LAu4cYj`hYanrH+H|a{o=A7V@=6aDpr!u`0SR|zN{I;_w+9`&o-WI)iztS zGQFXDNvcX=kpzu0;vt&M5*R6pDhMf&X9u2c$9?YkKp)U(fpSiK&@{_m6pp^EUfu1w;`~L;) z`HWT9V`-(_L&Dj!?p^QjlLK;foOlV>B;)OFwwWo*JR`8UK9^Ma%1=L zZI_}rVV*qA-5}uYdS%to;tXsa`uY0#OgV}6YZrx<##|_uRL~C8lO5nFVQuLX=U2kmZPGBN182LC7<;sS{ z-e;m4Lw66%w$C)4ZwW6H+(IWd?6+G`a;xGxQXtuBc=aO_I0)+c_kmOz4NC2?63ynh zWaiER{Hx$}gCV{S@HH&S&JvK!K`|+YCVk90<2D|q&UZ^vzal&bY?I)bCGE;c5#&cO zlx~Ke_y_VQzexV@U(w^=g4yxlnt%fZ9t(0(cB2cv0xJ!Tuv;Sj+_2S(SOmi>3qi{$ z^3miAL2GeM){7V}4kkqs*0T~tD?QUxFkP&1|9Rn8h$Nq2*}<0}`g+sV!P40*{lsgQ zN*^5D!HjsBfiXyS#UPnCP;hHq4GIfYi`PGMWuj}8=(kMI8?=Qjlq3o(^>j)H_cam zYH<4+7%U^P48sXT$G44Nd#rnQ%QT={;IxYsq?9}i1jqFn2O(8J11S|-;Af~HfvD`H zsIW(sXN%Hv`OBv+#6Wluso*GAd#&;y z@Au^6&siJh@lB)HIO+EyVAtf7dys;!z=Cu-q=WB4$^rlf9IMZuhQJ?xW<$4Yg4ii* zyl^q5B?9=0mL>?!3aDsH;fnX#G|qq(B*v0gtW>FEp6A|b?zvn!mZe{I>umKW2e&Z{ z#z?_nZ_ZS8Gc>QqLW!Y0gFTviN^?XmR#XYb3Jqp7w>n!@0{~}HiHrRQy5Qb5Na*)Q2ug!pIa zwlBx(X%^u#(E`o7lc0P3iLLIko%Nn>T2Ik|W!DM1N5?ZM=1}=sj**o6`vW~tYx`UI z%?q2g!IO%d8u)msxHcei<|PITwBt7RDYp6>qfdS}J?p`VlwHqnoqFx@MK^vw4(MfR zFm`(h!$OR#4%QyLc7N{2cX!TiA%EsXX-=EGkqkRi=#d@HfE0`(=#}{jV1r)6oFf*l zQ|>8gw7gG;#se*rxC0n#Gu>bVTnkDAvygPA~Vhm~WQVl3gDhpJR2K{-E*@7m__lm}dGf|{59{FYT1BHe)3DQ z@b|wCF3iod$xC4l<4hFDczkC3taM;?=MmQPF6gd8kw3qaO7oyS$=2 z$>qsP?{kyRZH+lTfA9Qk3oyw~6kZ(CY*@Y}G91fT(jXi>w4Q4&zp%1;Wp!v}c;wMY z|Joq?qA9T}hlQk*6d2x+$VkwF>KmotAGNuly;_)ppkzEr2T?p66O(W3hdE+gW=$#v z8_KHxq-01yq7Ov+vv`O{hJfQru=u52%hDIVbgR{1kM6Yn{pF`<=u|rqIY8n7k0)o? z_<80W=DG9a8Suc0xZN|*1^ujdV0AAJr?o(b0GyK{G-3ms9a?*O04_J%vD_;c*M>F* z2R2VcU30gJ{u51>HFn2}US_bDU_HmKRdXmQL?(i&Jw z+M;$Y096tI<*Xq4!(~HY;(;ZCkNk-K*6RGmgD>pwnyub#(CfIn&hES)eOd$bowyi5 z^3-LRy3!`EPYpTv_%8d^e{CC#oI2afH~$PYVLjs=SWh!Yo~A#y0l1pih{{%>wp`kK zN?+)5qa^tB!Z#ki^!5vG^F#!;;{B1q^1<#4Yl=ms*Z%7JkxjIzO^WUDv$3Vs{@vFf z&je(>x9{pyW$;IHFTFi;x9T+7(97GKe_M3qF)k3G)om=Lom7l3!I7eoYpz{d+n8C) zALIsCh6Yv#`_>1*bw<%lX0TiGybB$M=A(|aOm z8AJ7eV}(^jTNT>Mo{7DebhJ*R;UL_er5RT2I>|@^(uWIuEibd5{DWIwi#?geSXOQS z_4~_k=5mb3mjih226FZ`@&Eq!;L;8ic=hM-t)KRwDXcq>upT>t`Q12zby&Ubz`|

    zb!yiUQ4PVi z#@p$IVh7eRI3Z@CQoUZ>|38;M{l@zAi)&NRQz|@M>-)*wue~#KcdJ+x=u4?EAhE6l z1#}bxs5rh|bTtLr_(7prKfkeYVZ%Y_+v+cE^!IJ{7q|L&>jc0!_$8jI!9)$F_Tg;B zRq%;`q1Gp5!mT8ilqH)7BJ7wZgc`mw0)f!UEiyVTfbDilh*i%lYoa1**4DH;T1`1* z@xU1f-6MqFs1i)fS;Y0M|M_`Y``dW$0~TDwr4|`r_iiR>f@$w7Q>59BLMGzSDBr zjvPBB{Jn#f#6QihZfTU6@4Om(KD@f6l{AZ9#-8EHaF z<{F)0Xemb)B#@dCadB0aN>xxyJ*F+@|;(g`rjZNnFH23yUo*d92t9 zF2R9Z{8Kc_6;k&0uPt;qaTztE&^T*swULbK+K^6TiR_3r%XIt~jNcbpPGq{6Ye^e)L7Uu0$>G;lVJn!SVz8+n>2lW~&G5U?1Q6tKh;$CZmAl z(?<LLxRsDHhjOl{lK0Ze5nG#}%c^AZC1fJ`)avluCowfah$sxY&aVig$~REOv^45!eG z>8uyk5ND>KJA#pjgej0OjYQM|B*v2dXY?cIXD|GxYnlF>t=_J!W&HL3@c-?<@c;e4 z#J7KWqzL5hpyi7WMLvX<^}JntuS)>#&;l(Pux6y(E2E$sVYUBs7uq~X05RtsypIhL(#oy1zag}?lF{=fMr_}~6D-uOG#YW5Og#F|6JV5VXlla@pzAWn>j9DsLg>d|jr`RMMs#e;lrP7L{$dv*TG)rT{Ez8k=B zaB+!$h$EdfiN`gywlOWWd^N9S;{*m7)ygpZHU?O)e6Y{9dg@)=^A4=w|Jl7Ro3N(YA!=%V zzaV7MB&Su(D{}wC(MLaAo$2m%%ZBkTS3`Kt?;-=A?>Co`C|Fj6kGw!gVV zal-SZAWiI;)SVy0!d!rVks^((!M%GEOAC|Ble^{f%M&9zJ+Od1yK!oKr~K);2aiTJ z{BYQp$9TzD;#8Ql3>%io7AhD2y%H>fq+kp&26?fPEMUkBuTTT+$jCW%HQ~UdV9xyj zfDPM?G%9PR8X$e6=`>V}0k+d+NO>9K;N+YQp2 zCaj*@l?I<+aCj_gdGOIfW4F zQO;k;vCP?_^{?$rNHO_%zXZm{4+b8SjA606R)=bB48DGep9}`~-9GJ=T9vP^wOBkE z%q)XIsJ7kuVaZddgMdu7P`4N^mA6($w}uW%Q){Ot)zhZc!Jh*U{uI6_5 z1TZz#z(ijlO-wRpDPh}~&X`R+M#Y6nH)IClk;zm+8`KGAQ1S}3p9Iy&VN98CW^&d@ zwB3{N4RPhu#nOJAXSk59&wJ4#@mv3^jVt@d&SLBB3=%%$9$4ak`%0IIb!XY(I9J>& zCdmPiD4)UZ@$Tsd-3tK+_CM2pxjO%y1>f~f=p#1x`>FofF_$R4A{{|V4CK15OQ$pw z6&E8tcDGQQy|DQ1cR&94+n+5>uQUozB~6c127Yww>#xs$rCcwy-(mPx9jTd%_sru8 zG3e?v2TPF{Onf1!ciOkelx%o05+Q5hjNCye+pa0cGTXo|OX*MsBLG-3OC9z!tUc74 z3hjAB@*t^<1A*mtq`Z)7$OSQwgYg5W`gVw;jZYGv;=_a(JrY;4@!xEH!E5mr2A9A5 zkBlFm3H6u(Kie+QZQ8gK30%itht`0DRoE%9shz|>U#7+ZTParV4z0bgd%BC4o~#X6 zji`dB-5;*beRqE5`X#5~p2UZ1==wCv`PB@>0*&9JjFd?3PZ6CFGfo1@!sSYQk*WoaKN4vhvPxe);5Qdb83bhOi(kIi zz8o8XdwG29aSl>&o|x)&2UfQ}l&2Xlvcs{Vuv-M`9;mtmUK?9Hw_omJBHv~ahp_p zF|LOLIdLc;N&=lg<@=O}17kL45b{%#xMIb5BetKsKHu;8nf+w8zO<`{3^@2-qPIi9 zQ#0M0f~CF3Zij%+vLlt50ssznb`P1A1lOI`JEtG~bot70U)@1|`k;TMR51+~1!fvr+tKb2JrS43Qnja5<9IH z_`4%}d!u^->hg_Tu~I7T^%W|`LamSwE(ZlTY`-lw@|TyVf=knBZ4Okn`}WrccGm~? zC=zo+PnJ^aAs9@q$hbyE(i>@JBE*$hF(NE(aaBCB|IQ%AlnT=vEjkPWS^KBKPI-}$ z=tD_iY0?8Ez4#K#fiUkV!x#<0o3vj69R)+ZX^W0>Q|&(LttT*?>Cf5vvM;=Mo&Dy| z*qNEOsdahcsh(?diPdX8d~eT6UUIn3u_MX*ZZ+>T9drdtjL8Tl8{PeZ&G~_inabF4 z-@tUWyw+Di!(u|pq0K^T;r03HPtFcMDW3=izkNP<*)|3TCycPOS7Z$VX^XJ}n1p8^ zlinnUM@*W<>U406>^c^Eb+gb6E_-9U^r?7gO64@$?o_dINKo7j8e!ia+Zkd#$S#qTV4`xhBBL-67r|8Q}j}GaI71o~JG_{#(@0k8) zNDVpY?1q&R#0yL@YL<}?Qen4rI~%T5%1;r~Uj@bG954Z0w7w%-n1uWQp^Cn%JGlpI zg=`bg)|YrazK4(RMgJ!i`=dnU$(eTA#N-UiAH!X>_Z?W%FZBNB-S4&o3y)e~FK(Ci zPgfvjGL!_1?dF|{`x6I){l_$jKD$5s@u^2z)*fR@;#`aSi{E;9FkG9sJ;{2V4;VcE z;9KW;)7ihW5r(oxKOGP+D$T!0Bf${!PCxE%jc9bNPRlM~*+s0$ylq@4wi<;N;z9IO zF{X+>uV_AnSb~5r+#e&^*;k9fT3#Qb%@D%mm9c^flnTeXG#yFeTtVO9TNjSf|D2xp z*Qu9Ebq7s2B`L=ydBCU=Q>FwXN}dW-XQqD!&C;1VN>Zm0Ozzpa5nE}#CW*y9Cbk>V zAxo5mefIvnZ2Bl$e?aSCpUr=a^B+h5vjqQ}jh%rhlKV6|)ycJE>6v$d4%Y5Qw+#5G zlMcnLzUt|U6rzEt1vOv+Kl|;}+|M7qc&rTF(S!bypF41xh&Bb0YaV z2fcR(>VJQl*PY7CtBjIhh@cM1oR5_Xi$NeBk28s8E+z}Zu@()W$Oe#-lIMpyHjDFe zgu?8Hk9DNK6_1cu3<^86Z&`DCI3^D^>%m}yUIvPZn2T0An})b(RCYD8OL~Z@#;e#~ zqiSsdw&7y=G?08=O$pjsUk3Hz=ADhGX7X??3W<_nRF6xjx7S~r%k<}L{Q=9<$krd@ z`eXLVZ=?S?3Z*tVmUuih(iMs*^zkGd+NZ$e zIT1;f*U+4045F?fgQek%8C_!LiGrE&O+d~^=A>8rC!Ad>ZdTlEm(JF6wCXkX;3ht} zne6)`6L98=pvd^>PEWV_f#8K;=9TvD)x*_Q7xdYX$s84}xBA-93rXLN*lOe?j@aTq z8(lhe(06g~7{%b}O8NfqI!YrdrZkq)So#6CSMl zO={aa`IWMxj3yZYTNaaaSkvO@Jq6+bEHWK$*YfNGtC5%f*spO`Ym zYlYb42Sej{5SMaM&5b}LO|8!;Y#&mzLsc`Pm(9o|gSAKH8 zRcd;%7VXDm)F%|vf-jW(#C_2`R+8I7F(khzZQM5TIgwm0ypI#pJPnCa?Ge*_Gf;h> zN^5in`b*x4_?`l=AimaOaB$uE#XpLlLj_SJe#>CBM^53Ipk0wf6AZ-w5@wwACK$m< zXuK>Kz^Q>N1Irz(aI27H>fuFNpn;r>{8%mVM0r1S-#%!;pa1SuD;q${R<=%b9qhA( z8~FAw@zy)6S%qM`W-2RJyY;Uas4F!P+$SGq*GA1F(zoZ^)DtIPO1_tFdm2ZO3 zdaYN5R*JaR7BNT*OrlOG8lMkw*Qd1vpTtxl`*5NEgZZogoULqqUTb%Q-FY8>_ZRHp zXTh!pV`rGtO#ul1=D#qndE7_+2`h}TV<_R6_YS6aky+DN34w!lw&2>p-i4hp2aojp z!B+ohWP{Q#Ov`>%Sz}|MzISPZ<*{9&?j3W07SGR*Ko0$hDp-r4Mlv8xGgN@t&~nAY zS=zsXe&M9u3F5C5j0FHY#7dK*FU@_D*DV)Exz4rQo#;ve)zS;okz+_`7(E(gVA`lG zkyQy7Qhg#36pt##^tvn3Dh1W34SFh{Br%bp1rH?CN~TQB5*-G>Qr)Jf2c#Z2UJ{+4=&O@8Rut+3k1Rkx<|GF?;Rj$14)I8mwOJN`gLK9lE$W5S0L> zObT%gm<)Uh6xk~7zj@)_j~=|>_=msB#NKcb-8!^<;_UAm1!iqe6Fp5H$E~ldRxWOq z?~D)3kAc@C81|ufeac6Y=0YuPqF?ZxKHeD;&1=wabM~by3mB=zxQf7w2tPI^UMULhv0=3SJ2^CJHX`B9R zUNjY&tFm*F_rT*=2&zKU8Zd&P>k>U#VX;g_PBiSD34l>!4XZ?w2*qH5(zGryuyth9 zXM^;)W#wBSET1VBrYOlSi~83^^sk6q7ABN$-X6U5xG&oSvz4tce#s*7XK%NQ#G_~0 zvr=hmZLuBWc+B+zbH2-tJy7eF4_c!&RK+$RcD$O7x?en~7VBr)7<|YowQ>(nuQ5{T zqgZRw56Z)2i4a}CKDBpnWs?;=w~~im)P|wO-TAUp$>U^|=Y0i+CERjL22liormD;o zAm?C_ojWPgDlk($T!12wd?bp&8m3x`yA*3df!^dgk-no8sqlfWG8BXcd8E3oYNsqc zs;IjKQ(Z~rv+1HSwPYi(+BpnKL+iB0?Nl0#2G-Jt%Wi5^y0txw^MJ$KJ&bHO|nlZH>+cf21$=sB@#Z_{1RZ!B6k*WU_Np8|CEU1>N zY1kUmWoS@Rv`ETk+qFiE(P+7{mVp^5S}TLUpmcBf1m^wN%#y|9Y@$@>ubSqIa*&2ziC zTCQ-v>@Jk~##6PLL*2=}HSOLSYkp^`HNEGfF@wttIwf>wru1a7jzmk*CL*{(MKM-}jzE=yL9HMg`m@vT^&{L+E#6Fz&{fTPp z#ZCOmHk7<}&Ox<%GfSws&fG}As(D|FmFtl;qo};bdTb&W^Q4DaGp~RS%Co#bwVl#L zZ=zsxo0QGD(L@a$E5Q&CGN~oxL`zpCXy8g6Ivhhv7jrFGD5L#s<3o(5DuLk&sXiyO zb<88dLP+_TD*7`RRbfm%b4+W?Qd2p#w+;lhA#T4C)@VX@PE5VMa0ba^fA;3-dXs1S zTehtO`Bev{% zvA#CCJ-$8SdUo}@y{iM;DDRkExRMUq=#IXzCs)+%7YiOcvyU%svTv-zc#Y?M*5d4- zo4GI8&pGquwl3{)7;f-_&&g>6cv6B@Mus#C3!`AHT(~-RoKiDp>ew;GW3z9o1N`fE zCPxQOOUouOi!cU6MH5DK9LNfdpj`na?~m4ZDOyVTx^seq{19mOL6Y@rj}OMA(OQ~q zq3rUH(+QJD*|ca)b4VO0MIV+N1rD#@dQ#f0I@z9uJ&>xIFJ-6!kp%J{?S zrCP2j#AIPaC2X06bjQX1M$2|I@>QlLge74XmV{ejT)g<|h1F}8^Vp>wc6ldw=TLWH zrG(4P#-Nm|CnacwUKGFsf9)6zn0!B<+b*$kO->PeaY6%Uit``?%g)}+H9vag)>mgQjjvk# zqPfp6Jw9`1=7C5Cj6xA0%oh^Pq^hSqFbldwQyyF|340JuddgULdRUD$Ij?L|e+ z7RRjuZWnMjhkJQH_*!!@V97NH>nSY7@nNUn+!^H$huq*~GhOk|Rj}05F_em!%k8 zG22*EV$hKyYbVT`L?ki8rIL<6F%9y8j2@9g1Fg#V_>Bh{0B0*( z&l3O-Hsaj@pIm)#;ms@g<1}RD9}Ih=wZt+*EYY_Nia#Z2o{Qn_KKJjuG*=m{T$(=* zMr-t$6HP!zKjKIgC)V>_zGuHfFKVCnP9x; zOw^cRg%M&H_zf=mt=i9;_r&vy+q^K85YV3KM61`_OgLbMwoD&I`EfdST2CYqIC6u| zb;MvuAIs70DB9)fl)5jFYN-4NG`PH_%L!EnX6+ZMwdbk1Mrh;BlJX*X(WC`THOr>` z;y@mt&~+MUh04GZjbip0X;1f~3yZTgKDyu6Xl3Rb*~-@Q1i-@$xix#P_xOzkuh=>U z0MCtZrW!I3!(nrhRomai4a?_NK6~|6z1YwJ0O88~1vhkr2)?@E)4HKMg!r1gE?2!T zXl+jIPxk%9d{<0hMm9w8l_(Hh?wM!@PVS&n^xTr?lsq0>g6o{+h>xQie+}MhaLL6# zf*ao`W@nwalIoD!a8zn#O0Acv&BFErLu#Ac?G!Q1Ol!whj5GcAlFhxUVa(Gvz=5p*^J>bR15m6LTus8brft(48 zL06c@`QceoWJ$iC>Cf59*7LOTu0LFAQijy*Yb?F75RktkJ_A&9x#dBJC%#LL+7k=L z2n}fLD!jfoT>13%Pc|p?F{+Wx;feLp2+y1dw_Hv@&WT!f9c~96$zd?Ikd){v<~|}D zd|$y}pFl6FBP6CXv*uqRE&_2S9!JJfPLge)*iM-pQY3Up6QAi1s2EM#E}i?WtgJV! zGb?#xsP_@8_8_QM;|X^}n^^H^#W5};DJl~P7jv5p(85cOyz zEer%nzLRFVLA%J3REfAV3q|}<{Gr^cIQN!H*_M>8Y&}2gRIEQ!3AbLZt-k)CokV|R z5)1QjkwJgO4Byw#m{*Qugu>6`g}md>GRLryIAsa0)6GhktCAF_<+j1L2pfx zUI+;T79=EQz>@yb2@y1=T$1Kwm3+J0cuVB05K&}^F0N|s2xR6bdU7f_8>Qbg-pEq! z(*d5jhOZrMmCXq#4IR0JUPU6N=+*I~g-o?FQ50`-o>H%)tTC(lu&!p4ZfewOBqT9H z%TW+_mhUPWF2baBEAbwYd|H}$k?d$cOp_L-SE5%-CQH9RJAm1Sl&x$%SNii%Lu;KC zn_hKdZ{vGM#bD>bkmET#1Q?0ICiF69gv+Y@NASGO>GfNG?~|SJE&2Rz)4hEAlItX* zt|Js(C&}k=!%(~=x+flEH7ze>RxZPY`yvsaB_}~YxwB6K?E!R1lROwGuLX<0qbBfj zHJFI24$>yyWi0i&cA)93OFzw}qaGpv7!?O*G(=$YDPrQ2tCZd$g@lrt zP9mtP*)cjwWgkH=Dl^SqgDJLL^qos`FY`25++yYf(Vtn=fB^9UP!@oL%k|8DGF#bt z-T-*8Ze<{C44&HE{Qd)W6bx?h-0X+|7>U7-p|8>oBa^;Kq-=*8l^PFTyZ7MRv&}+8 z(97%Pvk#{nP8hJGR9Izjo=XC5xjNnh2FsGr;7QP>MPedEQ|C*nMQilY0V8>rCxWv$ z8Ms73cZlBwovG!jfX`CEn58(MU}t`5y-)Ax7uQUlZSe}RW^0sY(PnFzoPuehalW_RlcN5Wv|=9mncb-AS{OLAfcmNGC?nXwQ;cc(V* z{`iyiOG|=?PtTqk-7E{}6}X#|iZs!9ow!lU0o?%uK&Qft;4a1{MN**FioudMqbhJ( zYNd?i)k8vs<*x$BDFL|ZbU~n(*^IF|&|ij~L}?S(^xLsFc+8pF%#22;pX7^*5@Iw@ znUXa!WoC(~`gk(_Y;;pACeolh50dvoYqv^Xqx=JC=ND?=r7~~vgwhoq<-nC6VYbhb zBFB3f{moXkp0njTIMUygfM?yc>t}X%ezJhMLwd4cr{FFQDPixVKUt%poa+c+F>(@U zo%2?{ws`OF-`GC05o}&FH?9or4#*Z_iM|w!g&fU6O_k>cq13Pjle(|Wr143Gjt(3% zlXRvSS7{KdqcRW%Y=vIw+L1DJ1c!Woq=l4x;!{19cskC~Cv>f{vwV4)t4wn(v>_j0 zi35uX$w{4nD%GG)O`$TXB$Z^@R-IUJQHKpAF^GoS7oCP;>Qo$(dWLYk@`VLQcxkra z!}+ZLIa}F!-t^}N2PM-+Aq?^E(U`34{cH{on@!@sA9o1M$X;JZ6QYTsvBu=IZX+qi zdVTdLug=Z=@YDU#y(=Glb#QM$^cYh`IsxPO)m>U!HM|}*21&<3F&3g}>x?XrqXmRh zyQ5`dAT91HLq0sYD8|tS@ik|G7(3*aBQC4Ca%ZUS{B$lO1{%z;L0&9nV)pGPj3seN#Fb$Z*g2r38WjedF;S=kM-p|CyR_b z9C0PyX8o|t3E>rol6Yo2Qjl?EeK^WG1%ygnyaU#L{*Dbtd_@jw6eXqrcq9dD-Ngj7 zfShim-J@)(LX%#TS5k&?r7?~oi^xqCn@W7WuK8_>5JxCiV?G$A#0t( zjnVz3Z_hP{YI)zGnlru7+HlTMv}ox*L8B3XSxiDS5{#r0EPaqb^WMTv&BJM-L%RM< zx}&WvS61|eXT(1#{B5{;0wC(vjL9uPoUK4%p8zB)kPPx7{b|WFGqrb`G1XbxNL(QF zmK=!4sFNH5Mw0!YWeX}yU1GMco;K7128J$iQf8^v`KOD0*=CfjY&~ylqTJA&{nO@5 zB2Jt08h?C0e{GWAIGsj<;r@_wahFNcW)_7afUg9@fNH*I!b#d7FAJZ|8>qL5X-H4G zxCKJK3rgpdWU*pP^o^EVEmfYPFE!HgLlP`rnp4G;42~uz3Bv;<4IGb-ngE?>Xb;dA zdqk)~05Da34Q`04uLYvgp)O~HsDBo!5&@MCsN)Ah;#-LtuZ_Jy6eLC!iD^L>j8tcN zCQy5_5tR-v$+i<4EK=JR-_i!Z4;K1cUS>a;t!zDC%XRSdV3Vtc7>p$@&2)2?C-`6& zYrVeYPF4Beon_WQg162%=l6i4a8!=$4Z=7C>NsT11BYp-CL|M0$D}0Gf3O7GOecng$O?K1k_qJ8p|HqyS{kh;L63z?BtKqyf%0+vnt- zSAOO#21ffOMW93_7-%WI*bT9f}_B>k02xGycB zd-|!%o-WSLbn67te4f&ptlQUt47|#T=s~ zK}rS^mt(F(*qT|wTHkU)axfK!xeCB6w%SEh4gp#XQM?DP4*b}`0uy?sjkZ#t?og>c zHs1BFrc9Mhk?Af*z|t;~dcZof4L8 z1~Dl(jY-`)sY9s#m)iMG{%<3re}yt54vDL~?!syz+l;c6t>MA6$a{any}ZNA^~h+*kC0fIrqwvNYPw*8js$Evw22aHSsUOv zCW5or-*}AoJ`f&HhFp}*SZz6u#-+?RX=_ze5^*{v6dfBJdkT=)qsgVi7%)AoX}>8V zA|Q!9(1Yt~$`B@sE`TBVnwkc|W)0dC<4IJi#YspaC|;FJ3rv*mm^1^}KyKx;&J_Kb z#lz-V`&G8G^;`q+Xx-sZI8e?UN4&lFmIjS1!hX{Y-z(R<@p703NGzp1Msd zv!|ptMPnrQLS{`uY3weeO!h2w4#f4R?pZ^HTBvd=?wga{phD`Wt`sJz+?9Fl1q zP@-snPFJ!>f|lyEB*a`+-)J%0WHqGwm9pU^m`F|PrKt>`(}G?@Wfw1qho1>tg`0wM zJyDfWQHhnlWmHY6j6UZ?f@X>(7OILd&2m5)b3~?%kr+(s!&0a)V_NrB+^;l9SC3{$ z>_q5!P~1MdRNSn%*-n(LY&~zQzu=7xw9FlYF{Q0HZA+&CLGAlGnE(Dt?#1oK-%fjr z(V0GgWIk1OF6|2Ec?i+hkNZIBmI|aseWJbs+tV;Baa%Eyp3}kkQ$Je!)V2uQ$ zI=QN~WlA2(D-#%xbbAQ}@>rbsY$9$fN zHEYzUwN^za%1a=^;lY7`fFMdqiYkMEfcb-ffcL?G{nOC+>sS4|;W~?HIIGy1I=dM< znt%uy+ZmaVNZA;gnJAkW8hbd5oACZakXxu~IBUqsas%yb7!3bo!{BaX{|^lU!Yklz zZwRz9aV9Y`F|)AcBfIVFB_pvg<|9*QlVg&z7cnunko0skQSp>l1$tTmxs1sK_(^!( zx&IN^m^d4fxZ7CUI&r)6k^L7h_rLLf)Qn^#|7GHA#YgtPg3^#vBoVQ5G$CPQV50{z zF#||AxEKIztXu#99SJiNfQ1pj#K_D=4*+npa&QCKN&fdm_RpK6u_?E*sQCZ(^-tm> zGk12j=VoMdb8};GV_~p!G-CvCad9y+F*7nV)Bm%eck-}xHgu=Abt3<722m3ypreJo zvxS{4$$uCPjqF^U`N;l7`oB`JvHw4`wod=MO#ceT=x%7w2w-6PPfGu7C@1&-Z)#)n zf2^IHl}-L{y#JrXPO2XECXC7^PIfMiz<(2GO8y^L_S_|#=VApgvvY9* z*qH$UF%}^)Q6@1lF=kLz)Bmmd*FOIl{-3`6 zC;XqzH?jTK?vDTZdQXmh2L#+(NJ>;l)qSJ5+a{4WP)!}sdy{3?&Y+R zk0BPBR)5%cBG2#LyUq`}%kNUaZ+JKF?=5=EEsmc4`{YNj5$|l5-ci4P+L+C8wmx%r z)t!Ar-appbS;h@dKEIpU-aHw+!qBKy0&0&$+%IQL;2l-GIJny|~h z+BUZ(Bk0<>DkY<8n~ZBKgWixS@{!K2pL}Kqy{|6_lgJc)n3Gv1Z=9z|qu^PGclTAB zuSK~ruOG%ARl!b^P2ZQB=!ZI%Zi}@mx1Y@I*PSf@n_)Ep4__?%??d88r=!zVZ+@N` zm7uSukJnAE9&HWDYyGFglU=#KytpqNV6%SxbT`S`(G1ajXf82rQ zPe0<0k569Ycm(`r0q@7&#%i2rw_12?iV)QG+^+{O=f&(nNCEwt?TPQ$ls7KgeMW6x zQ+=90yXf*He6*Zyu3I;051$P+e{Wxt?j8Fd?)seG9kB?mp+57zCtQ0zK6~i6-YnwM zPMg0zU#+pP9~AS%Y-RA5fUNo1FW&xiMbAKL0zIs+pEgsC)lRAb^@ZVnR z@3@wq%O`KG8PD&WT;s^KcLrQJzOQKi>U-EyR`k8ug?8;fcs~cJ)DShgdtwC;qOZMRszEL;NSb)!Q`f;~A406UY+ID*PuyVOu@_set`^dYBDbaX2o1(}5 zYZKOK()X3wsuTA4GT_?wTN~Z^DBm(!F@7Us1-I`udePvnOi$?jg)?sH9sL9>(G!Xf z|NfRw=@82|^}vaS4^mN}edg`3J-|`nrK?I$Xahdh#psnBv6b|=#KAdrP5^!k7RiT` z06*Jh!}Cwo(O0X04Ke25g6i@-=Hp$52x9x+Gx%jhIXV}Z(P4XjsGpSlWh_pg80=6p zr932 z_=pY>CL-yQ-6}o;nMuRY83e~4Ut6w4F8;PkcBQYk$>+^Pk2i)^V|mBQ zjf4TLH^FHET|0>%jt+58{%vOk&}uP@W#IGG`d2yVALarYb1LqHyw#t9*JNe6SqiH% zv61}Mnx#RUCci_e6n-@vdg6ca7WNH8*08g>TYU5K0O-Z}L=_10imQrPh> zwSMoxFAtmzne$z4sYy;*nT1pr2dyUfeMchKBzs|KNbO4TJ+r}iBpoN_kjh!N2)-$5thEF}rx;_<5 zYqdw|Ck^Y!`rXCU42RMsOrcg98e zcCRSI1bu=PQ&>PY*X@pUXML+r;voA;)-P8{oeNj2nC(6?LQqQ(DK_ja@8ewG-{DwB zQU32L8FmhWntyEt?@H;j^3Si=oG?;b6aF*BXH5Xb((T0j{fPe`WzPG{;n{9HF%7Ol z2Y)mJAC{(rQ0q*miO*B}vGU{SXX-%(52_!v$^&w6l#iBn91 zgcAl2-Q&)@O8QursX%Zr*eYIhOP#rJ&)AWB?gp)s&L~z?uTD^Rsx5VxFo}irrWupJ zjMzBYN}d+hcd#gl4PI|{ zaGs&12&qV~Wn=dKOB4;i2#3URrv{xNS~m_+scmX z3~pD5KwT+G3r(-(MY!47p)mrX1R_JSVMqBa8f4WWiu(Jec-vUH;} zWw5jd4NTn7s)IlkP%~KibC1!1hYtD2A1-sbUsmsL2;*E;o44K_9>S#{Z=DiBm+u7X z1bfhlyi{y!jrfDg7@oU>Lp^@#8>tnS)hQBdIw!&`eFZa!(>qEd-w&8QdFHv{u_P1eaTh4Ukn6sn37&fAahQ!#$Di|D$D3 zdahEc+BsYENGkbZ{`a@tktIc11tD$=)h|8hzjl390M4@Y=4KP-@^cIj<#*x=z||>b z^Xd@)T)`38t{%lLW5V_PGU#t5j?m#BX9N93)Ph#!In#xi>&?AIHg}+4Q|8wOcFQf;7t5gJgzh@W6LrI*{xiH&NOn+vJ4g z4)Uu-&^0`EOZlYf{@xEN4$pN%9n-!ypng=k^2!7PH(*|M1>>sJ{{$YpSW;_Dm=S52 z{mehZm^Y5baYw-D^wbJuhZdNe+i=k{_7r+yp^`@%t?t+1S3bTI(w#^8vtVJm30CEw zX5UT~GtjENcc_9c+2^>#P7(nVpNCiQ#jLtX-6U@W*Ru|nbo}*)8uc%?xThTc@UoDY z`Cu?R7mYd)oF>n%Ulh9l^@lMUjJX@;u*%XGvObXrc05q?G1}#&u#4j_c8Q3|pUW7_ zTq@sS-iIs5QG6?jF9Tg9mhMp#D2ez#J)YEkZBxo=acz&Kr!uF6BJFlID|XgU*~U^d zXo{w+V&mnDJSAg9Pf^54@n~m>0>_Y;^XKgG%O8f8xUo-}>G#}!Vi3$!7sGi~igS#X zO`)1Z=4}qma>!ytAWqBc0>X8J$o%31xbGCi?jFvG!jX9weZWLPH>NE!Tf)(q_glNI z+eF)!;wVC>n_MZAi-MyG)rp_7y}JBRxzFtj(&M@f2Gyg5$r@R3=+R%PWU1bXR2`Uma;8hm6ssbJL%{8ROW(5vKpvC^5Bs@`N0@Igjg&*H6FhB zxgpmi*zEa??>r&Zxb=OPk7?kflTL)i4#CfXF^2t?N$jDQ&z|UT_{m;DQ!7Fjxy8?$ z+ydp9piX>(Z(Qs(l5_IM^45$)T+7p9dhYS=sB`4QHYBGKy6?SfJsLF+mifRhkBjO4|p%j06oW03OUMZC%UhF%dh3u|%$2&Dr?$dRt_ zU@o1rrwHB65OTmfS?vfua5PChvsqV_O&%c6Y@`b{`DWmTcD}OeO0c6c*G21M0?v=) zLb>wER5md%d?xUU5;CX9UZ7<7@Yo8Qj{2I~zW@1((B4*hu>BSAm#W`<_rsugv$Gs% zC{oY5UlxJ9JtaGRCvF1r6u)e#3X=!=$&eUsXvyto=RWY9wSfOfh9$%SG~Wuo0t5;Y zzm+TI;cRbnyh>+E$@XaErN=qQNme38yzt7If2?^HSSio9u>lG=(OQ{Fc-mdHHvH&jH1`rw z6sEif3v0@J1u1?5S}72Uy?z|>qk4bEg9LYZL@ngSg+TtxyA!#&v7G{!g#4{a_v)AC zIHl9{gQv(ndC6JUMHK2(dD2i*>aX(n`htQbi7>Nahf?}qLNAqtKXW)|iYq+rh3^OE*j(MD<3l^tish-AqW zCAgG!=vJ`(=e3HNLrzFqMfDNQE{|7{eVmUwN_~(gps&I&z z{bd?T`d(UR;)l$iyRWh`gU7o!Ghcr_J7woY z<`1B#0kS?k+skYed*>BkZ^$b*E)@uHx(;YDNa_(08mV8hbwYLJUgeQiGG)|iRho&m zQC*8TgEhbKH|?UfFSQ;N&Bzaa5;d34IS!w?Hx=mGvLR&DXhxi+wbL9-` z<9Jh!e)i{VcPw423`=kZZZt_bU^%k zpL1$O<5!55e063Uc^%U#{)O6nFy!vxa&jPijF1Ygb;Tl1+Al#`IWW3*Q#}2rhxkl& z3kORdDns3ulgFHS^oW}C%>t-5g+-%3zuP&b2LUe$Jwy&}y@IsG-hfoZ-PPO9cU29K z9@ObOg*3zB9sPBe?i`hF+EL{5QA}yBsS;PcsA=DZKB^mjkEXr6s)rHA-=dWiZVC%0i;@w+V3SVaKo! zIf-6BRuBl@2)i#IT==a!1-MzW>ZBZ@^IC?Ye^=$Y;vpz) zx!)0SR_K{Xh~fI1LOV}*U_A-C@4F?S7Q*}rb7gAL` z@b}mBI9KPuW1je45sSSP|5yA^MQu~EdD1yT`T00rtq++?0k8CNx5UtF3kIL8iFjlE zOP^7X@o6XVQ}Gq%@2q^;Sdh>k^8GVlW|u1LLJizrf=`tBiI9!t7JWI@#=tZl2iaV=MbBrC6kFo=#Ls*@`tz+nwtlRd~Rl~oWlzzjW4fCEb&8bLa1 zlgg871ktDQRnPkVOZ#|qcelS8Loh^Hf-EBMg}HGq_5LnE}NgfHDy z!O5awE|+d5!8l|(V;|MfMmm68RlYso`EPl@pYXbozc08{4gC@VneyK<@zN$r?GdcP zU%C??XlxKV^k}0N2vlJ$ll@y4UJHMi2Xp_b`zdl4S?p^r8N002vzhH^*Ryx0&2AFw zLje>eueZ6X{My>tNYCDx3daJ};zvrEjw?H8LWSv;(<8R?Aj?`qGkSj1dd<`$lH}qpKfDbkd`q~ViyKy>v?LlQK zn8s{&c;vFtPX2|3pcQ~Tfbx2Gf~WWAk3#tDQb zeR~)8Q5*}NkeAYL0>8h+JQlCq-`l7wY<#x8U;9a^3OQ$4wq+Jkt6CNMdtg01QYfQZ zbvJOuUlm~B_TB22l0t_7`IJGRV@P@XrP;>F>aZhNJS^BgFLlmT#-LbO3ncw7Aamg&L zGZH8!9zm`_+_M%;nE#xt#N8yW3{BkE2R&^lDT5QNJyXB3BtwI-THPsa6DNk#ri$8Y zuS@7ftP$DEuHRZuqztMuL(m&ROR>8|A9?4Lj5qDTBd!=C7cLuY00=1%*WML-E%Z3N z!}Z7wDL;+jv;e;nOcb-$BeSRSnQ<_q3uDv+Gh<-EX)oyGrW=~H3!n6U7+_DuhJ(RdX-r~GD`fiwS|QWpRN`Zioa{Y=dvV6g-8nAmcZFgVa~FT09WwiA7E=~E?+Rq! zwGoD(LA&rs_IdoU2=i`05}Fwi>QhM3KLX1SavH%JJ;VEs>IA?a`4e^&cf@d0l)ft9 zB`(uF*0HnKa)uq?F5t$CTg(~lm8Ma4iWrW|^u&n;EDhZFV+QZFJ0|@oPFel-=q^*L|W>a8~aQ4|6ezJ>M3PXGCxX{Wl2FS$bra28_+DIw&g;u*@q!;`d4 z1YW)DwkUS$d2W2K5YSbSDwLSPI65^@J7AjpIhhR+m?Cf>K0k4V{&7IB>>yh^Vn-3n zw-6;P`N#5m`e%gHSz+~L2U`#r^zWXC>MhgDglslxT}F1=BMPD4$i(2bBKx9j*{*VP zMPDIA7No7#8)2fzB`M+}q`{Q@M{4%Db zka9P+6I&r$+G>+C37_V6Enm|5Gprj(=d&3y@nes+4hV1c1gGtcGvlyXA3p-y4fORn zI>0vDYFZq}Rlk%aBoLxZ71gC4iMsj+aGsx3a?iboel05XO0If_708Xq0-X6;PlC1A znfIhUD8+;T`5*((eS-Cn<;I+PKyR3L4Rov>Thj(33zK}QC{c6cdY4%W(1cc_-MYC; z>XDqkq7C4alF6ROS;_^w6^bwrC?_U*Hi`t0rO-!uRfpSN@Tu{kgt@*l{4mV(aMKK2 zkBC5fkK8dF@OVYLXH72_Ts`TIYxgbjgKQGQ-+!|f8JoaND|%M{bS2+NpB#nO9!;dp zSh=%5dKj6BuS4BM=M>Tki;}EH)8V93S+Vkp(wRD8b2)O%D1}ao|F|6VR!$+ zY~7FD6f(p6<^5(nN0uPnh8VP47q&qHV9-OE%=0;s@Kk|FaVvnX(a;H_!K|~tCaBp< zu)7!b)|iGiug;)^Yz9+>RVsR2*M+n7xz1YTS#%hh0UiDO)=}Y&CL{3pdz88}(+#M; zo6lw0vbI+dc_FYNEJgg4J|Ez-M75T8cxw$fKowwz;)A6t@8|g)49KD5iXP{Zv6fua z-&XG$cjB}rGX-%U&YCe|*4AjnIn=}!Gc1A>%~;(r@SVXSc1!ZQf=r$9^wjgDpeY~9 zZkWO<%2;psQT0VlM}#)&xaiM|?-?4hvE(Tz_NbYtG2Nrz^>GWs9RD zZnUw!IxSL1uJE(9u5|%I>R{3#)?pI1C9!ZQp%VOLu9#5%fvjcD94=O}+8N;WslPv7 z&9*Nu>nJ#l&g3=0If$J5(C!tTBSTkQ{dsmsttR z_9D7gFL=r3WH7|VFT;WP5<2jt3Qajbg0-48O7^)|T2}mKf07GgPtEdSR~8*5s;P5K{b^xa8bK zL)D_A+aQD4vuwCqHd1LV@2s(xG2KUr(Y9}n!1#0W9$$v$)ZY>r5U;TyVW5Q92i!mk z_7^DS-FClRo;$kFlw0VvUR_Bb<$z4k+ahC5FFbo$kTebFVKA!g43-Uk%K@E+-}rnk z$mQ&g+|@-orMe7kQXa)BuD!Nv%@dOP2+SqNaxz$O=#xp@cKbHycb%*LRZhIX#OfZw zN2R&mK)VKtOV7JaLcd;bv}Rx&j{$7CNf@+5G5rHZD?8lRn7MKw|-B4Emv~{|EuX#Kx?)>p+V>}o_ z&|kfD3wUTHA%er|-&-<3Y5pJvI5FYYPm1guiA3+@=CFz-H zo%{?qV1Dzb6QyP!X&8vb>ZBBsrs{5!#s#MIgSp15p5{^xQ!*-tfL_!@eBk!nBNNz~ z53Jf=CYZ{pZ3=l*mX|(D+pPV)<4f*?Rqi1+gg$SZR-H4a3j*9p_TVAuBh?ldp8OD#36j zpwn-KD$2#6uNohQa@ySFYwoXj1!w*L6Kjtrqh(e zzAHfq?WwH>zBD~xDRSc%C7o_;^|l1`${Z_f1sYt=G&$wxT=W}bnwy25G*QS%seP{C z6ssG!RtN+$59-MbX|H5WbsJ*fsYKaPhQoIIh?kWY7R1}wdJwS};z2956U-qTgio_N zPe1|c$R}@V_VsmvC)FZI+jKjIHgkXFEO9xQc@$Kllh;Xkd^Jy8ID;8tR}J(0pS zCfa#<*=d5|!81FfyKHd1m0*=|M>`-<$BRJE<3J88B)IsX)V+^^;|)t;^A@4_-IvC* zJXUr5EsMf%XW7OFopw=yp3aoy@4+UD^7%@-lmla=(h3a1Ovz8frW1v)giX^9JR_0n z`3jvLSr^MpHq~~r4%IK`zEbM+(G_cH5aNY@OYmkp*>T;KCakz`lhV-cd)4MvNbN zhE{k7eCCW5NMC8Ufilk{S~n-QC^C6C0F3JA_IyjvDQ05n;5*5IUl!arjhbWQRlWF3O`}9c_p?O|M-5*OE=t`i8J$P9GF|sfxZl zJ_%@uoF2bWkBdUB;Oi=u|n>decs8mrp_Lj-biQRZ9`p_Ohw7 zK&qf7Y1c2sqHFMq(gJnD*lWkmVWo*BGBPf<$Jt8#d$}R3RcNDM1hYI`3T>Dm)9VH1 z>Q9CS=IFjjc9grARo2GMfAA>CE(TH-XvqmZt%Sph-M%3w@#ygR0J^;ie_Hb}u` zHzm@ll0QYZ{N+|LcQPP~Sl*L9JGTpPUZmUMF%7bO_n9lhrv*>U#$g=Xq9yRS%d?ceMfi=DqdENB=BfqX&^F=#(F%Xv@KI2D<*q`V zjh%jTSi3Tc{+^8A#K}3r?wk`eKu|X}M;Y#2Mye29M?V(-s5^f(;Zf`*v;75j`W9El z*XuI4R%(P&@QzgOSjXjZrfd#`CVY|54zn&2C1Mu3^QuKf!Y@TcXQ6reIhj~K6VqREOEQvP%~i^L~PwJ1`b!@F-K2wZi8Gc%f!non7=~4ch-=V|r!_|~+(w8?&^#ly1bpU$S*;7nbbqx(M?Cg9rL%26 z(^5-02NAOtd4j7*(yfW>m}<8Vzh z62$W;rW}y&kAjm)+i`iOirZ|_P9607f((4hMIgiA>QSPy7qAvHjX9I}gA>M=Nilhm zqiOaSeoi>xZXPc7wDt?;Zhfl?Btd0*PE7?3?YtGykmxoNZO7Ko+u@1d5t-+m(i05P zLLxF6Dc}6o+^bXr`(9`&mG~VWVbnX_Hb29kOt*fE9af|j6*wzX)H@ujag5V+St;J8 zc&#SU2!}J_s>5cV_S-zv<1x@wo@Nn5J2wXOmq30ukJAp+P?HZO4VDPwg4mUVX=#gZ zYX;mpmweY}^18GgC&X~lZz=&-@J>w&NvtDfzG6<|VHugdCXaC;iM;h8iZd{^($3Ij6g)7sKA@eKz##t~}|y1Ot6G;Sh;B{GS23dpJf*QN`k&P}d5A z^w4hV8+7Hg$b0QM7ro!mOGlyWo3eaW>orgBZLhQb{3nWpK!16K>5QxABLS08a(ddz zCF3p09v0lHvN&(h9_Lm7g}wH4uP1!@e&MjJ^t|i`G^IY4&?3;d)Uz+2zS!4UGqt!= z0N_Pr_SB0_OHS>|KCj9iH8HKB0t1EI9`Z6Xe3hy$y;2IH;4S;%u77h)r0#?4^4gyRw&kWRW}$vTDsRj`?9r6?phJ?GpB*HZ#=-~k5PQNP=OkCcBQ^g?;oQE7{TO@!a}vW9>m2c+;#wE0zCKis4+{YqFRu7NU42L}(eu_s zLxQ~s2B{`085`XTghbM*PCQL<1J3vh00tiLQ{6LS542u~ON>cC+wk9eug@-)>JwJm z=ERYu`76~6j7WLp6-)Xt3@X5x8oXfKN0Ih-z*i1j8o$>_H0bu!fI|R-RnBnbeIR-4 zblc&qLH}cg7MJRZHOrQdy}C`| zpILG!edBxAI|8^<)hz}h)z(uTNoPORDI12YX~fGlA)d<~xU=z~k-8irADx*%Fm(Yls)jfmt15bXa{C>s2NmjX)SFsCJy9)a`^l0?%y9$GiWGUDZUV2coGVV zD5^>9ve3UP&|iB*q$BBh9GJRT5mi~wR#?WfFmRs+BI|MKCV6jAmIcwgqkzF7_e$v^&*TRqp|`qBuD{tq+{bWm&xfH_5V@fbAjkdtTBxhuZ8JEYFU3 zBvWDiGmR$O{{H6Bj4u<3=fLjfNF({H16U+O_L|3g_lRgtqHWQ$#K2+c2fNhy$E^lc z3^?wyT?w>gZtCWlJ@=Ltvwq{*FKuH}_|YE6ZP&)f@$$0L0K^mJBXR)FJ0s#M5oR!{s!tuD~-EDnd4e3|ux7rRHsm zV)9hgrJ2wMu2P(7*bMGr^OD~VHIl*(EuZU7bymeS&qB&)!TPr;^HcjdLTuWPg{`)#_UnPE>mC*{fK)ySr84U zrVPn+%um#U(=csY3EQ(Tyb*vd(}c(wY6g(%6{#1#iCZ_N_|(hFWiGDZWRtke?UbuH z-LWBORws1J1l>TzKR(Z}>Z8VRMdtR@Z+gT8Z2a5mCbp9iJQ-c{S=`grf{v0-LZ(T$ zlLf)!VDf`tRzpD23I@nB_!TCW*hi)`QPndTplSa-F*u{SR$>CZC4-~iHYwqnTPffB zn5i*pTz}MFmvi9s5Qt(xus_1sOd6mg-B;G`+2bS?8-$Z)W|D3`no){akY|q?rkin4 zs&-=y19zo3hY_A0zK>c}zoZJ&!1^-#>RG#gzpROvbmVXdX%m?sCz$m}l? zbQb18R4M50YhXJw>8T=E^HReVNi0J?T;}b0<25}(c1Nf&QY}5{Tw>#={u1Jzr4Qns zK$9ApU>belA}d{sOMC)9oV4;+ooeJ-gWgBs6QnEYYQd*-!O=E+q?6?QI`N}MPZxl8 ziJzn|-eCkI_8$t^WJ22zF=)q@JNH5EXrxr#o%FGfqA;qD47B{IN1_z1Oh<@rM2;BP2#;dPT$t)BnQ#8Z{|DXZSn)5Q!%xzF@xIdRl)~) zrn+DQc+{etmYCFInL?|Mj`5Drl)<=ZUiTH5ztcjo^uLK%{G`r^M)9trKrqO!EYKkb zX-JXyTbndH)cH@Do?^Ay#aJHgf)$y!2a+u9oH2Y3jj7O8u)(EYUFTLaY0>mADfgdw zIgf%;=moKz&Fed#Wy_25+J$DHf9luuNmy4Lwjmxm(u$cbsCNZnCW(ezSc7?*Wd97> z!)^9Gkduuaf*TeSLbQt*a^3}#w80}aI}QSZ8i65K9_zF-CPuw7y6x*qIKIFHWp%R@ zYAi9rnWT+DoL*tCg=dsBweJ#hE8n0`DJ2{l2GVniC zwe5g-r0Mz>-s4;q!JcKNuhj922bYAN`k13&n%^G&-snp53gGWnW`f>@HZfR#&_Jm1 z>VK*Dwr=T~M7OQOL1SIS43g`e3%kkCql-4Q@v&UJbmOsh?Bj$n#alDFK;&)DYSQG@ zk8AuDWby##&P-23Il9hTDW&}l&g?Ln7q$`v&XWCfzhv-2YOw;kwtiBHJ1ZJlkdBIS zwir)sCy~2oZ?u0|SgI`Ag_TxKL>6QwO>Dr|Ac2&V{Ic8s4fL zoh1HQOBk5N(W}#H-GgXp6de-$&Ryo>$B@f*UO zNWGYHpbjK)5`W1p0DMNtQ+}b%D&R$bu(k)}{dFbOFrCUw1ew(@>tRuv@RloQ+*yt$Bw|C^yJv4_@Tyk~GfBNvaDwl$O3??;R(ybsd^vVQsO&O=CMn z7KWLb;~x)zGWf~QxpXL*p(=ZgC>kX4M)1RO4oE1VZ{J7aY!G{u9BB>2+K(!ePVbF$ zk!?C4AUTjC~g7dV=BQ zE4P|vF50$`;)Y)qXMYUle;PR;>*AzILE7`a9_2|u#&BNI0LdRei)IzcwvobXRl0z@ z+{L@fh=BZ`=jAn=iHj@IwM(SRPjGCi$L#3C;cEza5$T36=aq0DUklfaXp?OrSEu8X z&&m!Hm#ApL(n%hoaq-fK&;jl4YO}`)WWafgh#B|&wNe2&-U*hk%KCy&Y@wC_#UkUA z*Qfl<;K!9=>Nw?##2`%|^`LQO_RV|Hg2lhnYaANv99wN5qy$b$I4TOV>3SMJSpu6z zn#C(Bxb)Wr=W$hjZN7K6dg`jNsLC}Zc>l*lZ%TthMwe~+BrI& z>M&^CfF)1LwF}xRuJs}ZE!bA{zq-t(j*G6nTolzi99q-yu-_;WLRtM~oyvZV);ffP zdw?&}|0w(mi<_Lf%9Z{eR5iz9kk2lJgHCKZ!o^i|y@a5D^F|OZC{}WI%PQ^SfN(=| z_HE?BSLI<8tWVP9vQ(V8nW0f%7jPTp4|77zX-ebyM=)3Z3h#(JyA>Q|2_Jl${p%WIVbg zD@xJGyvqkeg&YDLOOaXpoSH`D@Q_$;_^Q37wxx|N87Bh3QdMdYA@BBzNic-4546wE zS3Lgcs$E(YroeD`aJ;)Tp;wmHE3!)){dz^)-X|EpmS{|MvSuytMmW^nSl{5Qk}8-> z4_@ECO=GV~TPjr)X)aNBI3petd%MbBgFd&O+u8asgKtdG0O**u^mJ`lAwvr3mZ=XS zj-s|iCQk^4K#R}hoa!!azfRjiSpqaFGWHyACwLhKgF(2H8z}Y#m5fSdkV*Z<`G2;i zZ4NOS)PT2TlP7Fd*N~~nLr)K8S6=}`nk|h~6{R)OCg~g_U|auO{&ubA2OtUMD%cjg z?CulkmY9KM`Q@z+PVlUy82lhh^Jk1WHb%;)Kbkx#36iPlTc@xAKyKuIZ8JtkWx5zO z&dwRc0mdBY2(c_fKg<`2{Z)UI^&Dwc|JrC)RpZ>r#2KVaPcoDwvxg|iBR&tuX0p4n z>i2V~2`gm~YKTiu%isbVsM=4vUp+J;Y1EfOJveXK{4LDPQn^3D*BH8%#FIb|0&l3w zn}#s2<{mK+18<(WXBPIm_=blFK*L)=aWZl~Ut@Tl3Eu&z@;fZu51uG(k?dr1m#Bai zKgGJtu@E1R}tg zdfRJ=?qNZm`op!+CuAM`aUVU${#xjX^(i<0Lbhjqmd#106IMJxTnPClKs%V?XVU_3WDIscM)}ANz1~`SKAj zEK0uQjAjxpEBYx|CWFKr9EWknLwkP6aHSs-{`vyM5_SF^EkZTBm0I`b=5HAtY@%Ox zOJWRcuCd1_sgVQL^HaFsT*O7fVXa~)!MdHDWx$q*hzijdVEtwZHy0DG;wnr}OTw|i zxiv*^^L=zjNgv4w)hK`+$PZ$))@~90?_jVFKGD!h6@DKAi9%4Z@pHbxth<`4tvoin zaPjK46O^lQG*Z!SGh`fSd`?8I(lNG&B>OS86#S95sD&l2qxQOrLqRyF%0d(dxlV`rqX6G7@4>`ZOIP`#fv_6kg_PayZ0|k( zp)nc+`~WyjVh3izbGnnU`rI}vbH!XO&>0^rF=Wm7;LuDd(aq&C{RIPjqWHbYKK{K( zNukn>adiD42AOkw@G$6R)F`6uwf-qQmi0B2Ep-VoV?-F*C6s)3rxPoXL*@E;5*PEU zY}T4~at(xIpj8fH%AS+?&2V_1kU=`0w32yAOQu^P8wC+En+wH!x8$iKBhP6ru7ofJDX?cIt;?7sIw`vItn5Q`70%bm);{k1r#3vm zR@%qG$6d9M{g22MXCsn#*y#U=B}%K@0KAgglUFsIiSn|{g69m9grZWjWrX`9*gf{V z^r_)*wZHj0p6CwlEZ;XUGq1m%O1)U36P@%% zh2Wrr@+V8{2s^Bu6U8ashoBme-jmqDEYmoL1$=R=+*KvdA-o78wlnVH0XJ_2?$~zz z<8_3%C=cwD!!a$7Zj{KL41r9UTiXQfgX342fI;rvZO}G8)vKou#%!X- zw2Q4PMx%cYlg+B#%9y2U`lb#(kJxt5#YbTZOWh7?wtcwjLMFoqE#0&sdq&7mBaNHl z0nlep2uY*>(kvq6%H$9_%^7Phfo)E;T#_P9*oYlSL>dPjfnyxD0ZeT%h6)GEarLRv zl5O!`KX4)jBTKy_>ASDyNE|c92nGd85@F<86&9J&3kwbok{WB(b**i}6%3pKV-*l@ zojpPMtJY!Z`?`|}#*2r4A%{xlx;INUHm`>6u_tkm^J7qfjV0Oc(&uw1=4M^UJ8M3o zKm#I=YZkReH3Xv$j#im@cSiv*D)iY*;)7?V{29`F4m&nWAOUF=#W_P_`rBCHt$Bxf z?PJ4XcOgp>ID_O%ASxccp*aMSp&ewNHu3*UU+Ud)5Q$@z`>C9Om)Z!PqK3i!`z4DZ z@WCF;I`t6C9_jsJ2qHrQ>DVxm5nv34(zx66bzLXNnK@^KHYL@R;GHGOAb)#_z0<1r32V&q^;lf07*MrKNR02c1ccnB3s3|R zwS3HSn+{-^Tcg&p36x|6VSklvti>XRG&dS$JUSJ2Y?9*-YJ1i>JJ|FT7G#QJwhg~n zY+&$_Wnyz{SdZ=Lr+X?Z0?v0#gf#l1){rwDhHT3GUfF*@EG=Z+pvogM_S_H9|c-xmnwC^3A@L_VIHronRSZ#+a?D?rU zCtcS$17TlP5@m3Zo$Rr!NM9}v(k!#c_mNPD&`VS6pb&=X*} zsn{yof5J(4Cd9MkdZ}@&9U8=FGtGrCqp>|Va$RhgjWV+Bfn?}mT)OclUY8k0AFN9mapqD){MW{u`W!WR!U zQ2<2UEJDC)u#({xh}t?=o|zLu3@AE}ON3J0t=kt$a$XV`eUdGxXJ*4O)kgz`Q#pwr zke=@{h|i>{X7UG3uMf57?p+E;>rHABBay0QUkp*jtsd&(qQ{WR%hz`eV< z_$%~;N`4Qb)B-MA@y&+6qVnBX;ed@`F?Gar`vI&J>QgRK3n%KZ+x+yrHsdp3NDfrx zVHVg!hZaE7Hg|LZ(mXX9Dza(e9$q))T)<&yg?+G~BQSV_FQ)PA^>wfHtOh)Ic^==IIe!6@N{g8qrPz8Dc z83#d)*pqT`)9M4m;o#qzjLscI`!K?%Bw>?&2}#?ags<-1OE^gsdqc16X!lS$mN-Hbj|_cG;s!HFZB58 zCl6F=I|#W83|N(ZgrYQ*ccC^TrkgW{ksRd)G>TRJBSUnaMS;-B8JacqdN;74&gB@= zIB*K?o`_|yslSMiGoYH>SMA_ndvm5f&%NRZ*6!2U*vL5Ow#87XBxg$LVA?6%lbo@8 z$%Yc_8&Br{LYnO}NE~tHNJGOtn;Ie~d0Z5r-7QO%Wd;D21oy`8%T!@pA_Q(88n~7G zp|(xNZ;{_4R>-uI{p^Fh2=Xi`|J7Z~t47L0_Hb66)T;KAsgg!)8J!fE1dT~zN(-Ov zZ1o$GWL>U9ql+|ahmpSbv?d(lLGzUDmKs%IplA|)cV7w&QG$m<^fwaSszYLRJ6dLAJYA*C&@xTNLGp2XvLSS9poy04>EY!pJkh=Iny~+W9*C!0?BHPT7c^@YBISIU+{t~CpX?@Pmx+MZ{uqz+W1Zy(Kk3N_ogGKSqT4=v01Uv5-APe= zFtzw!W>Mb0>h(uR{85_M#t<=}J3JU3V^ebox0U~Tn=G1Wvo@om>{V$P6+Yt>=1KCjt&gFnEuZ&s5d3*S?I;?iNSW~(UThR|INBhhJ~Qg5{55> zbo)xu(G2T_8_}H)2k(Qsrwo(JRn5wpnxHpW-fzz={z9Iz0CFHbcBXS6L zC?PACK^nUYR)^FM+b++C*gZ0PV}NA$!Hi#H^0+-Y22Jg?w0Rq?9{QFRk}C!#4WjSf zBj>~Dsp!#U?V){ocPsuFJzIWQg-oTx9CBO7X(&Kz)gUz&P`eK`0xpViD=Q`qz^k$0 zK-j)%ETm9MZ7u9B%tXAN+BT%TMpoJ##-Kc+pH zGg{gVTf21}CB3rg!r0gP{1ymqfD1jR;4>_Lt=yzyOEe z*E8EVeH8xYUM_8<*0;uE)Jk|S7~Ch&l4+XG|F~JfF_@VlcF()S{XoEeC2Z)Z%|J3c zp>sMjk7wGU^_u5Gu@z};v-)85F<%Yb9WPPuf^H3!v3 zR-E2XV#2B~dB$+gbrSy(>xqM2u#u`7(GktF z8eL~Y2?9ZdNkP&od{ znTq!@Ii`d0R4x6cXA2O+aEv!~nBL-IMVS_G(3LP%xZP!qbC9M5mDulbsLH-rJibeu zx#pf(!?~j5=UTdM&e-g6p_J1^gco#MJC2zpsPgR0m(Bo*V~**HOd}9+Oo+__(EK+hehxizr%~TD0Kq== z>}}kf8pjuE9b~OXfHLM&MfP6{^5#A`)Rbb?=R_6ja%Iy%hFC6yF^fc*Aue6^NUT2+ z!$Lo~g4+zn!@-r6sg;aj1Yq?hc0uSAE)1EZJVR8a#1UycN8#yl zgh^Y31xh?Tb9{L!Y3p-!giFyv+l3`~bDI;r&)W~~=M`91#5`Zcp9}>I)LG3nkbRU=rLx+I)@z)er`djgPrY~BNLCP!ALD;Ahmn0 zBFpAdS&5UP7R=^y5I8=Ie53=bBSh9N;hI;8)OKA z8;V4xdK|A_s(qfSt`nC?4aS`HM|nSOnvbkaMv%Lb?!`o8N$C_r)qLa2jS(&i27?_D zt2ThjV3=@EES&6%wPw9qA)zzzJk8=X_isgXapb?lxrV_ec&2mS5_VL99)~|o67>Sqbguju> zY`V!A4%7Oc&YfebW@>d>o?1-|87&P>dFBcrjC-jEY$c>$ zt8E8SQp$ck@z%VKAip%YrGuNMx2R(=UG(kNWhSR+hK9dZcH=r?z@9%g zj7Z&kWI#1lA*Gm2>(D3E(RAi6gJ+Cl}P3Llqxvv z!)gF&Egyvx^CnbSd$b9QOGC^N6v3ews8C@{n0>VTtEG>{DA~9H=(r%4q9IgT=}d$g zfmL&-0JEFv#Lt0{0mab@UoRqkXzgQvY<*G@7-4ki(%!J~F4Jd|RJF|%4yVRt}az>9PhAr2GI4&_P;cM88GD2L$ z)ln<=qqsqaoG&HH?gmB39@R4zFsC6&`Dws~QG&^gIxS)|=sjOG3G2w=i`h>qtvtBr zY8}lWe!&;@NBxQ65aE+C-Ql{WBV0r60y)(>_*$H7pahndcj^unyM`JPFq#8EldfUf z^p7@Ca?`r?d5f$F+t3EmA@r^3-OTcEO0V}eU%7)VoeD!qoQ~U|Ur5HP)*>J&U$D#u zV9<>+Q-l#k&ZQcorzUfl@7rp@tPXX0a`p%Of|k93Ftw0%6UOiJ$99})Xbi{p#IS8Q z-+*)Nsapk7jTn?P6sTs7+#IGE=RKMWgx4$-6A9TMz4GLb2hMeP>(?l56l9ndxg~h0;9b0=NxSH?g zV-#qj3gAFB2X1L|)d!y#Ew&Wmm8_oYrB4gXi0Rq3XY`3RH?*bL$YF^_NRecnP7r+K z3yaivA4gNxH9dEkyfOX_bz%S_Yz|VWQuZy;n0S<4FXuF%>LM<6T&hbSPc2 z^wr2#{J#jfCEl5->6;S`eE`we-)d-cOY*?TdyXH(6sJ}K3Ny%KbQO}9#x#GgN8y&g})g~3;syt#XGs{1ze|=1d9bQ%>-)0DBh%gsa8C&-cEya>W zFt$6G;guuZu_m_+EbrwG8$OAQ<4YWFzDfQ`=ys0RLCS4>D#EHTg}W^=Zu^=SANd~+ z7}R=MpFZeo5q50NV-z}u%mDN9(gA$iI2aunmfhww32MM{X}X1dUtOBOL02BzPUr|P zHM@h`GQGZFYY?}yg(-Ff<}`!=)dr|{w#y8d8@0PKzMzxJ9Kbj%9(ekVs{!onw;t6S zv_P~x!E+%gdgg~0(iB`*Z_duL0t)w~VaugQ&wx-o;-C36csQk!EX zU@jAknQSq>9>Xf09z&rv$rP&bHsa4;(CMM*7be1Jh`!OriN{)+u*v#2o*UYNW3!NB zzGAXqmfEjJQIF*~ZS^6R5rTWTz0pr4;+Tqb?eK@{AKQ4M>R1}n_iFf!SYB`lBM_V) zt31)GzO+@plz{N{$!3-M13^R5NG8>6Xg=q*XDHH4J{M<=h;PrU3#K@L(AAQSTgYw=q) zcPhXULN=6Mq_!Nfar)$`u!K2IOgjSU*Nrpgl1KU5e|nR;$tBtTMkWEX(g(C32w_+w zu~$p343^F(w|z`Mijs^X#36_7!Cu+r2)Nk)&Pc&84ok+e_!&EH!2nDwAg4Dfjw_A` zTp$5W4ks)!=^K}{LR6mcVOKYLY{RAJ!5A`TE;{dZCmP=$sW9vSTvyd_CR{<=Bpv8_ z!7y-a^Epf$F^Gvb7uN;7p9Q3PbfZ;5pObe%uO=|yehJcC4$w9BzKQg_2(*55O?n`Uljf^6j~Df{b0%R+;h@D;Z<*iL*J$ zOTQ@wqn!olReVO;pYhyotO{2>n0;n1bpahZ9QZADa`aKY^_EmW_@Z9(Zd41Cs*7egJX$z3p$Eo%Q6J;!sT3zgnKDiE=$w7BWu9En4TFs3 zM7Z(&+)kxHm{i}oSO&7Q$}43|h<>@pIdbT-uQDB9U~{*@95K>JzL{wDjEC}y-Ey~y zE(3 z-vv75l{V(0m?VLvzUwsEX8YKmKDPVd7_^&`qXWRzG(T9M`LJN!@k!jk=Uz;m(a)Jo zuCwhuT@DIIK;05Z&+9&yN8-F=XiT)$dv#GBnpG;2bsAp@6l0OT1NmnrW+^cH z4Ee}BF&rCM&-Zjs2$jCsx~Yg{OErkSDA9~Rt*P3q`FVAzNcTSz*99~6+nYj+UK}e+ z8{%z%;T-a#umR-)LsP<^tn@*w7IQyM?&D9dJ^NO9&zGVN2hdtx43+*-LAT7;iWD@0 z7Kq6mpI6f5+wi`5_|`Cy=8aBfCxGLpYOn3gC67^!f;0I&SPeP_7=ht>vm&R75jH?z zc=vJlGW7LL&6ZEzJ)GfXOFkJEbi+iX6=PEuWUfbb6 zTvxz;2)?!{+&MY-^Zz-}JI6%z$DR5_)(P82xlax@G=wFJnnetZt-DZ7P;jEi*8HhU zMqnuW3__T-pZE>JccgORcCLEBj0U^)$w`(^nOhFWbPA$E;lLp;aYVCCfLGHN;J2IccIBoh@m_ze;hq1SLV9(bnq45 z{yNetAyA9lKH0JXdU2$rLZb$znK&k^)4I|AFdY`oyU5cv>=tmv11gMiKDj{tn_e@wV(s# zx5dQjo&hmb!UPOQ#+KzscfU8b1eIehGgl$hu|=7=IkLjY3vzwq;@%hGHDwF>z>FWL z+*g?S@Y(J)t0N9ODrQ~lESR#552Q}&2ErU!a^1_qB$Rz3e!AjuMg?;yP?U^E8uGz6 zIA&$~(2i1+9Au12;!b(~ti$fmB7XuhK!S2~G}p4ofEk7r^Rz=UK_;NNbtT{`4QOq# zjD6Q&`Ws*|lwYX9^<~k!v8lJ{Rho=N>8VY6rjsluES&N9-(Vks#U6^e_rNqQ7|m;r zDVx=~;T8-vaV0mE2uYhWlzOU*hK?8Gpp!*6h)IXbw9#2Y}f5u-knV7 zF!)<-M)Q3h65X{B0ynbb`2(vsVGK9Vj-{O3@$eT1>hRJb)d8oVfT47TI?wsUQ4_Y1 z!e3fh$MRezF}0td4sJJYIQGY288+T4qDdD-dZN&MNK`3U*}WL0<(LPpq!1|vhmbx{ zF}d&~?8HchQGgeXIC&O>SpiS2Q|PG}%)P%7Tx_=khioU_9Q~!3n~FuZG92RF0;w8S z(G%Qto_k>`meuVSGjW6BVhX6w{di~qTZTA7Fn*n+77k?UWYJ6;gqBS`(6GO~4@dR%%7=0981DWc zS#~>Tj?3YrxF@>>1mO{<;ZGr}~2(AsN|w4>RJ znNJTqC5sbsw7|9bfuJOsy z-s|Hf?HBO{;S8jgjn={uO47h7I?+mW&>wA6wcZXRuY>87e?kCnNSHc$*D~5egH|HS zR6gy4iF-QD(oxxg)^W9OW`!%%T|mz#DsJ>M6D&D>hPXXSpxy=?FOqnVOvGAFi)5w|i4 zGmfeX7(-?_%=WWkp2gi=Bn{1=HSP!p2o{niZZrFAsG;`604|WJ#Bh*x$Qr(kI9p`> zQJsYXSL%T2JyIWF@A#>Ftyd~=1l5C+8)hZd16o#xPP6oElRWIk^?Th9%V|#|iMeT7 zU^T7W*D%|VC`)XaUH#DTE2zQYRyJ{LXkMDsC|uZtvUb&CcgEn-_~KUZg_1&Rwf@|y zjc`1Vh#K%L7%YW8WRpgkS5$XT<6AGv$}>4Z&NE&As3Kly>L}7MCly(F2Edk~F}#%q zTORP4RAyV=rS|E}hshZlhLGRTeud+cum<=y!UuUpt#}8q64Z}c28R@}Lkd2m<~N%% z&T^J6V@?kHw@IoG(jkXUBB8}&B!L1RsP}|;kx)W0yZ%#%E9z>7lZevVkID{|W3Cnt z%H<8p$;(RNKbP1XYc7 z5CJMI_+Dv>;vF{Fm4LPrIhGS+P5^JV#)hS%v~5Mufmgzuk2Vm&37;d%0u zY@uZ8fBXl&+8rlPha?A>(9!aZEh5Ctn@MY!+yQi&zvN*5@QZ80h$UD|cb&$ZQ!BwR z%~_+$?jMnsl12&E_1Jkza#Ij zxR4b;)1>yt;Iq5D)o`)Je$3t%-K*y-DliUqbe}pKqLO3RE!zO0jZ{%4(5%qGu&+R~ zu6SQQKJ?R7FZ}kn=h#{{ItW}<&9x3J zas#w`RljAkZi4?d@6cyvs$}5ar&Ns`>Em|Y2FJXNN_YJdpu{)8cqMn4WC!rN(^3{- zTqKtSW%{+%N2SdljZ1be)D);H$bnr6`{yBlSgS_?huN$JLPf|P9BC?a3+&^&OQFh0 zkG^4zEN=LLWeq-%Pk~%iUf#crBHTyK^rU`CZmA(dD;ELPfbW>tca}Se3mIvAkETeW zufJ}hR+8^zYl!e@Cv+L`Zhl0Dqp<3FAJ?8Ou_1f7ISKb6rRV7XXEi$Pz)B1GzORsm z1~^Df`~6J7rW<2zY`4mnL7#)XP}HTKA>{4}oVe}UTSGHh1j3}l%x|`z0;SLZ36PiB z!YD&QNIhYBZ;It$_nVcW{=;SADVG_DWenbm?)#60>Yt*DsA6+XXBl z`{}a1sa7z;6>2tn76&A^3wm%^MNsX1che9K-Hlyb9Cvw*Z5cGlvn#+}{I1({bP`xr z69UQ4i1iJHEjBY6tSCjlNSw(|i8N<~8YfwnR_pN?2H89YDbQ9x@418Xi%#b&OY{iN zO#P8Pc4~|+hpx!LcBW8zih&#lsK612ZVx2bn3!un^s}2omU&B+s|($0eWLFCkK8#@ zRm9O7%~dDWUbAEuk%3(vO%Gp|U0=aEqZa|ODTy>5Cai)$LG_W6P(5&AaBIvE>dxvj z?l!{%iw#~bD)6fEpniCY?0`*A-t#%00_YIf9E0yoDH&1mfC=+E%jFA~G!ILg zq6-N8k96@Y0w0GqBhF;dF!>qxKkf}KT30#SsdnpsEQi_PTo~I?3hVRC2B4oc`j|Sx z&Mspfk!m(pVFgssYcMDqN{2@)M!uUqnb$QDn<|5d2grFo9@>;%zs81ShFe449WWTe z>$Z~RjL&vg2e=Fey_JE`jqG-P%{7zICIerQf8TI9_IQaM5yfC4k^H>68kTu8NhktU z9$8|+y-~I+$ca!Xv*p&I8#pV$0XtK={c?FxJD-3nnO{h@9fZd9poCvtHSqg$@eGqr zM;a)qH^+n)h?{%+L`7?Pp?oX3$YNbH@~z{dvI7T$MltMgGzKBRyVKj7$7)CN`?dN6 zJm`o&^-8!&G)faB-%GfCxncj^+o55m^fQ75(j-uCBA5*)hl-6!`_hgwnYoRJRd;7Y z0Int_@d;EA-TCP8IqIM| zP=Q*z&kG=Qno&5Zv3x;{)7)CI=J9rTwb}U%AMELoX*MyI8v*17f{Pv`BgS9wJw&40 zjR_t&b1AUgBOhE6NPo$U^eQK1FGn=4j??i}L0lGX8xj&(XgP6wt!BO%?%zq3)pllJ zgEGEiT_berOo?`x8fv#M$558Jb>31U)9Zr{VOO~v237e5&%XdVV1_1MY2_G)Q|zo2 zg(_v<0XDmFvh6rb#gX+8&>?S*wp&6SZF~@F-8WTu7dZ9OZXgJV_=ZFq=45W$u;eZ_HoDD*nz5zd4ju5< z!!&$}k$S?-wrOZ4N+mY5Ij-U7I!9B&m8RE~6W)BmQES1grf7*1FHw^UHVcHw0du5J zJ{23`d>I%Z(p%&PL`uR1q?HG_iT*f>p>f6b87CuL5XCwi(naOzYR;sMbou@hY>c*x z*PtGGN3b>(d9jbgA1`PQLc-v?WBJkF6O`J4oP(@T7ueC(ub;1=GH%)$KU`*GN~Xhr z*`fm6(-n<%)6-{*a3;l`cOG*w*p*0BB6f8ER7nqMA0_pA4*#gQsK;fy-5uNX*Bg?| z`x!sd{VX2X&Zi#R#nFx72@@T5V>G|w!B)tDfIPF5fxiBNm=TfC_cJ{SjA=JjUSj*n zmk){?^irxfiMIuyX3IL%!REBe9;*cziY@ioFMViTWB8CJ7?J{fp~0x>Y6(e9;;hzr zI|>543_U0&?18*^qBM$AtHJcHBenz?ClkD?V@iyNYk>TJe)ETOz@P5r#6F8X z9Ad>*uvN7W=?FzW!FC7pJpjUhFE*1~#8FW$+ELV!Sex+f%2wu6CD_l7P8s$g0bH!9 z7P~uns@9P(?8)-TNFgj8N~x@^l_^gSH&Y%Luz-H!t(y5x>Ovzg*a{BhBh`W?*#Zh- zHm-fLr78HJs|U!z(qe;!i_Gz*n@7B>nbyH&fUP9h(K*P5%77seWb%DT`59-UHKw*I zjLlRUe@9ZxpftaAa<|M0+0b;Hn)w{R-S78s0n?V+B!XtjnG?%nOU%j9#%(@xz>Ko3 z<3mFF#Ad-qu*yWv<(ixGayN??wK2(!p3uhlVav^Jl2UWY><-ZF^BW3`4z{pzo_;$j7RhGDQ>M^xYwu zV_x7FEpKdtlbRHi=^74eM$)|xu@dxJCzZyMYof-PHE;d~oNbDokL3bPgQK!PcAU{? zZ2i(ziO{M#KQ%;oP=pUlUL4A2;$dT{&0NS_iHagz?RIr+=Rj#;I)x*yr9!xq}-HyJx_q82dOfj8R$#-oJvwXau zBXS}Ee3XacmaTe=Ugk`<vf2XXsQh%5*uoTF zkXRGKoDG>&grab62J}6Dnj?r_?D{s5fpwg()ev5G8K7q?N4szN0psTjC`KRsG7nms z{rkKGpR}2zAR`0W%in|qc{%4VF-a#Zar1FXK%M|6Nb`b zNy~uNN%c0%iW@HV#|xT==_&ifW>JlkCsmWo=lj{rZ*qbex>&?EdB)wx@)n~IYhD@R zaM=(8%n=~^F=2@4WEL$z$(x=rY$H|=t@Ac>O1ANpqse0a+oS)(hCzLUVcyssieZ`c z_u?-T6)BKQf_UH@ z2MdiHAaApG^^Onb_B-##M%|u$kFlKS~QMKmreCUHT+3G zwxdW`qyesJ;+={)=PH|m0UCja(d1C`2Z4QdHhqiV?RBhwfWt9VpHT?GBL+O%14&bB zfpX19gmqUK73t`1kxlg+s_Js#GbH9(SWRp-JSHc9AOHDOhlvnyT&hbSV$sCn6)vtT zl3c@JGX5U;bV3>u#ql(;)8&k&z@1IH7VbhA)l$jzuYjq1f#8#(Acfyp>~bEs^%yh) z8OV^sOSwC~?6$#BZ!Z<$oTZ`nBhA8dLrY@S%|*F~MRrGJvX}+|w!4oNj!2Ai^Tc+U z6h&WCB04ccPqfhqI{z}5A@qvq`gO((_yWv5XAu1a zDVs@HazA(@P*HPzMy)K}pxX7rLn*kU-p33J}5T^`5|Z5`jJj+(MI9ZPj_ zl(PWhQFhiGdZyQ6hHDn*MQF$_`%DIqlVz4)%ZHc+gWDB__xO0LZv0)|Vh2zw{j}3? zpL!lYjt5VAJ|CQa?!{wSmdlk+if=b5v&)FDqX`t-WN0`T5p}luXvn#V&-r=>XZ8iR zT*Kn)vHs`sRg3FPDQ(cZ;>!jy>Yw=ti}<#w&a_;J@vCuIEa0ZPn4F*1>x`h8U`XLU zK$N$(!=4PLQZ*u|@+SCb$ZD^@)JgX9U;TRgf<;N7TlE%68xp)-yyjuD5m==P@K7<( zLIKe)KeylUWZq#@?OmB}<_AVQ5ZY%q(MU7en#luS+{Y6j9grrn*zHnG%~Zr$j?XfFmrBkGn_Bk?u(~zhJM87$SmE@c8;glWv>?~0Hno|$ZEx>JaIx@HR~p`B@FSZ z%-mr>bb}4&&^YM4LY@f3_03_5J?zYp$icT!MIR>HM4{Yn%Y_nsRlk!bDm=QBI9P&8 zSszg?Q!0(?yN_4>&^w!6<4r9MV~2cgSk=gcy~X;yYgsX$cNzK-p$G2P+{Evr_8pzN zOFj@`^(EcKEZP=ljZ>z8HNd^}rzd?YEszRsI~Fp9=AY>@lyn2LDhcpDCuz;1O^x1+eR11MeEq9N zh)s5=-(6s>^$Zh7RceeGTxrk#d#o%rS=x?(+&6CWf(r7$S&Yvdxz=b7C>{#~RgbyR z9l(t}SfH0ETiJ(B9#Oc&PvrkkO&c#9N3Pu!{4^0$%ukO!l@8sZX=LkgQwuiakNePV ziUtdUyW>q5vHVsWb7fG!eufC`&V-SA-x{eqna>6MJz9a^Hg7(k%gG($RoHnDfrGLG zl?Lp6yy8gKy}e?t7(}^-Kl0` z7xkg8!FC6)v?Gp$L5^N&-QUdb4?kL&>y}Lku%9K$77NTr61@K3xs&pD%WR zR0E-Ikg8{yiOz;h(HY?jpV0)5T-?K|(4Db+CF1=y!&rlSt#M42FUFx8%97Io~`p{kmP1?HKnB<=UBmG`we%6bNix%;i%T`~)chCtu@ zHQ!4=UXk4_Or%aWGsW|bJxtdOGDKAm(d-Zv(D*-Jk|s0FsGx6<eG`_}OBTD+ zCq$<`PnS61M+LU)bUS)~drRNDPWNUoiT#`B^=f6d7U<`n9#2Ls3zj)& zEj*)X`Poz9(nshey(>1UJ;(dUkasdp7SFB4S90+9#nHlj5E7C$ZAfAUHRg0zIHP3O z@mQ@fdMwi8?D<2Ktb<6}TQ{-4N|PWS6WY*wj<3ywKU}MPosVCTROn;i4%-Yyr(A3_bwFwKx2XKsK+DmK%+k#3VA{f@(Ko(}kgdMV*G9s2jX;C!%}Mv}>+?Ixx4^1}PYi9=kX!EhB; ztZ7T!;%!Y1aCfNjzQ!3-dC8YV?q8q<_E?ooz@Z8Cq0@|uHj+sF=MVaO`(fCAvJjo0 zBB^@&uBJRrBhM{+wmI{_F4w^89+;u5V8l~q%_1Ba5p}LUMjik}mx=W)@OUjRa`(K6 z-(x(vip(y>kV@qDC1C~ijN#pc+0&9_MCizq3t+RoB2ez(N`{#dt-$f%y1n?qM9aKG zZQvQ1^<&0ORtj3fvSDm}J9Gr32>{myOe2P@TZW=N+_Mxer5^j8$bH1@ROjm##PQvK zWu~T>yRF!G2TPIt9iPCN*m{UJ>^ambRhCi7qw&-VwNpq5H?uWp*TV)y;Hu?PHn}*VA7NDlnm_3KR(5X?;}Cg z4NVJ{vtUU7=-?xcZyR)GWQ#|bY-{|uzhZn%K@A!IzjmJQ?ZiOgaagwGCeD=|}8I5u_JfT;hT zND~du0*g^_Vz(<-!|Q^KreGzsoWL~gl)v>X25?D@*+lJrv5*`bQ?do&)+4#^UX7+{F)h1mS_8DK5jjq8xVNH8!-# z84W=)DBH+9e(};ElY48TUhZBWaz2tEsrWXl{+W$XVI&E#R|{`X@OwrbFW~Zrq)#?v zUc3RCQOxaOMU#RnSxAcrM#1^3)a9HFSTLWxh-~f$^cnF9O3T_&e$&h70cRIXk8^pM zowJ?`5snLmRTqEppM(~^0o_lHF8mP0KBN>CKtxlqLgGe`jc?@k+Twx&a>TisNzw}o zeI|iUqpv zh6$*{cpC=lakGi2GlGXD)1Uv!Ja7c}kE&p{q5)btj&=cZkv%=le_XkPJl5F>=sA!H zxn=EVwvW2*vW5#;BuG8&g0sKIM`Va>tbfpwesf z0_9E|>SAEC?2GlggpN3EHbGMhyZRyLRCb)K0yew=7Ay=lXu|C^kizWI$Z-?aQaA!T z75&G#`>A?C2mcS@-IeI;|Rb%xja6>0FU4Znwn0>b;>(P{qXhm zz@^4um|_}!@STyYssehQ$;Z*k=%Zv6n$blS;;7bN=g&^Dw{2MQTq77A1*v7oOdJXgC3 zHE8KEDSXz)1t4=g5$UP&Y}uv)Dv2=6@y_Il@`Wg{~zdz@X>ug^n z0{}%py1!{1=B*ZHo9aHhV?F;r?=Q6j4JJoI%)54|)(mizWei^z7=6MK!h#ydPIX|8 zQZqC%ppNaEdUF=M&vbDeDv$ealDELM0f$N9$_6q;&M$+I9o*(Zzi6Kdu`|ESrA$tV0*OZ13DTU=?im@Is z$g3#VNxaXrylDHb(PZ8ia=qq>{BZ&3IM$ghCKeJR@K% z+QBlV%x-=O2ELO(+C}FKqWclDHZiN-_TS%DNp)1>7%tP(jEs$I@#Z@(&%+m4EVIO& zp#IBpQoWi1=@WhgeE?BJFEZfsE>&mm^@SR%$;d5!k4FESX(5?&Tz!iyK~0K}EM>&z zT%S)|FJAsEJiRVs_3Q|-NvhNqvbQ?{MYA1Gvq&~@qLlBCk^F>T+m4|LT%Xw=%m1Kb zuY~gJTFl_-xvoGQxMgwuR)u*sG&G8L%iL0v;>e_4|I?BU-acx+VG6hqPKKocp+Gcj zYNh0J>-$F95iB){mhyx08u;kVIvy*r2Cimcha3hy(9^DG`8WSv_>4|})Sj8oCl@As@I14`C zSse0(!FeTg-*4=!h@jLO8rdW2R*MkXkDZo`(vDI3rF;D@+j1H$kwtj5nPUUKh zQJ*DOA?8lABEt2%Etqp`1LF}qMt)~+pnPAK)v=8``x^6VDI6<>QX5H?Rpe>L!>1+y zo=pu>@f#)^$|qmFFx;V@#p`>^ItS0Zq2%*Jp_4^CHsT!C)nQ?`&7NyN`CGaPJE<^(3fYB$WA@vj_jreG3s=6Ht+DR ztVbs=PSuslM@16PQnauGM?sf^?d~8dTZYga;fUPAx9Kh8>4mt8Z;_4T$tigcnxK?4 znB8FXL^DZ;7tvoi4D8CveW`#&z492NH`>Vuz-LU)pJmQ^sexi`_$7+pmy-)KNU~`} z86YhnscpuJ(#w~WEe1#X5UUn)OC3j}b$sHdJ}2`i4Y~W=^pQh_uapDg5%vECwug!> zA|xv3j4fz)DFCI$dnnANMG(^dJ?X4jufQ`?Vp(IN5_o^Ei{I1-oy2bL@M4$9z?`dVT>iM;+b`1;Vb2wb060kh=NOM2R{b7O6 zTQ9>UR35tjwY&>zeAvT1Ed8H_-sAgQktZDs5>PCCrrUy8ZV+K@pe`q|NA5089x4sA zISiIyWid}U$^>r#JSI44yoL7R1@>znb3V-2A!u7FyIiK76XZgz#>H`2a4Gh2C9TW2 z4Bvbh#fS)u;%~UVh+G z=@6|PpAz$r5Sz;~P#NfG0n{s-rGoZoc=E^Oi5H91>VD=LwZU&Y>RmH1zdSYFB5*omHp$6-Jh1;JnByI z0%S^3{Qeqty~&^`tCObzI~K>6>Gdm*`(6=nnYwZLc<`m5Z zYehQtDTZR!3iZm4#cwNb1w$=7YYM*Qy;AJaV8o!^pwo=b(?BA$(t=q~1fxWO&Bd4Q zTdo7@i!X(@z5b#g$N5d#NwOdTR^7l_P9AV;A-Eeb7Zk@1WOluD*5rJ3xbQxq%!Fd@ z%0IuTSXNsTlHo?4nb`FRz-#zMEio2XXwzi!b^H8k&BniZTm%ATK|W>MM#=P{)+xf! z59m8X=|?j)M8%Zyr?sk<8HrpU9fx<#cqTUNWRg4ppt+T>^~=gY_yf{VDjxL)guy!%JkRvN%E(4#6$x?Q9R_z zPNT}~!lD3I&)|q+a6pqcf6lDE2T!;#cR6$cSz;N*+zNwsu&|XJa7rW=C*rY={My-i zpS$z%x&-^XK*4n^Bof9$8&L&s>_!dG`H3n&PE$O?hY3Q(wbNHB*H=D`) z1@Fr3`j>mj9Pgk#3%-q+n5$pzj}uVmx1V4RK#RMuy}{%RxL}rmf*M4ye|o^Py5#1z z`KXxz(8MX=x8b*b8ZFfpTfCt2GBoqcOWd-hq-&$8?j3qY z$5iTZG{EEqgL?hDy#FV{OKL3dbF1M|K-3e~Q+!o|F%{)A?h|G5Enzp!GVujr9sw{x z_fVG3iJM?2!U&rkg|$2b5jWNonf!swA8H3Z?(apSOSphByIJwik+N$)ASvZ`q_Vb= z{#PIAI-G(y5}W9ES_%>&G}#MlT#Pv=Q0F$4vTE_hZK6-OAq%@_Mgz}ugfxsX8*qOS zzI@9yfSOi5%?%8kck@KpvL?#%v%5{%*b3Tu(<$Cb&663fyd4(+dtLwj=n?8QAf_XGspr$S!0V%7fIu=!G!PytVSpYoY-4 zG)}0JnO(dtB-Si$`&SwMz;c@PMfS#`sB=SJfe-sejSF3tydvhn(-}i;H_vg_GT!5X3=`euhin;Q6nNea7QRU$#6|i!w z@O4tVUO)ARx$J^opW<06Ex{atS0gzvT_T~L?u>d)a|?_Fib{}r51p_?E!I^9gg0Qb z{ri6lyDntH%c%;LKJF=DN;?)uUdTZJYj6dxaN*H)Q>l;o;_MyNKTMr_4E5=$+({A# zGrgPOp5GKAiDEnP@vS?RIxNa?57~6(@?Hm-1^9pklq&0Q99Res_ssV@qb#y>TiOzD zoOZ(QsC|fci{shdywB2^0#Nhb6R2n>>is%0yZEv+t+oA^RPi6|SIFw<&&6q-G@5YW zoZTjI_#n)jMl-Crf43~z>&6|k1Jx-LKHb*46bD+s)@t?qsFp`x z-Ri4n%xnlbr*LQLS)F#Q9hdFm-|43sxytL=W`ox!Y`d#tZP&7Gh6r=;+)Y2nMuhLr z91)7Q?|auFQuw3SMj1^I(Q{d-a`-`Z~urBFF_;^k<-OV7l9ZvZ?>s(RSi!-jE zSYCnk5cxWOKA~GRM?K$kA&!9-8)c=(azcW{W1F;%yOE% zq88NNt#$Vkdd;&b(&@8WCgxp^ZMKqQ?{8;BppVpK$qNsSNYn_OeXAQi!LJ|V_&$yJ zE3>K>nhFV>p0SkE3#^!cxk}SSJsUhL0;^eI89TWf;wyfeBa1i z%>kyyXQe#wgMQ%WY<{2-bylXiEisg1@!=iOTS;WpFd1WgqY;Q8*Lt+{r;L78i&fgk z=7wIOlMvZNrZ{z`UtoN+m)9~Rlnt!=BBDF!Sztz@Ua3uV)%q+Xh`oz=6N2T$)?d|& zkI634Z;$8&&HS0fk}5fQZ1U4L2@B%l4kZ~?h^>VK8p^Y~o!pPS06AMQ~_RTA^C51nGJjd(4{==MuSEUA) zx!I+R3@QI|^)d1nN&OtGe=T?l8*RBXPI<*nq_mPHUKUk z3u3-`&!a^%2_gW`&kJbx<>sd*rb!uCt(KVIKBWUj#t+vpTIBnKagI`?t?VmNLuz%{ zXb@)Z>p28Ne+69Gu362?q|#;Q18t|2O2YH8}eR7VrN=;A$9vkQs;CHA{NWD0ID} zUR{2b!pR+bcX>noUuX`*F-)%oywqx2W>4EZ;CAF|W<4QZidjy8^kx60W_v+Rt_WEm zo1Xm5lyAu!|9;dXi^3QaJ`hVlGwa~1s1Vfn#-^gm4pxK)m?|8T)XyEfgP5yU7q7Y1 zuUiCjNcrZw;dMfBHgzyU^VMvZih5X}9Hm9J8Je}@WqL8?>q3dOSTkF6ZQ4V$hErmn z_qWb4Lt{Mm^mJFS(l!4R#DSIpX6|)DF5_vb!op2L3&Q64Zaz?nZ7LIF!}kdea!hy5 zoe|~*-(9e)xO7FvC-+fx#R$w+v`XxaAn&mTV=y>GS#3@X8Slv$Hv|nd^NgOSXy|K#8M_^% z^e)rmyQejjj|mC$KwFWk+!H{Q7Knm+cXjV7`vYLDFXmfI*jI7{B@D3+nrJgC@xZ8r z>zN!|-Z_yx+h2+VQkiBLn0HAQ*z>M;*En~s8P2-D-$@dCZU1^@6+{96Hcjs?=7Ses zPa+j`YHePDh5qy$Zi!UEY$FqH_Z}?9VK*C&9EIGf`0wWmWF7WEBfJ}r60&u|!+HAbALIPFJo3e%T1$ z(-oltkc^I!0URn@Xxmc{xX#8y{_s)rm73%UR6NkgKlcA!0`RbAs`rWfmja@XQn8?~hG1Tva~%`6M@Ik7#S4mM7*9h8SpQb@S%~Q^GPg z`f>*GD3z-RaOM@wRxwRWyjj(phj_-MB>B=I(->Kp`?^D`l{uO+T zpQj(T%8hI|C}Sf^4GXus?4S2}Z|8^_WAmB=@yQ=lm+Q;M#Q|reP*;KV!V05iieG9pE`&&?N)1F*@>*dzL3iO_2?$;wBm2-$hUuN4 z{yZd_%0i_-@QqN+m2Q+hnWePffveUGqa`=Q|S3hMlb zM7iyYM~rptC9GZXO~v~KOg`~{ee_{5=phHS#7Zi1u(dY$NAZX_`iI@;gD@D7MdQHN zYyqTE4~52Nqrq5nAN=Y&5S`1}P1j}QzuS&QqTjyL1m{e1ks6(W)hZ3o+2>BWhwx$2 zT0Na;f1{9I2w=KqjXZCd{j!6Spgs+u+e{h5CWQNS_z0e{CO?bWvAkt#Nio4uLBkHB z?OXoE%ibFbxhr;Jauddz=v^ACPkIKHEpl$0GPP_oj*jpcHD=mPiB1OcIx)B0gU>rQ zE!dXDn5qQZC9>UBi9DQ9ki!LAKLweYEX>cKCbj?#0sr~)wQzORZ&izByTYpiHbBL6 z9*%|pFPl@Aq~Y$pM7XZER2y4r3fa7^F2a8vR_6R3o$=jxfZ*UsQR*RTyLS9YH8l-F zhDPs-Kxo;ni}pvMr-z-j@~)l3lVxjf2ucLA%VpQ}4LE*gG;j+W%(uoodcyqfmi?Sg zf3|lLQjIqUZ=Xdpb0dL2F{R^3I5ntkM|YZvj>)f!)3pmFcY49#`&pHI7z(sC<1Ryvw8zlepn0^~ z9crDOA@g?C^7WNoXQ8C<58xC(IIyD}I>u&+9f{pVu0 z(z0C7js~bSV~?MPdG5qoSLLvg^z_^=>wejx!IO+BTSW%cOSdb)SPVs{dF6(kF+U-M zsETwvwIoSqZ>7m+qXpKoTm2`CIs3pblX#%DFs(ebD&BnD#)0!Na*k$#;zGliu9Dqp z0m6KJ5$1nf>W0DS@Il4`T7Ox%-rI3__WLHKC4=)|np-d5vCf!r+5Uk%_w6+F)Z6vR zh{vvF06Hq(|bjCPX>Q~VXwE?=&FhO`dX*KXDcX*4K|2~ zq7dA10@m*$I_K;LW!{^MZ%VgEGA^U z+!?EFqJ~fL^<&|s%Rqz)yj;xR-iX37?1?Rs#n_4tXScELE_yuPE%{Qc4RCj7QS|3I zop2>-Fc3F>!&fE<;Ocp0jN0bJ;QR8ex^%l%fwzeLQlkse%##sJ9rB<3yI{@`N4CfE z*T_r+oS^ifM_QpJw5E^hELSNqXW=y9}BwwPUPQ@J+*{whg)R&Eqqf6fGPE2}p8*nl>0ycka$^Ryd1wKgv*B>w`{|^F0SD zn`ZK2PGXDiH=+rEudF@1nN|Mnyj=2O*eJ00(`hs;% zI98GZfQ=69R#!lVYs={0K%abWba4J`>!XKNO)FU3M1!}u>}`_eZ~YK`a-ciPD)C@O z8JGvBWGW%j`Du%SrX_Qca9m`PJb_$jKQzH#lTi0zY9BkZI_bw{cjkruC0i{kJlO_l zft2a(p1xb#|K(E7<P5Ta4p9paJ#)>0!HbxgLgerGhmAky0J0|x zjAYFs5^bv?`ESyyIEO4{4qmbGjU4T3j42+vl_#*X+n9ksEa_>xIC529Sh4rNn^VNynJIBCZPD;dWH>Ftq)9Z3&^K&6oySPw@jc6*wDCDll*rjv9<*$Ef zWeM?u{4hl~d}fk(gFQU!(Wr5%6S4bM;*#KalG+Z$_}mcGCjbZJ3bmGzg}mo+$Z(?Tcf`F-3jh6~+Zf(=2`#|G%I)e1-_zJpoc*o+`P=s*9#2~W zbPUmq3-C!DV{{bvpf$Cx)Izjr3Ya7JvrLmDmEFR@ z@PM;i9vm+zlSTH|M&AaZyi!7PDyk%{bhax&1Wn#{UN9vnR^&5vgd3&bE#!cRwH9BnMA+1zQfAFLDn`ImUiTFyEf4E5{w4zu$rJ=0(|mi>i$* zu_f!6ht|8qHxt6D^R+`Fh2tST5`zZHOHzG~SZ-m9=vZRenBf@eY0H+6~n zg7tQq>24j6amP?Z?f$cwmD>ww9dlvt9i`tLBT*P!3HA@MlF%I$ zQNSy6NGaF`&%yWe;rtCl2!j@&K^dLc_8sadB(YNrdE{o-5jptnORRDC@{d6w7maRD z9L)RFY<5$?P2#BLz9HheWn)9@9ss?{piUbqBD9m?xK#33Fup{5Z(0jVJ5fAW7?=Ms z1aVhXEsx7p=s+|~>BVx2S1i>+?eYDP#GK(C(&5402hI>T5i9-$x@iS|x?Kt-Ke_c8 z%rE8YmoMw(jk4RFG82OeaewC5&1$dC-Rtb#X&XEwIM65DQmZ!i2JItf9A6yu{_WrX zx?<4~ET|a%Mx*L;y2%@qiHw8)ZCA`#M(_@aj`25&ePWQQH+r=BccOf2Gl;(-LhkJ&O` z@e4roxg9~x9^aysdE9{}Vt`^o+k*%CFf>^x%UATWei=CSS;WUU?G)#!Ud%yZ%pC+d z6VRB&8Z|p3n(#B8cOS3n9Fqdw;Pndw%2Mg(fC1a~%Wghm(-~lSAE2q)P*->U+KkTc zU6+fXX?bK(70Nu)(J=?Fo^3b0L4xBSpa#o(j>o}AAR*F7YuOJfvkw=N?X~A)z%%L* zK8R0<6Pxe|LDXViwY2(*?St{JXd4#FR5e-8eXu8p*cXL(fIz!dMYW};H2`uVo&Pxm zTimgEB1=9#3w2#?#|yvuLht;5wNS01bd2{Qo}r!-uEs0s8$z)lOM?&}VI+_9{l$U+|AuqA5*8S5RHuz|s>*=u~~bbC0k;B1hzBf5A?^Sr7WX zi%tW7j-Z_n{J;j=28kyifs1+Jk~oJKZ`5|$f)|kZzMJkJ8 zR8l@-0(cbjen_ZS$i_SYIh1R3D_?azxIyM4Q*@E&I3z1`GrbIzb;<%p`_*+JOIpU5 zPU_Q((H5ha2!T>cVFq;HEw;b0^en!CYI8FwGq>)~4m9Y$ljoTXK0F<@SFtEy-la@) zkUsGun9{1WK^_0Ih7Q5{KWc8i{>m?u3Uoo==0;?0%&gEZh0V9Y71KgeN1u~M-x_~` z<6811;RhLHx=a4L@xr94JWmSKum?-pmpX)6ccT!ubW1=)QWF_Z+el|3HK3WDP%uxj_@vQ}DIVv0p|sf|MDE~=Jl5ZZ z7@5|@FC3^|>pTZL(wQx?O}NR>+r0}#`0*rAI*9kP$vZ>aI}Ed8H^^41I9k#&Mc$le z!~@3QOd_uVCYiE;)qvt~^rGTmZwCuKNyPPDSrGhW$|~nrfNK5#;9##*{N)iyc<@i! z!_P=gzJ%gD0~Zon*z?UrmU+?jyv96i?)(B;G{?KXdc5qTdNe&BDFqvN_-|`fp`aZI z7vfffmtyt!sq+})QTfUX01~0>Jt&B06;2@b~rqQVKl!NKZy*p#LTpcJON9 zYBq!sVHemM-RZ%oUOGaRO>im3;aDhBY?42?C20h|`1DS;rY6}S8TElyr^vQ+@{95B z0t}g;oi@UKN=G&xz+l#(DzI`+>fX%Q5idLC_%`89ly_ZCH0ziH$n3a^-*NH2KX$*-<(igP*HFK`wg>dbQ@K8OXL3Mum1^WKpFF) znV&mFz3fcveJn^BFm%&L!6T^^3!`?)N8>;*<}CeaN4YoRI7D8${DFR(f^XRKfu%Rz z>P_i3Sn65sekUFV`2|grh2!)NES^^BT(JlpO5o|9XY5qFTM(e-Xyv=dJRK@)TvMub z5@_bs_Wz9XUWrrEVpv9_#OiKwi3aAodbYYkzax#*g>S-~v!$LPgA&)%*0AsiRKlT1 z%GnF5nyzAO%X5H6wSSarE;N~vF*NnU{$Vh#jiTjUu+K#6y`|K0b(vs=5`#-F5MWpl zE%8fw+Z=JTB>)LQ6DpwcD=cz_8^TI z>kD~&i%rzQ{0C}cT8l|?OiV?v$p(#TGz?$aQ5dwG4RhUWqu%arYi44>0Dg!Kox6(s zO>)JXtVGK#nX_xp1ETk)Rd42{4Ft`?3G>+@amgwSiO1ryl zE&CJ4S{A)@MC&Dp7IBbCtM_aBV^)>rZ_tV?*M|mcrEL-X5}ZdbcNwdSS&84E4kdxU zRI6?tP*zdgv({fYyn65Az-&}q%lDa*r&lMJW4nPw_K3&QvKuuj|7E&PzQ2d{$@rzd zB^IsiIsPD*=K2+Gv}%Wz?%{90S71OKR&UbK8@{F*LxUdOI&1=0EHDSHmTOM8*&Mgm zyL@rJ?KGP`W#lL!o^^5JLRdG9q&R(88AgNHjCewDlZAMXi8!{?CpQi{1f&ug2Ul#Z zR=GKi9~n*rQ`2kp>Sbf$vC`qGQw|Mp-!diYN4f;4a)lVI1ciU_g_7VLr3lw!*F@md zqOd&tM53`gQ3XPzDxNI6nEsrg+Sbu9`P43G+1&O}+6xHHSC`t*uhI&B0TS#^ayLBG zdq&;b^}c!XuJ>Pu<{{hsL{&tHK+W%xKFm!uk+^O>#|FZRQ z(1BpXRQp{vRK`~A&mQao0woy{p;sc)`~B6!v80C))8SsMvU!C-24yHYL0GO0n8sNm zc8E(MrotbD4ruCvWe3EFNo!O}-rlei7Vf&0=|tVxuc^z&zH}d-bS&$8FPvQ&27NRc z5AN2>s8_6NXfyi;G2aZmX)AvFVPv7Qkea4h=D&Sm zSob)(9%QtW?n64F;vXx^zCtyHlr^s5@)S(TTG}__EH4n(5@rtUQ9S8t@hi|2`_O5^ zj?_+0ZC}UpNbjr(UwY^b2MnyQ3#0H0rLK)~T0oLt(?re@STHTD#Pw94J>@ZXWz%(Z z*;xd;1tY70IfT|gW_DYLu?Eb|6(Nm&`4XUV9J~B>^x>#0Ea6EjtKVf-T0lbOq|gEd z>d3$sB&r$;Ms!89%aOQxFpisw zw@hzR{7%7L*UZZSI#C`nEv5s~6S)>r|MpyV)^xV@33-WKe*^|qFGIJW`f%)P5;%$04gbCbHz+f~3ktAy7aJvoz zLSnGEg&3+}z>&X7wtY*(<*(SXpJu;z0*{_9zSRTV#o#-P`p{N5ptppkV5jbI_$fYA zh{=`5V%p5sk2nF~To*#?-uHF$&f`X8J1C&u#okrFUi?3XQ5aRp2KU`R7GB-_hVgY;1pXdH5pZ{< zZ+QSRKOBPBO(0}AS9Q`JsIk25EUhn3e;gx+MH>T=3dY^**%*qd+^POXYZ`$eoR@?# zCz2!CeXB^k6jO2LQ*=!a8D(g29)?0guxTsT5t1Nf;Jf@Ilp5d{c(Z)?Y$JJHR;-5{ zPb;>g1+7Tjo>(ZV{{>Eq-lSgA9PK&}NMmj#{hv0SFy7@{w3!o6Gkjl0`OJrsO=w5O znRjk<(cCSED9SIwZw2aM4Hm?XFUOAMr%t~QzJU_)4w{vYaj{KFjmEqw7^HqQGBFtM z_-=`nvH^9+QC&tjf=-zn-~VC3dYLcgO|OwMKuKl5k-|Yw)?_3f*X1Rr*gqV)CB`Ub zfrw9o>?|w{ zk&>#hsA(P_17a_DV7qbW?JqJKwt0zVG%9N!^dDm}8~XZ*w1gV8KF6Xb1FT2U3GWqo z{~2r0OzQjwHt_W@t%fwl?qaH7F#;hTaEiuzBk&gV0r<%DLM9+Ae?8!Z&$|qDcWfvK zS0*BbhkBzn=H{w-e-Y~B`J}%I`Hf)E?1SP8HH-$QosSKOiURmaQ-cjZj+D5_pL@g( z_D4(W1WXiy79sgdB;sl4;`3**qSn@N^bZ`kBQ~z8*2rQ` z6Di6IV1`r|mi9@bNsFj(8q6ZE*z&c` z?sMh}N1CXeFJS_4G#}5&g_Vo8W4WqpYc!NN5dw@SO1fU=fa0Nr4R5O)jzAslw6RUv zY~T1{_D9;?_P+t;tpVGHq@Ok%L`Ct7umDX)u*64Tv=ZGP zW2wYu8OV$(1693Z73-?d+3XiG1rrO4=8J~nm|faGgNEmKmGwviwEQ2Vvvzh*;*zB7 z((65N$UUJEW|rL7Hqx@DBPxk#$M?%=HRmY?y-|p_8wm5polF08KAu=7$v^W7nuqXF zziWBCud))CRAMJc~HQFs0i2Tc@DwZ_&P>#Y!fh`ox5VPG`20 zWNCcNHv+aE+e1o*8&44?Mfd;)Ua;;!;HF%r8wph21 zludTbc~n#gR8Jm~Fw3Nn!nh>Kcrbm8uDGAq@J}3p5Ge{E*Rarv`q}O7XitB|>rsuL zK!$y>)#9Tum@a2D0}2>@rD!MP-XV?}YKtODx`lzd*!Rk-Kk8z{;rOBNL~NM8G;{`-^Q8Nj?UnN z4RYWX94(TfqP1V>+S+6M_29a2Ru(bDC5hOLhg=T{k3RvyMas&n`w~rlORY-$-#Wt2 z|AqxYSol_^b5+64yXx+};}kFBvLqyrh64x*#792LIGY|25hv&1X;3UHIF;_YZ^>ZI z*#hv=TK;`0QN=9;LpPY?oL6sid11*x780D(?!1c()jA`v; zRCG7ELs6b9p-whKRQ>O4c5Nz}x{wmA8N~v?5gH4l`He{O2+tQre_qASxEHfalVNO5 z1lKSHK=Gqq(SKXlhVfR`M2V!G5ALQJ&3whP0b_y5)4EnzfCB!z#_;>qZ8x-0GS9RH zfhC^}gQB~E&N5u$JuYK4E7?ggCQT!fp}g9Qve-aN-rA)^fHALmX>$lEsWmuexef*he7Es;_(j3Lk3SdAe!7}$2vm!EwRu zsx6h(92HwvQ1e77p4ZQjYXcv?B1|j~74Pkysg}^xo$bwImNdUMDqeKS=aj>;NF~b- z%H4rHbF5c>{mOraxom~VBP?nkC-ADJ`e9gppNC*2Dkv*UMw5KLs$HtOb6h<>dscAn z1gm#If@!CrPQEqrNk$pPHIP9Khedry(BTFigh*pcpE=C^j5w5CaY{;R>w`n=NJ6Z% zFRdcpE`FqB2{LB+xWIxb70mE`-<>9DrW>DJi(xVx$a38F9ETi}fM8su`+p)3OEfcs zSJuEuR@3ou+GRkG5_B}XsV=T>0tZC#lFbi&mB6Cqe30kma(R6D3nzGyWMX~1QlS%o zFkYIiA-7Q{S|@zEO*iz`)o~aOAI5OCuBA5q^NH;@(PX-kjN7D z-bZ zUY!fsDv~{I1PW~u{ABcA0%2wxFgLR~bQJXx#%X6awC7PbkEtq#!*t8958L};0H0tc z3=%S6eX86nE12c9<(gF5b{$~b<}win#15xxfRl%lo9+~)))Dt4%tIOj{)}u*Aw-FC zR+MD=Uci{0!L{MXCn!3$Gh_q^Mq5-eS+yv&SUsmU4Av(muJ-z;Ej0c8+Coy7A zyt<~PPdvy0+e9+151*!`!hB(N-+r+<8*!{T(FR+gZ0W7u7T>m+tZaF?R3kYk@4MVT z!zhy9w+}-&mA~2_({jU(4DZ9}<+EoB6zAYR^)r&6IJVyZNP_pWxKa+b)Md|2WV?P; z=EImL%F@p7D?69R3rFV{=O#Q}0@ii_vdaIiSj~%*&fPE1bjbsOvb6WMcs$Bd0PwJFLrE)L(nelzlFkMnbDmd3x{#fnTUQ)&o>9W)8 zAkyre`eg3a)876f*reLrr^Ai*c%Xexv^--ThW+)K{0Yv6cTjJpvXVzJ$eeh7Lspkg z59Z3k>Q;+l*NqSOwK`r;A)g>c<8vLjOat+2xU{IlUc5(_WaW68p*0map#G!IGOSte z4aXysV;M-R*??}`lnJrL#IvPl;uXVsYxLt!SBeQW?YY`2(3b^~6abSIW@suQMjF1u zm8!GL$r$5VAA)^V(@4e^9?#gc`NR*|8U}EvYyiD7;TE7$DZeIc2Iu7wrM&8YB&~Xm zIhBXL3=>iCTI0bGKk`MRz&8x1%1iA`-<0GBVB6ZA4?MYvA&5sK@E40t>PadGDJxB9p z8DYPdS&yWMI{SQ@mi#i-F~Al`y)+#HQ%s~=G%~(m&h&?Va=4B$mIl4@wYTL;?f(WX z*dE5n`_2ZN-F*Hm8O<%mHXCgj^ApDdQOR-zH9Y1b9Le(25%3-;@r^qAx*hX{r);FM zwd48P3QtT#{7;Mr+UzyLa<71B&!s2f<-;;rUee-=hjG20%n zLfN9pb?dij&w%>5lNx8kde0K--D!%S~JOxtHt~SioC=-aW_^iyd8Ac z^5}bNNIR@dV%H_rB#BWt{r=A%-p3sN@Nplllt_krdp5#eO*O+&r2*%XK|fM={Nc;_ zKMiKY#lof;iQWOyZtmla4-BO~kbO>Z=KyO|6SMt`kFX0C zMB1ve$1;r}BAVdLJtwPV288?9-b$B2ZG_SmXteG)Mxu^w>YL^Lc`=X+m_Ph; z_+j^`5tTeB)&1dN+1vNcZF!B`Q}OwS^Qn?d19rZ(=#qFuHBnM87UsQM?4N4JTQNQG!Pn zWe(+yKC%0<*DmZ4y*;Ej@%NgGWLT6F#g1Z!?_tjN-M1-pk$riyzV0k3LZCj?P%-B$ z;$|mZNHH2yGVBx&Lk9J(g}}-5$YRQ>gy8XX@%9>7b`{R4o$Yox&D;ozYPVTz#|uND zz2)~QN8M23`emVM0WZYCrNWyZVgrUx1hn9{=6gg#We|YMZVSCESpNM)+S2|~Rwluw zmSAtw0eGI&z%&{BFSwl!B7bii;y6I`!mph$g#yc;kJwe65Df71VngBRy2`@;`2qBL%sMvEx=B7A=w z5M)^rrojUl6gn}iOrUXG=`>i$xWJ)Af#0AX_7U_;&dz}f0DpaQ&Pnb#AxRB;QmBvW z>b4^k2a;jg69{`GwMG-MK|xi@+mr}Ngw`F*g1oP~I z&7z@MMpC|0viSo*%<|%7e}Rg&A}-k3h>tkpH&H#iN&Y4jJuZX2 zxz!|^lG3pDU8E;_c@4{`|3h!p%R@5L6Q_45hh^b8{=|5SB2Ia$1Pt8)&d?6B5UI`= zq$3vX!fS#=GnS1P8;qH_M!p~^g4|;t{q%Uj z+Rqzz?#xjQFsBVk^p4um49kfDi|p*f3v{~AaE#}-?CzGVx?+pw-!Tbj8x&u*%GJ{0RNkW_4X3$zmaNKglXBSv>8j+4Y%fqihZqf>SN?$2sb9mJKv6@U7@3a68qNy zdf15dm@>eVX#`#;{Emkn$^+T+Un#d8-#txQ!iI&p)_|&vv&dXig0$7-ZF=Unwd^Hl zIQ~dlKh?qfa89Xqjm}1k*FTr7NGaMU^49G$2FmB0J;Dh09sKG<`RPA+Aw=zs{(#|- z5NSpR!0tqnSQ7eW?3YJNbmB3y=5T;`2W5O=AT2zyv!iktj5+xAk;Y3K7#zmm4<3Vt zB%b(H;E@Gk8QU1{7oyC67GjD99rIO1aq;;)TIocGd`FkOQ3Iihc&7r!nyK^}vq09z zf)SZ>r0P!Kf%DD@R%S=hGPZjc6s^hUoswXvfByPkpZ;)jGTPAKNw!<9hkmMZX?dvT zGpJXDN3hPDRyJoq=s*NHJ_tuanIwY+gHRtbcvMJT2XSEIo1L{6jI}+5Qnf~23rp1J8n37yZAiR@1*3@7MhP;$2XVchfl;bqDx>~y|I2& zR0Yu$G-b-F|JFTu~d!B*KkLQQgEgn!n~8NxyM=d9pzKUcDXQ$D}h=cI{|4x z~+?8}U069#CpuONSm91HcmN0!4{3Hj7XobmDOtV5Jb|UHfkw2IYpUg!dOM zbdYhFmY>C&Za4tEImml79S;yVR_sf*1nz=x!hh5k2A0=SyM-^)#2Jd*NIioh=l`9A z^|dRTkaNEPq)NhJM~sFN9guI%1~=*@RMWKC{*TjY3HGy+d&w5=me~vvgv3gXppAzp z#|&te2x6Jq5+~C z*u$CNqKZXxBBSv*dCuQLZ_*V-!yol9qzF!D@3>I~VS=a3 ztYx?=MnBSj8<>fkG@0mZ$Zk!$iJlQwRgHi3?V(BuCXEE}$V01LgKazwd)z%xyjjNj zdAS)Nm5Olrv6RwZbGSYum?y*18OM=umXe;TuRo$4j#p5IMq_4`o8QE##WQ1rb=_#I zu0zdX9^=2#_`TfHJGwnEE(_>)sk3TopamPy83%3S&(puy6>o{Ohf6cU^VSCru4W#@7>Bbjj`yjKJf~OyE#Z3E$>72h!vC?CHXhzf z;gx-6THl?$JZ6Z$Ln7G<1E5#S{=2CElJSg$nO9R;d#*UD@1r2PF3U%QEyv}qHD%$s zBT?{h8m0wzQXxWD#OK3fUm4Ynjvn~`=B?!YcH*BAYG;psw)6C+GT94-`u@&)d&JcB z%-3+0Cbg3UjQ$2Y==o&;IcuNs4Q^}D?s;tF1In&HmJpT-iY}67*o|up3p^q`J+suG z_s~2BkU#O0fIZnt{q)g0wRC0qLCBVwq1jPI-O8rH13is?!W#LLBWR zF+DuX3M;AU&~z}AL<_bBF_*SoRN%GZE4s^~RV`TvBaSQ?)76~zNLz4%$@pbOJIXDJ zQ)ko(#!{qatTUDGvlXzgizp}F{zE$fp(-D%lrSs*1ZYvM1fDC&e=!Z`9hi?aHg1vp z+MG8|9=!j}0fTLRoHnds^)Aih{o0nb5BN}OG7kgw+o!-UpUxx@U4cFuh1jDuq^vDL zJa)`s6WEBsYwJ9BEp8DhAWAw_;Enwu({rJzBlfN(ud=VKK=C52ONP*#bB6HXoI#ER z=7bOsqY29ed7^^hL$&1O{neYM^!e{>qs;DGe7PW@byf2P_y&I{kl~lVvSW`#^0Rg< z)MjYKOEHqqbtPR?e-WqTYt9rutKZ&r6%7a*DW^CD_up#R*6)tziA4ZT6E|Yb88sQO zv`5=$&Nplc7fE@oy6@3KiQCtt#yp}kqzvakvxCr2Ap-r5{Dj84N<*KWj$nNU$OO$BJ9pJs00_c zUx)|y#v^M4&LVTpbW(1enzc@0wb0Yy5JcB~^2)#ylrc@J#v}fob4VCg0IKnGA^I;h zDnTsAL2;ln_+fVUQ5(82V3E5M!PHcch8{)t3Bkb9WPRlydEEx%IJ|sE=zziJ#I{Z? zY8@ftnv{C^DPn?&H$b!_6j~SccY+U#Fp)~F3{bNS-)q0|1xR3fNXR#B#wK^93R-hv zp^F);itV4z^UrqQOmffAPDGN1h1l&Kv5E|5Jnmnt_wZ~f34VignRWK*#Ziuf{blj; z&tb9m$PEhO=L#))&@s*AM-dju(@byepiYasR&26d$o%j3GWIUh;IZ%6c3wriyy7`V z+ydAjD*KIv$b%fcXhrg1Ex)mIfP(gS<_yc9jqf|?k5l%%I1JPkX66}rw-rX-B~*{> zb5%~!+If#Wb2h1eN(e2%z`c?RP%y!jLlRAVX&v6tzC6dycD&xf>6`5ayfuiHuQ$TJ z6^t#Ydd`=QuP@Ka3nj5E7>@blnFFBzOtw~Ir}2!x7d8En?Ha}^4fJ89d56S=qVg_l zxwkvW89;rM`8wv*EF99`i>h*%2o*{9rW3$BFeGlOzm!U{+2f$R7nW0qP)}rC%MurY z@AVd)gKI}-be7G}F>4`esK)`T{l8mg8fGv{j;m=nmU!?ynRHRi=R?F&`Zzayp!69n z%LW?q;7A!U5tvJ(65?3R;@M>*2;839Q2hoN)~`cr4Z%YZW5sHt>j=u}+*_2>QVgfe zU%2vmTVucc<1r&{s2I6=Z3_gP=J-T>Z&Ho=7VMzluZ|v@ROIkVeRABvZ)_)}tk8cZ z_}Ek?B9h^iE*7h;Np>qR*i<~~8LP78NoNAMUw_sd4e zwi?0a1n40E5a#-ZJ(1RX$ z=8xS-mEkGcHH(N9QU6NAT8HcfErI1^knwEZfxfB`{7 z(RQ7svhn!OEG<@q53x>NRpR&&c{*x#wsd)C1C&Nl`sJFM1F-OeYzkst)U`4W5=nv8 zkf!e=aSyw=qn7Z!WyooJO^2mL1b(tmR7{Aa__L){dT6xOd(^m*2{C(k``b9ij;9AA zfmRg|;OT~ElVjilJzuoA^$zAKevtw!e5YA@G6Umg=V4RIxW0*I<>2-{fDGuaT)Q@) zYf2Y%<9p~M=Npi|a-tu-hdvsM0XxU64idqmPpy++95zc1EGAUS2~HxLu2G zwI?vzJ2IE;XYxlWsSE0jL2XuBA`xIrnjDZPvDEPFt>jVQybO#tj<7 z0bT!Cj@cBtMbx29s^udM4L6rOPa6`&<2<`NwuwsG?sP~-c!(pS2bX*mT-@yC=K})0 z=<31->k#@GrJ*3F5G}^B#jS`fU{Npf1I9x0JtFz9H0oRr-oG-N-YglKk>-&?P|BNy zjDctZ_rD)^VdXRpQ#RynEWoJM9agiBj?N*g)(ZByiw(ErTJ`LK`jf>Cg)t=}QN8N2XX;UmN~ zd+_Z+EYwuXeWbc;W+hIz+jrC;8XR4a z|1~N>zpq>zLUCwQ;Y?&mK@Y5zEAl*1bf3cnvdw+3v?pp_fCrRJ=G|R}H_6Hkd?g+* zr}ykiz?kC{8=b5+=S@hk!ybA*YK$FMz!3wC$#Lbu4yx!ZLHdEA6Wua{M?`CJr(|=Q zP1YO}j5*b;1ZP3@0YUGtdtVTS=okr7UlvSJb6q>@8%=)wDQ*Drgkw1mm-QSG?ShTx zSy=Zz<(h;7{Ic@xOX_b{KE`f-Wu3L}K;6@KfoPu3vOo4$dRGk=Np^h)P}n#g`|cdZ z8$_7jlbjyG@7&q(yFUQRtZ#;CXH)a!(XU_m#7H5^_zTtXXnPd&FWqBUVvP#L7#feZ z5YPsfJ)o#$w|v29|Jo|g36i)cmGTR`@-^eyx`a0;J*sLcE>+zTKPgo1oX!Napn(hW ze9z#c8Hxpk!?(lkh3%-GH!C#?=Qm$$#1SN5SwMy78n>2ikbnOcn}B5IOtnJsH+2$^ zR`8OIUI&c*Or)z-TWUUNWK+U}T)y0?3�v^t(?zjCLH= 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%0DAELIe`pc`<%q9{8fgX-60u&9$W2nK= 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%0ATdR>doKH z{s+geT>iC|3atA{ zg~I^4gyJJjoI|>5a@gDZ1pz$2aPY52&KEU<1CJe>-R#|%JvuvhX)yAGEnV&`UFk?X zw3htTyw+YI*Am!6U)@dd%1p59#2{0`m0;LrQ<@93AxJE|!r@Rb2DKndj+hU?f^}62 zbwowwn0!goK%cJS7R+3Pl~O72T5^?ecctL4&jyjgShlN|>ES3U1>VS-o3~lcQNV67 zsLnwc*bm#I;1zUs)fIY8;s(LRsZ=^w|5bC;IbfREaRru1eyX`SVA%aJ78Vx%@DILaZpV%tGhz6pmtX4Z?_XJ|I_9zeUT44? zAg&@^*60d~%Lw^e_R(HUJ-xE`?cN){rL29qbZ{}XmF$}Aj{e{Y3mzs7M7K}6yMOS+ADCVt;2Lx7?uTGT+oAGC-mxP1jF4tKs*8rYR&i^-lq7M4H?M5ikSEw+oxp7K~$Fie|%HFlo-2uWlF$BRFl*Fs>hoCvA*v~|-0~l1> zA9( zb@B?f$%=GH#VcX2xh&{4FLOohqnpil;&&E#8`9jJ6`{PH5ls&GwojBf#Xk5PJop?I z^K|JQIQvt&@K(c1@%|WAlK72kpJh(XEB_;$J2E@JZz6<1v9ph^4!OL|=^+ zWDzN0XRpPVDG*B#ia`)*Tzn#5;x?F{B?Xlg?;v06u+NJZyv0H=;42w0ZQBH7KL!K9 z>?=hEm9hu9J_Me{_X(`ejMOS_M8?{=ld+l%u%i=pDVDHnuiWs(2iKd6#vS=BHbowS zj>3i^!coXYBLrQ;iHaxjk=hF&n+$iG7$*m)QHdgY*ya;{D z+0NN7&L58UY_yaTpISWd`oI{lS=5t@ii_!IZ|r;Ra4cKL1`rR1A6-d3v!JyVg(c>% z$s%X2gylnD>rZwj*p650=!Br9WVHP-$%%v2(WFXADmBs;G{h>3H=#ck%~2D5ZAHPD zG5X=n*(<4)O1^?*pDMNg$ts1Va6k?1KMHDcVdx{=bpvm#_KMHAI2WlsQQ%)&*u6&5 zkkV=5b|V{Lg`n?1{qyZcR;wD>O;}&t(H=~ki?-J`suJa$gbI>P`1lv$@h@U-i_X6Z zXI`Nzr^@BxwJlJNK7;?@zo(lQLAp&eQqH@33|O!kbK7hxTD6;#^%}q$Rw4!EkpeWy zVKYHX`O3OstZbIQNO~(P0xg2bfw2Xm03r(_Lq--#E9Hsh=8;8{h2Q>>kGHI>$8#OU zh{gCuH+rU9*2deH0X)kDxg;s2iZh?SKK$Aty%4MG=Te5@_j__@x=k2vdS+4YC^*?b zTk$FshFt)*{R-{4G%>kK;i5P&P7KTmk%KXy(u2w|IJS*!qym|;m)9rJOo8Yz)2#&X z(^93=m+cvR#ZBl3Z=Xansbx^U%xYOP=|l zVU!_xWixhdHg$*QhG-e^&NZ;2?EnUdtTH*06P0SgCtrjoU&PD?ee^n={9BkfU-eGR zQ`Fgyo&E6W=bi7{@(fL1Y2fiL2VkTqVp=(%9KfnG8x=Nib*YNposqGD)xM2F>ULv7 z=C03l&U|ihH1c!u?EHbnrgSEemy;&4%R*~@?la?qua9cQ`WLTRx#9fT?!@u+)UykE zPfoZ1?1y2)0bK(Xrt2|d@|+%sUuW9>obQsF4CeBVF@;ca6I>T=WmZ|S?`4nSBFuyy ziv^$dB!C~vT&0VUV&0WF&4-oFEhurpGyTtC)9g9Ch)Lw)UXkbNMfwj3B(TUnEnw_}! zR;Q?e$GdFwTMRftr6)Obt!h@?vE0@(-?=id7D__AubB*JBfZ}>{e^`?kso|trtRUS z!H@RLU|2TO9N9fv%b&X0^VWV8cdj_CaJAjsVtcnzPcOy}ZP*1?NK6CEjxB?y*&xsp z_fU|S*$?ccUWBPKB-70cP|QPP)OifrQuG25!zQC00dOtP z+IwkebI)ceZUiUkQ01lZw&msZ!Jd5deHo7~4KK8AEH$Mi>Ju^38icL>4fxbV&)att zgUwNzY)MbH=so%5Q;Ug*R!Jk_HUX3C(<9veB~40HqQh~8tnxvIxV$lm#h(@W>t4~6 z##|psCFWQ_st{E$s?4__&;}W3C>&GZ*(wtIj+83wQwD+kivdyvI`{)vep>11;=Ne% zgrYwsEMo(X%@Mf;T;%D98BA03-8)HtqxB**-{IVioq}BCZU>uD6=BtZ0axF$6@f^n ziN}wB5x@2;_{-nKLytGmvEHKvu+`S$b3>^G&4_B}H89{ZyDG*;E{Ffnq=lc4pS%;0 zagJsNB7AP@P*TJYl^ig!w$;qw#>#UO79!p0VJs&zKOEWk{o{pq`?P#abG;m5uA_tP z%@A{K8{)Qk1A5ovF~`WkyW(E=uEea3-BUJx2;r~5I)YTniM*Rug&7RiBRj>ItsePgZfsYDQ^6JxxBsEga-JBc*l>o_Fbv;{OQiwWocm}WcSE+nvFO0xq z4&W8S*^-m8{kJSXIo-6Gy@Qf<&Cyn1?J^fadt{=(n0myYYdc-T(Hb?bLSxi<^h*FX z9J*RU5!KFPTJ3?^eIE_SOSmJpUQ zB^8Dt%mSASW|3mw>v3G%t{({1WdK%Sw}5~Zh_r9_4w|AD>+l}ND#CfSmzNmkfpeLz z0N0U9_6M^xFrxROc0wNAbg$f<;p$=CA^FKV;mR)5z%tKkBz?e5_f7>?b>V?30k~&K z`zL>ZpZ`yxv#)`yx66RZSyJm(M~(UOFX*MHcFrAVs}AerJky`s@vg8+Z2uElsv;{q_EhKR8x+yC1jW z()w3N1&Pfq;aUz$Y>w;9hd7J`d;GX!$~H{*G2AtX*Htq|KlHWjR(=)+oP~VH0-(@P zCHPIu#X+r&_BxBC5Hu4Kv3Ke<8FQk{I{UM zS)AtG<8}c_k0wu}%I!|}cPn9th?fjdOVtq>^hWYLpk$EQA1q*1=|Zjpgi|0V3)4Ti z-drf%St;0Pn}B!NgH(?SEE16jabR6apzlTn79X1{)t`q}?^j*6V*Ky4|K-oMU-?~p z_7|b0vw^j@0$|NPb4A?{(ZzE0o#m_s&yBtvbBdd5-niJYaD2TkIf#MHj)xaUKJ1$!Ra%%FQfLt#4e~AdxfibWzIiaQ z+O$hPXtAEizCK`X@%`!K!>jQl>kt!!s>Y;Bd42lNJ}Mjwr=_5jy?woH%)P6`%O=YZ2alOvgqwqmj#XYBa=&`N4+x_y=7lN|sz zET*K{(iVx*-1B3-C-*hYw(qJ>yg1QnZuq0&#L@M{L#y$1H~MG%9VKp=TBVe~;Yw@M7vI`gSx`s%_!se%TzYC43y zsqC5WPq2jomkE{fleP`R$!o2&8@@hU>t!BeZuaqO8tmDNLx*8t6oy7|@NlK-o84Ig zO&crG>}Yp*MnznKRYM8%?VJZahR2_SWw0v3kn7T0zs{Nd?l3q-f6TsSenLe&jT~cPxOpX5d(D{u- zj#EQ9i;@e&MHI8oPV}4~=(y6m+dg+HX!#Gj%uQ=4m67=PT5NdBvpm3X+~ueo3oSXO zfDlz^3fGqurb>DnLE|6_se<-tJM*A1Z%!f?#*rPDKjMMO1?W>dE3U zXODA};wRxmZ`^VQ`(96mo4m4TyZgwza{7+PpKp&M3s{(-g$aOHoVSu{fuX}TOUA*` zGB78sij18D@TGU4XBdVbxXV%2YMEbc($ zo?bTMkqMB+^$1W<&&T>c7@7an4Y1UY%M+L8>eXwO7m$j<3#s(`-BV93@2$s4iyQi< zZ$9wKp$i2xAb?NyVK{6UXz9V(LR)Un2P1b_D(iM<(5+b6aOwRXt+kkZa4mj#BR-NL zBwuV_5pO2fjxdOoA;!94APs>IA5hH-ff)*gLc0gIJSL2a^rIr6p;B89UsZuk`vHgw zDg$dPl1D;58ZOaR<+Wi>yj0E`H z7|*^+mrvI0#(0kbFe&^ro_8dE*Am&NULq0zE`PQ&?d>u5?S}-Mumv?X&XbnFgici+|ECf8}*yex*$GC)K^YuXLvZj|AHRpVQY`?$rs# zd_PN5v@{jj4(D&s_kW-M?ho+LC(5>cCWT7wpfxMKTSL$_*hWPjqOW0fvcpJ#>c&_` z;;ZjF8m;%~==U+;V$7aT){@Zya4a(B*Y&}0p>M0$Trh-_1oki(z0jjX#Gm$$y)^ky zRLN}L7~Jf9Y;Nxd12_HbPLeTu+P6aGna<4Q3s(kC?`v9U+vQ&g%IWzFo#qA_#rJL{ z_NU_q(y@VTS?3Eq42`LbUtNp`uJH0I3OeyIc`0O1mOZ$@(z^0b9*71K)Bu4#UGl6E zDBxhsodo^8#pI7{lS*)dV$>Aexq2rB{E3O_h>;bbwrdJ(`N`8`t##b9*e zz1DqflFaL+9}iyr`qYCu)x|OJ$kM<@GB?(>z-M5N1?Z9}^NFZTf< z?EWFpuGoUt023mK-i=;_G+R>zR^(2}JLv$xG!d$->P$Nw}pAg9ny{n9PeUCAS8$5qz&AWWeRUEjM>u z=-+tQZ7H<)rB#RphSc9>f#{WF=Cy(AFU%gU(>Z)%W`6e2Ik#P@ux{yi zvMd;~|3POaDda3>dC1jC(ACD%rXIHXPkJ$#8C@-9r zi0jwc;BOF>Mrj1KqDs9pjP9~sshCanD$+ed$FbQ}pv)Dta}x~!u4XJu(BFNFe(;Al z`WQU^oQNEEv3YTdwOVCy4`*{77VqqxK@K z1#+UVz^X|jEOG`cjIt(4v<&$6%f;)A$HRflXgueAk>;Bs(+f*P3&19ZW#S#DM^>Mk zFi(K4St?W(`fBI|qJ!0@@AOS1L~L}Uryf7|reflAH;%qKdNHq+%*oR{3<3VGSGzh| z&cNoW-{{-OeExF(`Jwiyu6qxFf&4xj6i~1NR`#qci+J%ca}6T)Tnz4%d?>HJFnKju zz9KK;-wDZ&*=~t3Y+0-2$6Dp3!YMBXg|{FC_5I@7uEe&(eO(P|^Liuxywg!C(3w}t z3W%*;_=zu;L(>wgiuq{tAEHH_GUVL2`v9C-yUXXniD%p_t8zvO9U2TBsujolcuYV1 zcRMK%8xDq`^D(LDSI|o<$xL@X0{@jn93g6@ljO~g);)VRk1UyUmyT|ojb_bY78~ZR zoA!94|9V2`Bk8&Xe%o_RpPe}J)BTr9nvfWb0ggnjvZ1h!S;UO#$0yhJtqy!RoY+j> zLlc`8v|CVetMWQ3knA+P29XII1oiy;fR#prQhTgKv)x>>?al%2SHbB9Lw6kzXjtOT z5)kH~7>c2xkGajbjgP6jhsD&dgqs7lNbu^Ger2`^8Y3D?w@R=7x%i#`M*NHaj9&dV ztSv;>1RN>wlp&{TH@fI6uxiiq${Rhesi)eUd3iKUCJ_Z9JXuYU&y@;9SXi_AJ zSY4uMt!D<4E6uv=KQH|Xk@*C32Va5olfkP~t!s7qiT7-^J~wsPHsZAnjDgt|12bKRg3Ye<_0L{eJU%C4_e{^5#-*KrgB*C(RTz-DKwddTie5CTjXFf zJPwcpGrb2!!Cmt0vo*p1s8S{1mBoI62N6|9W>%u+oHyb5d3h@5>@ZRa|{NCzC2KBSQtFfwo<1b_uxk7vr~t*!C;QW9EM9s3!@9yUbwWj ze+BR!n07S4aLTx-;0P{ldJAQ zioOC1YUz-Uz6Yre0330weg`!K!T7U1VpdI%J!LJV(gnQ~!B@1kB(%DKioTWTIJsAM z8L)!H8uN-}l{)4;_uA0d<<9v!{jx{ax<5HJYGbg53I_XgrmCBvdA$%XO+P#}_Wae& zf%V-ze}P8iDJ&w_SSH3zQAq{^sO>$Z6mTJjMp*;JilMydH2Nqd^V|i0_pP*c^4kbP zxO4_FdJO@JObq5jW_6zcHHFY$SvBU+U)#Hb9jC2_HMw-}$WMOEN3;Muw+l3|;123g zMjj8igNqf~K^7xe0_`y1Y=X@vG|?JCC3K4spBt?g;3X&9TQBuGxXVbe;~A{7PkY`I zMDO-boavpa(;@9$>v?ASpssmG62xHQn}lI&%eLljJ~?syvnC90>|OxoKFpvpKVDme z0c~UtgHfqK_G>bO0^43GGAa-#lgGSAs4|W9JMva~EQJY-@OKf|X@F4s91rEm5y~@! z#9*5Q3(x+j0d4ojh}4U4SCl|QZ6WBWzpTH8`etn6ROw$;mWs`cz(YO&S!nU@%Bqn#GUKJBUgT=&fD!`BR~OWJkc>YgX3 z55(N#(b1ol#=vYZVb~5vX4{&+?Dl_Rnm4{P+H?f-o%=nIrPX1XCw|r4)jWY z1q_4UfH;R6xUTR>Nu%XswkbUDc@l2}#=f3z37e)?I_*ZRK#W`oj*@`) zt~cV(_sB43;f0&gC>quTIam(>PS?kau)4L#^@!h8gcVpj2H;8|=!DCF?fG-Ic_SA^ z6uP~Xyz%hOdI1SR*BkrIBS#m5B@)YIFfuhmIn3q7-ygWR6`l1H9|xEFo|!xpvt!Yk z6B|c&eE?zp1=0)0aP=6nExF0ZCa-z?;kw>*_lHF>F9lv_OABmLVI`LwGxu&pJ;1wNU>pgNUS*)z$*|opPG*s=Z?%T z9A0Rj@93K9YFTW9yG@B>vROzfNnt=6l7Ja4sJc-K{&ATLUaLhF5R|ni`9YL!jfvik z@WmVxE(>)k#xRss|DH4Px6<61*##O{4!7F{RnWV=0;~4maHSGx3xMO&gvOo#ueC4k9DqBE zWn1p~1B>m;ZLKSHiLS*~Q=O-VS3Wr@@r>MdRe_4!0-tHkNiVOJUfFy3p_Rc$7e=(G zZ4!poyFNR9?Ck?r@|q#+1{vGW7`L9(@(xBwPIU#Z6IiApjxD1|zsVtEKIb@ml-$tQ521PC}4@_C9_1RGWF zVG2>qP}~}zg4?2RU4Uvx0F||ZtPht3fu07IZG0pg`kT#b=O*sj=h_(2P0&lUu>_Y- zI^R|keftkMkUV$@2M?FY>y9CZrmw)?|6gT+kv$_(9{esS!rJ8(SUc$>uM~eS18^qc zBPy%O>|E344t1eB3r#kk9)INK@wbk|iggLFrEYY!b>=pXEGiO}M)^_*M^{+glgfnwMJ9(C*~y5>rg~-0~>rK*@u>pv>ep&9X>}l_RN09Lk2t zM>1L6HGd?rk}*;jIH9mg6qbb!WzS6BODbAdqTy((JxehxS9KDY1Je8B%_XBDKY8B` zqXaY8$PKHOKmC?Y96IFU@gWnQYk{2275dR1nOoUZ;E6Bc<6nrND6BS(u!t4GL@kP7 z+l;6yuqYFK33T}|4NWh^C|)9_nSZf!eun_uRcNxO-(s>j@#xJ%?;osF5Vh^>V5u#i z8Qjoqs|Isg)5`9J_6+R9V2_wtk?N(?)(;PT^vKfivBg1*lnQrbn_sx{;M=2DR#O=p zeK{3IV$zkUfQ}*n<=VH~RZZqJo=X<<`<9pYFYAQOt1V5-EzK(}snuq!R0l9NPvV{| z_GfW$3rEs11)mrg`s$>Da4X3rXUV|@5pC-xL_T~427$PpTBNgFz_9x{AyzfBLLC*! zCvDBEqg9tfa2w9hj(b?p>tuqNIE#I~QUCmL8Cz>~^*t~*`&+7MJDL+GE|9r>@AvT7 zvt{k;{?2?)GNiQFl9KlYRhg38Q-rz$ zLEtG6a1&BI2f1Dchv6X2W_8H4J(u;Z7*LhdVh0I<{RekLLg|W@#R!TVG&q6irOnv& zxl{w2K8_=jI%=at=ih+yZ&0cQAA6x}kbK|~f9>h50k|e;iIf4;%&Ue(~i)?~T*Xsw2pTCw* z6m{Dz*m)Zc1}k)mlrXQb9=4jyyn5)u!Sy{4&yJYD8P$R5LVN1Q;m-cq^Mg|ck)*;x z*xkuWMEaI6%;VC?#4yY&*`|EHB@f_fbLvGso{#JKSga7!49%p-Ws0wnEofEVNsK62 zqEv~R3`mvC&0LV%e{x#6EXrO!ZI;4G47eDDkF&Z`8(yn!7}EJzBHX6TGH?II!13<7S4rKU2#g@t%ac@gC$J*+tN^Ws-Zj0Xi z8*_uM0mmS@_jV0gYt)yBwhwqN>K>%%4WnxBK^lbZQm`g}0^UesO|4ft79U#~YBFk9 zpYF+*0ayab%||B_Clk%cChgRzM4Soup@#Fl?ibAcsA3LX0l&Lf4GIh#950 zfvs*kGJ4%mKK)KdL{m7iZpT$Z^N`q%q-qX%C6@?Nhyk#?A1ctXT$o_sYd*{F`^x-bM)i? zSNtFUQ2g#M(_4ReTNcQ*LCf7XMZN_si@aQXvqk{krUY6tV4sk3IrWI*T-WT57;wIY z&vS+*u0J_mXe!j{dlz4A>V9Lt-G-~%Y8XthQugCw03M_oU|A6X#HIe3A3uEN>fnqW zKaXT|r`uk*^zhz|Ze6#!mYIU4&f0}T>?KgKm*5ECQ?(Qpf||3s;hMV}>>k5_lC?QE z1r&|)oz9=^o7Q#vrv1p0yl$vM91;!*RY6D9te(Q~HMEH+_*dEn!>NLqQ6*fYZHaOy z_@iQa7$oohf)LM0*dwUihZ4+?h8dy+!2zvYYvje~w2+1K_>R8x{)sB~Y^38m==Co6cm&TqN zFD2@wFOisS>pDH+Npq+e3@WyPlq6yXadvx14vmY0Q?ERF=E~m5Tq2qkL!uOW>iXfQ zZjLsKmVL=8rNLfI=FODEZ&(Gq+kMD!sS{9yT`^<;UaNKC`ep#A-sI2 z`xNs^ETpj-lw2uQC{(V9z5$*~6_l!+$hMCib4H6HX$D`3vJCG8!B`4p`kI(S4jt9U z2meZb9&CR+P8wJ^&};MHcN_G#G3q^pLFeCu^KWjKB);oSz_~40ja0ih@(L{TH@jJ5 z5!Nu=BAS}mO4?yj&!^SySJcG&-BX`k7_IGfI}6Qmp%*oUN9J4d*Pj|cbaGTL)u%CO zysI-=T6=7kULF>qmoP>bgA6y4Y{QUYUEu=Sj*zpf zYN8>?fVn1201n7*RHCv$R|Db)TA7CWY=Fb*GFEt*z{{g*K!~g!$|oO9!PHnbuFUh)7 zE?hLI9L?S48vv@SK@feB6){PlC68@WWyT!DV^UP8{DRELG%`UAXrtP}jEY~O@12lp zIZPG$n?d#(*=6_4e8ax-(PYzBUTfe&W8CS%lf-X+w~Q-WReP~T+k-^ATm#Gg*{5rC ztQ&z_<6LSp=*X3QBUKE#{cDK2jKJ>}!4|L|6%13A& zsxD*Z_AzU2h8X?PX$?b?#2CaE=6dJ5NuYSc+bt0?7Y^(Jc4Rvi497xP!!A^)P{x1( zuw<5g*z;lSEv2a_+>b~uB$aj`3^_;23#o!!=mI$g-f%_T4tr@MnB$YXn6O)q>?@7- z-^RGhWAY-I+xPwzXw@^Jh#BziR)H?l#&svaRsFbS4%i&Y^(Gix_w@6nYaGISDt)DW z@yJGR4KF>AZ4DKoGIzVxkr{jD`snF{dLdSiFIW5NVTg;&{?!trC5A=ioCc&YX+o!C z*yh1fRvMfZUfW1%tu?*To*sW-qBYapwbIeK+}XO;TAPkX|5}&1Ew-;;@0(j_UpK5d zUn4cM3m#1JLT-f0WR!aJ=*=wJ=cJS0_B2yP9^7eQ3mIcK2uNl@GF@yudg6ZLhqJuP1 z0O6@;cyVbPzcolVXELps+5NLI(pooKTGrc4ByL%6ja9j%=-ucrw_Kty-Mu{5vAW!x zw%bR8NG!;SlLHHpQUw_1%vdRLN2p=35Q7RRTdB0livdd==3pBx1$eX%gTd364mVps zH{>hxyQC^^vy3mf`EX<_4h2M+p%bcnpYw182vY`O@0!^aE6N+L{N(BDErw{wPd3Iq zJ0fJj=1(HpCg8!*+C{-C9b>Icz$0*5X(khZ&6(XG2qnRJz4UhP#24lcSNrNZX}!6Y z`KEMGaM6bOd~;#!*=q;hKhU&R2RV8&F?gSO0y6v7Z1U<9CAqp(L_n2|a(K6ot)78e zc?w0{iX$>PtrCijuFcKvO&jVGg?K96l-g`grc=pmGGT7HB;IPiO%)P{<_68JsFzk+ z(`(IJi>(_=t(zQ)wLm;shFWid!R(5(YXs)r$UPI$&CJ>@!uCBe6_3Kt8CDRbqDqPu z+cW}&)=y(N>K5z;LE>u{livi3u4FEhbCjpS(W&;Yi0!XI(J(t$pUpmND2|VOW*D zw$~7{vY1c+lkhCat4-21he?AQoi^{0Rmb*RT}&3uZL?>C?@F^osq|u5ok}a|WfR4c zCQpTdWS~*hjS^BpFXiHeT&!3E(y9FU3E?GfL58EjyRr=r22k9HktO&!`a)2nL#kp~ zx#w7=Xfuz!Q{|7LqRGMbYFHJFco7t#W@-5#BkUp7&H>dbeOOmE3 z=xg4L;qBBvSN8kcu9iww2lY55OUHVCz@!pWP>c~JPemSQ<{yJk(wQqta-|XS9=UxX zwo-h}9E$~cvK!7K%T5UkIC-_vebgBD(a3GV^|N&Stn*_re`Y;HILLgTx(ByY?U>#5 zD$wTGSgw@;-)^TvYPC7jo0frS1T~?CVDhuq2FJcMb*w56-R@jVlZfZ^B2icbkxh4t zql?+D%*gxuV!7zmfm%)uYk58W@B(lWj1JTh>+=bv!geE&=C(80#atj0!)`1ZQ3)Gh z1(!Tw#j(Nl%dsQO!g{VQ^{q4qiS0&Vo!2eaf)33uqx$sVg9BF0?CCx$Bu;`!T`rNoy#CZ! zBmUeN_i=cdU}>6`rs2caoF7NBsZ5R~9uJO|Npqb5xE>W)P-7Et5^&G$1Mo^SS`1Ee z<&kAiu$+Pxnl|6xGxgNs-l`5`IMZ>xeMyQ-5QPQ-%P*bo+8TTQ%J8|pt#j>>pTefI z1DK1apPVr?GEvP@qOg!p!Z1;Yq=aNylC$cX(`Mq(UhC)P(ce3mBR+%Kne4!}yAP(Z9>yVW62^r9E%K(2!6qrcJ(X}jhhAJ#&k*eZa*q^el> zgCuGsS(ZeRTBiVVok%Rp8;Che8$^E$8AA%bAW$V1Tv0&Kz6sg-DA?)ceuUR%Q!D9M zW0r1=kI~3yVd6YZocHGau6`UkY*S>Kx*X{=-)g=Pjy_tRy&@bnRY8wfOy+jcdbPO> zz3A25*i(({#IdLN3*EDOa?J-et4Ib9r#o+SERhtV0;RB&_kx-eX(?_@K6RsOx~uP_ zA*~p(Qb+1?kG>U8f97UMFXN|XmsT+t<{8#X0d1QuYv@6v7mjupF2wst-3cAl##Dz5 zTd%00*N^KUvF7n$zuu&8dS|B;Zl^UVfWZ=pc@CI&8(TTxP*$k!RMC^d?PRLt14zwt z07GBT{t1*W-cX43-WUaz2W~2tRNn|9mQDt>#wrYhp|W$5jD8EJY(QzImPJ_Q`14pJ z{@fTJ`>{GttMhRF%{oxkb^5peggX22iJ!+OUxdE>k?gfL9m)}?0>HF-Sixw^!6^vqr;0q@|Ze6tIMv}{cV)Rawuie(uIgwn>dW`PUJRcI1 zS|jGu%^>wSmD1>K=r21G`!)w)oA`z%gU##uul=IC4;4g-=`CZZJaPrDiM}h6Xks8@ zfF3jUY7+v+NfdZl3;?GOTmeHK5DK>nN#+_}tOOd_%1F4;62ngKTk6}n5`O3PJ*7qi zQe!kmT}N&U#?R4Pe@PeLhGHIj1|hMdwtM=?cObJ-?Xx~Z@q^W1aG^U}?8pHq?&$zZ zG}SV^qv^J_0~1YBM&PjQEx6da zxqrP!r`y`UIn_^fEpz%smAqe7)>vrGZysER1eH_N(J==sX^HDy7$?!6!O$dVz!{Lc z87jaWh~?T(hf04%|E95WCv-147~24>Gbv4uzTEdoUJu!H<+|p(Uw5u}P%X7Eb=ihQ z0inl08kiRHs~JJMlzhc*28Nw^A@n4PG1uF(kXq zR}V^Vm=uRdJO1o-w%L&EUN+i)8>2Dq!Vm^sdIv7OQ?`V9}3&9G>XaF)Z)Pg3qLO&{R^oQsUw0MPWnp!N|LW%_Y>YeLb)u3oREW z#w)>VqE$#ODugwJL#qtfxB2LA2u8BbV5ra%g_ZQU5W)zvi|eR>y>KEz&SL~?_LcIJ z<;{veuV?f>d%K&U5r1xs#<)wPRD_wUaQX*y>Ibm3h8=sL(rh3vv zeqb{*x~d_@*HclkVQnK>rovr$>c}fh7rYi&g(7Qe{jf@8K@BNGAi}IP-GUO1)+z+3 z{mhtAVJ|ct7Jm+Iz9_V&hMhgX2hFvG;8AFv111c_by+-FVX>ehCk5o48GvzNjnEK@ z7bwOMD6Lc_hG7-id~aB}Z&|sCjOBwOVM>zhf>Xb4TmRaY%eDyR8<*NHPB%B^z{Y5d zyFX--_`+M|BysmpxmT*vSez`|I9551fc^|r-B4?E=1SdJQiE;C!dAw1iM# zSi-Fq;z8sY;8J1$bz z6JttK5CzC1kL#*(%2K0>t6PAoN-Fo}4;phxHU_KPn<4qox>DhGMH)>3tmRLa)zqX^ zYa8VH0bBPQ$aSIFG{=HK9vr{Re&;lrPSxSzICO)^+}p%!pT z&bMX{t(0dN&0553mQ}4)d?bxfeK0xMwM-g4b>qxOLkk0EM#tp*iLTBrxA#FG zx&UjZWXECqXXS=BihDQW*?96sXKcJvTi#KrIp+2Fo5Qh-J;kSIOT(Lj0zJ48pi_37 z8I+nV*p_H1ad}>G9rhI9-bZo`g#NyxARgZWfK_#9@4Dt)S4!5w4nbpBYYq=bv+slh zSENLYm8K(9L&2zKkOT4OV7Yu~v>8|?9DVg!7#4FTR<6a?m0oZ=SRqYH^PK#n5Kgg` z?RsR#pUJxX@wtxig;ZmrYmCO&)x#*l%(Zfo_~iE?y@u^Q(Ao)?Pr=pqs@>DmPs|R? zcWR=#IxI9aV_?f@DaZ_$pYpgjG}bv&&IdQ;Hrmqtt6f^$^U$lm_mF49Xs}voloT#yVv^Gx?)Cn^t(K(BP@5W2di);9Soy8{)yeY zqE5e*G+=0pj;+8WOW2py5&}vZk&x9dkSm)@^MA|NaNFrGv&(%Ryka%@~N3m@ujBK%vlxzALEs{qg)R zM@t#L-rm7MehGY!A?EcPZXZlup|up-B3b3{x)XZO3X4ViVh)}HrCo<5TY;_X7iXF_ zGJ0c9YmCOY_r~;<8lKeGo;QE^oBbQhhZcLrdaJ|V$#(6j8M1P~npLA}d+<2{Y4lnp zdC=8Hh)hfN)T8Ggzj&lA9j=o#GT(P|cs3g^+QDR7h{~2_wnKNCJ-_j^9k+ayt_fC3 z7_5|V$+C;LFAXbu4RHewuEU{q^T;tD!}%tfYoVnSEvIOsicB1CB=l1Q`so4c%ZPm& zVkm9&WXr|~fk>~D71q4ifYLgQ{^bj(JzWqMizYP|D{)HJE%{=rw;zHA@QX?Kbx{J1 zQZc_%f-lhdVG5qh%VLiUFVFA#11_0+Y7{A6P^#>Z&%wioPaWd7J(YNM%Tv5EprZ7UI9Yl6>@T?hXxH4H3)#h_s-`_c z2)a039-2QTY@mD1&0d!UCX*%Dzo{SGbds7SjaHMimZXh1Z6<_ynT?UjlCwJHE3gz@ z;CfQO+^yYgkC`i*;k4MBrlz7FgEfWLYn6yEDA3E%w-WgI-a7_^xb{t<)|TG}4Eq2$ z^ttxhBIZG_ula|W9H6QkDUT5Y_zI-7>OuvfD+p_A@&r06gta~9%`B22pi;@(pTGzC z$XbusqJd{+1Uo{56cI{+o)}iHpq-m&0B~b8#>WYO+Y0XNKr>HG?0@5MqMCxN#6*YD zo%JL$>|xg}qoPlVKFvkzexZrW$H&rb>4VqzVW2d6Xs&N&cs`xVnc!5iW60KACLM|d zlR<{cHoelIS4dmHGFUPUcCTpQ7zk=sBNO&z_5Lgbq%bTDLs8IVy;XUy3B%sMeV<{O z5+a3rI`Qna7IZj(Kx~;mi}T~k*crMaallav(7A0f7%R8s_~j_d>FNr3Uu4-(`3DMc z`H(6nQWcneC)8J-r}`RE7;l!8m)MJXC19#pHm?^)_5_8lb3-dq8kV>fvjA3}E}RWq zZ;dlInhT|d_(o$i#>WYOI|_1YHe$x~v*SjpR0RN!b!ngqGBCm6U?;0@ez)(K+dF^Z ziHrGE!4Ck0hp+FCS&9(mWz5=b%uh(@9Smu_i-rGBk5DuWJxzI*u{-(mYgii@R?j=H6-d&gyO^RcE{{&1KuWy1;XnBX&|m!CvS4hq5Qk1zlS-PFQP6+e`9fBmUeNjq!0B ziJ0hUDsqNYZ2te*yWZcZjw?EM-dj6>F$U@%K!^f~)T)&#rTIXTwrcyK{ZOf&+Hd_& zqqgdoN)3=!1*){c4iMTJC^7g4CcE~&Tl>v>cg~%ew_YbpRB30lvDTaA?P4!;=A3)) zIfM20?j4Z7F|R=rf+Ji=kPwU89_b#ot$4SuCCxB}>9Jwg&YGY9?^?&c6z@VGISDtxwq zvYm=Mq#$%C5TEfcsEnplx6%DpN!HudndRRYhkeA_9)xu@9=Ic?#NuQgQNW(NzEuP~ z)raiov2+v?u`1k(l+jeI+XgGf@cQQ$3JVOlP!(1Q&MP6{*6Q&diddFF!%#pMi^L#A zqw^(d(E)udC`#V7UT}6NgAye40{kv;rq*f!-*kYn>2N;b(fl&`oXOEI8<;$&;uTuW zHY&|>%GN3v^JN_X>@MMbD=|i}6{Q6lnf)11kegT^(@6c82=#LilN-DT$5De*UvPG) zqy$nk;Gu3zI5_lu{@tM$If4t&jWh=ct=nnMjkm4#WGw+O5iKlwUw_enA5-&Ai`%ZKwS>>c`( z0~%^=hyV#ACx_O(;p6Wnfk$m#kx|F+|HQf`sN!eLm z9&?p3*CH75A>$obVo3IN0#vF=Pff8JRT3px+N!-37dmV(#2`j)UpNhA^i&)}J;OO( zRnCH=zU}SPUp~wI&w1rFZTj=TlFGDEgrVI%1}i(;KVRdcW|Qn+mkNLx>GdrFAsP&g z111-A8;KY@JlX%x57yRxeCNfv?YBSv_UY|8*JF%|v?ZYdc?N`^miWnl}BpAJ_{gwvPJi{`r?Lez|(`_gCy@w@1h0llIY^S=nh+y4IS* zg#)toy2r4Y*qTt#7a!kS*!|bFf6hKTwRC@FW~F?-_}rOIR`J(%i?cc<5I5-f*um`{ zTgg19X^cAxccDqyh$A?U>rFdj!I4ev8s!Yz}j!#v0=Lw8c%IeVHO_DQABWn`m7TQJy}nnxvp??)9lxb z0z-%W^{hYVmDdyk@ch8)2BickY&qZX^x(^1tyY(}M{Ur5UP0DdN}o}Y=!>>Nijhg1 zAqSnqgL5y|-&xz6e>qbXs5#>eYs0-lY0=_7fzb$L5+)%531+ASiw_b7-djAXdAKWd zDAb>!J385Nl|*0qKk-khe;Q?&0LY;=qkn|J*@_hVB!a9!802OA+rpKN?wvM9J4;82 z3qWt-Km?;sG6c+!{SageDyA-&UFp+?E?{u&5+^dtZr|Q{crtHBdF3^2Yk6T1IQw_a znKYcXHyr%p{>+UPduy?a1k?Tb;>|51rp=}aMW9Lr!;m&#jNznfkk`~#8_x9)iD^ww zt$T!OzDufeN?5Gh604}?Hb|96^hG1BswTnir7bF^FgWT@5|IZ;7dVcOnna^$=mgN0 zCq$^p0Whk*5jSM*Yr(8K)MZ8p{j*q01SlP#;|G)Xt>l2$w%#BLiCH5t7IY~}b(S-M z?#X6W9bUq=y$u%Bw%xbb;P>&pQ~SegKbcov)3wTi7f2E5C$ zRS~vvmT=Iw>`4xH!m!lbW_1V%YKZPVSUvEQ1`GD|EEsJ?pdL}FJvQD=uI1`g zabE;rCLdoF zHqhC%rX4oV?N@o_HPrxo zu3y+R*&-9FzOwn?esX|##e0v-tJ`}YUgl?TjvrF>yUXQo)+)py zl5F+3fBLJvU^|8c(>{WTBJ&b7Jt2=2Y?J$F%go_WkrF9-?B@-Hep02Ydg1THG2xZDFI74MB(M#+24M; ze`9I?fC{(WB-Nvn)kn+4`;RJHqa!uwNLG;nqNwtkGH~$IIHaOz2&XGeBSDKgEri%g z^$m;JF{^>@S7gIJm`Eq}Vk*Nov}n>$`4wJ}k3JKWikp(U?^R`1DzWNYW^GCp^*MV9 z+9*pb)`~G^Ik3bW!L%`h!DKir#flkY-B-C^1(2?fW(;;Bcph|*udctgu~X)qD6hPx zZJnAOo}b%~cMM}xTW{BvjsZc3&nyHEy(&p!yquy} pf}5}U5WrDrVA@;baQ#n!0RUJt9kspZ&8q+a002ovPDHLkV1l{=rgs1U literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAAppDelegate.h b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAAppDelegate.h new file mode 100644 index 00000000..54c3ee08 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAAppDelegate.h @@ -0,0 +1,31 @@ +// +// LAAppDelegate.h +// photobackup +// +// Created by Nick O'Neill on 10/20/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import +#import +#import "LACamliClient.h" +#import + +@class ALAssetsLibrary; + +static NSString* const CamliUsernameKey = @"org.camlistore.username"; +static NSString* const CamliServerKey = @"org.camlistore.serverurl"; +static NSString* const CamliCredentialsKey = @"org.camlistore.credentials"; + +@interface LAAppDelegate : UIResponder + +@property(strong, nonatomic) UIWindow* window; +@property CLLocationManager* locationManager; + +@property LACamliClient* client; +@property ALAssetsLibrary* library; + +- (void)loadCredentials; +- (void)checkForUploads; + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAAppDelegate.m b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAAppDelegate.m new file mode 100644 index 00000000..4dfa7e4c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAAppDelegate.m @@ -0,0 +1,156 @@ +// +// LAAppDelegate.m +// photobackup +// +// Created by Nick O'Neill on 10/20/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import "LAAppDelegate.h" +#import "LACamliUtil.h" +#import "LACamliFile.h" +#import "LAViewController.h" +#import +#import +#import + +@implementation LAAppDelegate + +- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions +{ + [[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"de94cf9f0f0ad2ea0b19b2ad18ebe11f" + delegate:self]; + [[BITHockeyManager sharedHockeyManager] startManager]; + [[BITHockeyManager sharedHockeyManager].updateManager setDelegate:self]; + [[BITHockeyManager sharedHockeyManager].updateManager checkForUpdate]; + + [BugshotKit enableWithNumberOfTouches:1 + performingGestures:BSKInvocationGestureNone + feedbackEmailAddress:@"nick.oneill@gmail.com"]; + + self.locationManager = [[CLLocationManager alloc] init]; + self.locationManager.delegate = self; + [self.locationManager startMonitoringSignificantLocationChanges]; + + [self loadCredentials]; + + self.library = [[ALAssetsLibrary alloc] init]; + + return YES; +} + +- (void)locationManager:(CLLocationManager*)manager didUpdateLocations:(NSArray*)locations +{ + [self checkForUploads]; +} + +- (void)loadCredentials +{ + NSURL* serverURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] stringForKey:CamliServerKey]]; + NSString* username = [[NSUserDefaults standardUserDefaults] stringForKey:CamliUsernameKey]; + + NSString* password = nil; + if (username) { + password = [LACamliUtil passwordForUsername:username]; + } + + if (serverURL && username && password) { + [LACamliUtil statusText:@[ + @"found credentials" + ]]; + [LACamliUtil logText:@[ + @"found credentials" + ]]; + self.client = [[LACamliClient alloc] initWithServer:serverURL + username:username + andPassword:password]; + + // TODO there must be a better way to get the current instance of this + LAViewController* mainView = (LAViewController*)[(UINavigationController*)self.window.rootViewController topViewController]; + [self.client setDelegate:mainView]; + } else { + [LACamliUtil statusText:@[ + @"credentials or server not found" + ]]; + } + + [self checkForUploads]; +} + +- (void)checkForUploads +{ + if (self.client && [self.client readyToUpload]) { + NSInteger __block filesToUpload = 0; + + [LACamliUtil statusText:@[ + @"looking for new files..." + ]]; + + // checking all assets can take some time + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self.library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop) { + + [group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) { + if (result && [result valueForProperty:ALAssetPropertyType] != ALAssetTypeVideo) { // enumerate returns null after the last item + + NSString *filename = [[result defaultRepresentation] filename]; + + @synchronized(self.client){ + if (![self.client fileAlreadyUploaded:filename]) { + filesToUpload++; + + [LACamliUtil logText:@[[NSString stringWithFormat:@"found %ld files",(long)filesToUpload]]]; + + __block LACamliClient *weakClient = self.client; + + LACamliFile *file = [[LACamliFile alloc] initWithAsset:result]; + [self.client addFile:file withCompletion:^{ + [UIApplication sharedApplication].applicationIconBadgeNumber = [weakClient.uploadQueue operationCount]; + }]; + } + } + } + }]; + + if (filesToUpload == 0) { + [LACamliUtil statusText:@[@"no new files to upload"]]; + } + + [UIApplication sharedApplication].applicationIconBadgeNumber = filesToUpload; + + } failureBlock:^(NSError *error) { + [LACamliUtil errorText:@[@"failed enumerate: ",[error description]]]; + }]; + }); + } +} + +- (void)applicationWillResignActive:(UIApplication*)application +{ + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication*)application +{ + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication*)application +{ + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication*)application +{ + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + [self checkForUploads]; +} + +- (void)applicationWillTerminate:(UIApplication*)application +{ + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliClient.h b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliClient.h new file mode 100644 index 00000000..56b2b66c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliClient.h @@ -0,0 +1,50 @@ +// +// LACamliClient.h +// +// Created by Nick O'Neill on 1/10/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import + +@class LACamliFile, LACamliUploadOperation; + +@protocol LACamliStatusDelegate + +@optional +- (void)finishedDiscovery:(NSDictionary*)config; +- (void)addedUploadOperation:(LACamliUploadOperation*)op; +- (void)finishedUploadOperation:(LACamliUploadOperation*)op; +- (void)uploadProgress:(float)pct forOperation:(LACamliUploadOperation*)op; +@end + +@interface LACamliClient : NSObject + +@property NSURLSessionConfiguration* sessionConfig; +@property id delegate; + +@property NSURL* serverURL; +@property NSString* username; +@property NSString* password; + +@property NSString* blobRootComponent; +@property NSOperationQueue* uploadQueue; +@property NSUInteger totalUploads; + +@property NSMutableArray* uploadedFileNames; +@property UIBackgroundTaskIdentifier backgroundID; + +@property BOOL isAuthorized; +@property BOOL authorizing; + +- (id)initWithServer:(NSURL*)server username:(NSString*)username andPassword:(NSString*)password; +- (BOOL)readyToUpload; +- (void)discoveryWithUsername:(NSString*)user andPassword:(NSString*)pass; + +- (BOOL)fileAlreadyUploaded:(NSString*)filename; +- (void)addFile:(LACamliFile*)file withCompletion:(void (^)())completion; + +- (NSURL*)statURL; +- (NSURL*)uploadURL; + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliClient.m b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliClient.m new file mode 100644 index 00000000..a69ab3a0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliClient.m @@ -0,0 +1,339 @@ +// +// LACamliClient.m +// +// Created by Nick O'Neill on 1/10/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import "LACamliClient.h" +#import "LACamliUploadOperation.h" +#import "LACamliFile.h" +#import "LACamliUtil.h" + +@implementation LACamliClient + +NSString* const CamliStorageGenerationKey = @"org.camlistore.storagetoken"; + +- (id)initWithServer:(NSURL*)server + username:(NSString*)username + andPassword:(NSString*)password +{ + NSParameterAssert(server); + NSParameterAssert(username); + NSParameterAssert(password); + + if (self = [super init]) { + _serverURL = server; + _username = username; + _password = password; + + if ([[NSFileManager defaultManager] + fileExistsAtPath:[self uploadedFilenamesArchivePath]]) { + self.uploadedFileNames = [NSMutableArray + arrayWithContentsOfFile:[self uploadedFilenamesArchivePath]]; + } + + if (!self.uploadedFileNames) { + self.uploadedFileNames = [NSMutableArray array]; + } + + [LACamliUtil logText:@[ + @"uploads in cache: ", + [NSString stringWithFormat:@"%lu", (unsigned long) + [self.uploadedFileNames count]] + ]]; + + self.uploadQueue = [[NSOperationQueue alloc] init]; + self.uploadQueue.maxConcurrentOperationCount = 1; + self.totalUploads = 0; + + self.isAuthorized = false; + self.authorizing = false; + + self.sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; + self.sessionConfig.HTTPAdditionalHeaders = @{ + @"Authorization" : + [NSString stringWithFormat:@"Basic %@", [self encodedAuth]] + }; + } + + return self; +} + +#pragma mark - ready state + +- (BOOL)readyToUpload +{ + // can't upload if we don't have credentials + if (!self.username || !self.password || !self.serverURL) { + [LACamliUtil logText:@[ + @"not ready: no u/p/s" + ]]; + return NO; + } + + // don't want to start a new upload if we're already going + if ([self.uploadQueue operationCount] > 0) { + [LACamliUtil logText:@[ + @"not ready: already uploading" + ]]; + return NO; + } + + [LACamliUtil logText:@[ + @"starting upload" + ]]; + return YES; +} + +#pragma mark - discovery + +// discovery is done on demand when we have a new file to upload +- (void)discoveryWithUsername:(NSString*)user andPassword:(NSString*)pass +{ + [LACamliUtil statusText:@[ + @"discovering..." + ]]; + self.authorizing = YES; + + NSURLSessionConfiguration* discoverConfig = + [NSURLSessionConfiguration defaultSessionConfiguration]; + discoverConfig.HTTPAdditionalHeaders = @{ + @"Accept" : @"text/x-camli-configuration", + @"Authorization" : + [NSString stringWithFormat:@"Basic %@", [self encodedAuth]] + }; + NSURLSession* discoverSession = + [NSURLSession sessionWithConfiguration:discoverConfig + delegate:self + delegateQueue:nil]; + + NSURLSessionDataTask *data = [discoverSession dataTaskWithURL:self.serverURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) + { + + if (error) { + if ([error code] == NSURLErrorNotConnectedToInternet || [error code] == NSURLErrorNetworkConnectionLost) { + LALog(@"connection lost or unavailable"); + [LACamliUtil statusText:@[ + @"internet connection appears offline" + ]]; + } else if ([error code] == NSURLErrorCannotConnectToHost || [error code] == NSURLErrorCannotFindHost) { + LALog(@"can't connect to server"); + [LACamliUtil statusText:@[ + @"can't connect to server" + ]]; + + } else { + LALog(@"error discovery: %@", error); + [LACamliUtil errorText:@[ + @"discovery error: ", + [error description] + ]]; + } + + } else { + NSHTTPURLResponse* res = (NSHTTPURLResponse*)response; + + if (res.statusCode != 200) { + NSString* serverSaid = [[NSString alloc] + initWithData:data + encoding:NSUTF8StringEncoding]; + + [LACamliUtil + errorText:@[ + @"error discovery: ", + serverSaid + ]]; + [LACamliUtil + logText:@[ + [NSString stringWithFormat: + @"server said: %@", + serverSaid] + ]]; + + if ([self.delegate respondsToSelector:@selector(finishedDiscovery:)]) { + [self.delegate finishedDiscovery:@{ + @"error" : serverSaid + }]; + } + } else { + NSError* err; + NSDictionary* config = [NSJSONSerialization JSONObjectWithData:data + options:0 + error:&err]; + if (!err) { + self.blobRootComponent = config[@"blobRoot"]; + self.isAuthorized = YES; + [self.uploadQueue setSuspended:NO]; + + // files may have already been rejected for being previously uploaded when + // dicovery returns, this doesn't kick off a new check for files. The next + // file check will catch anything that was missed by timing + + // if the storage generation changes, zero the saved array + if (![[self storageToken] isEqualToString:config[@"storageGeneration"]]) { + self.uploadedFileNames = [NSMutableArray array]; + [self saveStorageToken:config[@"storageGeneration"]]; + } + + [LACamliUtil + logText: + @[ + [NSString stringWithFormat:@"Welcome to %@'s camlistore", + config[@"ownerName"]] + ]]; + + [LACamliUtil statusText:@[ + @"discovery OK" + ]]; + + if ([self.delegate respondsToSelector:@selector(finishedDiscovery:)]) { + [self.delegate finishedDiscovery:config]; + } + } else { + [LACamliUtil + errorText:@[ + @"bad json from discovery", + [err description] + ]]; + [LACamliUtil + logText:@[ + @"json from discovery: ", + [err description] + ]]; + + if ([self.delegate respondsToSelector:@selector(finishedDiscovery:)]) { + [self.delegate finishedDiscovery:@{ + @"error" : [err description] + }]; + } + } + } + } + }]; + + [data resume]; +} + +#pragma mark - upload methods + +- (BOOL)fileAlreadyUploaded:(NSString*)filename +{ + NSParameterAssert(filename); + + if ([self.uploadedFileNames containsObject:filename]) { + return YES; + } + + return NO; +} + +// starts uploading immediately +- (void)addFile:(LACamliFile*)file withCompletion:(void (^)())completion +{ + NSParameterAssert(file); + + self.totalUploads++; + + if (![self isAuthorized]) { + [self.uploadQueue setSuspended:YES]; + + if (!self.authorizing) { + [self discoveryWithUsername:self.username + andPassword:self.password]; + } + } + + LACamliUploadOperation* op = + [[LACamliUploadOperation alloc] initWithFile:file + andClient:self]; + + __block LACamliUploadOperation* weakOp = op; + op.completionBlock = ^{ + LALog(@"finished op %@", file.blobRef); + if ([self.delegate respondsToSelector:@selector(finishedUploadOperation:)]) { + [self.delegate performSelector:@selector(finishedUploadOperation:) + onThread:[NSThread mainThread] + withObject:weakOp + waitUntilDone:NO]; + } + + if (weakOp.failedTransfer) { + LALog(@"failed transfer"); + } else { + [self.uploadedFileNames addObject:file.name]; + [self.uploadedFileNames writeToFile:[self uploadedFilenamesArchivePath] + atomically:YES]; + } + + if (![self.uploadQueue operationCount]) { + self.totalUploads = 0; + [LACamliUtil statusText:@[@"done uploading"]]; + } + + if (completion) { + completion(); + } + }; + + if ([self.delegate respondsToSelector:@selector(addedUploadOperation:)]) { + [self.delegate performSelector:@selector(addedUploadOperation:) + onThread:[NSThread mainThread] + withObject:op + waitUntilDone:NO]; + } + + [self.uploadQueue addOperation:op]; +} + +#pragma mark - utility + +- (NSString*)storageToken +{ + NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + if ([defaults objectForKey:CamliStorageGenerationKey]) { + return [defaults objectForKey:CamliStorageGenerationKey]; + } + + return nil; +} + +- (void)saveStorageToken:(NSString*)token +{ + NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + [defaults setObject:token + forKey:CamliStorageGenerationKey]; + [defaults synchronize]; +} + +- (NSURL*)blobRoot +{ + return [self.serverURL URLByAppendingPathComponent:self.blobRootComponent]; +} + +- (NSURL*)statURL +{ + return [[self blobRoot] URLByAppendingPathComponent:@"camli/stat"]; +} + +- (NSURL*)uploadURL +{ + return [[self blobRoot] URLByAppendingPathComponent:@"camli/upload"]; +} + +- (NSString*)encodedAuth +{ + NSString* auth = [NSString stringWithFormat:@"%@:%@", self.username, self.password]; + + return [LACamliUtil base64EncodedStringFromString:auth]; +} + +- (NSString*)uploadedFilenamesArchivePath +{ + NSString* documents = NSSearchPathForDirectoriesInDomains( + NSDocumentDirectory, NSUserDomainMask, YES)[0]; + + return [documents stringByAppendingPathComponent:@"uploadedFilenames.plist"]; +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliFile.h b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliFile.h new file mode 100644 index 00000000..e2842ced --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliFile.h @@ -0,0 +1,29 @@ +// +// LACamliFile.h +// +// Created by Nick O'Neill on 1/13/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import + +@class ALAsset; + +@interface LACamliFile : NSObject + +@property ALAsset* asset; +@property NSMutableArray* allBlobs; +@property NSMutableArray* uploadMarks; +@property NSArray* allBlobRefs; + +@property NSString* blobRef; + +- (id)initWithAsset:(ALAsset*)asset; +- (NSArray*)blobsToUpload; + +- (long long)size; +- (NSString *)name; +- (NSDate*)creation; +- (UIImage*)thumbnail; + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliFile.m b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliFile.m new file mode 100644 index 00000000..ab2bb06a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliFile.m @@ -0,0 +1,163 @@ +// +// LACamliFile.m +// +// Created by Nick O'Neill on 1/13/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import "LACamliFile.h" +#import "LACamliUtil.h" +#import + +@implementation LACamliFile + +@synthesize allBlobs = _allBlobs; +@synthesize allBlobRefs = _allBlobRefs; + +static NSUInteger const ChunkSize = 64000; + +- (id)initWithAsset:(ALAsset*)asset +{ + if (self = [super init]) { + _asset = asset; + + self.blobRef = [LACamliUtil blobRef:[self fileData]]; + + float chunkCount = (float)[self size] / (float)ChunkSize; + + _uploadMarks = [NSMutableArray array]; + for (int i = 0; i < chunkCount; i++) { + [_uploadMarks addObject:@YES]; + } + } + + return self; +} + +- (id)initWithPath:(NSString*)path +{ + // TODO, can init from random path to file + + if (self = [super init]) { + // [self setBlobRef:[LACamliClient blobRef:data]]; + // [self setFileData:data]; + + // set time, size and other properties here? + } + + return self; +} + +#pragma mark - convenience + +- (NSData*)fileData +{ + ALAssetRepresentation* rep = [_asset defaultRepresentation]; + Byte* buf = (Byte*)malloc((int)rep.size); + NSUInteger bufferLength = [rep getBytes:buf + fromOffset:0.0 + length:(int)rep.size + error:nil]; + + return [NSData dataWithBytesNoCopy:buf + length:bufferLength + freeWhenDone:YES]; +} + +- (long long)size +{ + return [_asset defaultRepresentation].size; +} + +- (NSString *)name +{ + return [_asset defaultRepresentation].filename; +} + +- (NSDate*)creation +{ + return [_asset valueForProperty:ALAssetPropertyDate]; +} + +- (UIImage*)thumbnail +{ + return [UIImage imageWithCGImage:[_asset thumbnail]]; +} + +- (NSArray*)blobsToUpload +{ + NSMutableArray* blobs = [NSMutableArray array]; + + int i = 0; + for (NSData* blob in _allBlobs) { + if ([[_uploadMarks objectAtIndex:i] boolValue]) { + [blobs addObject:blob]; + } + i++; + } + + return blobs; +} + +#pragma mark - delayed creation methods + +- (void)setAllBlobs:(NSMutableArray*)allBlobs +{ + _allBlobs = allBlobs; +} + +- (NSMutableArray*)allBlobs +{ + if (!_allBlobs) { + [self makeBlobsAndRefs]; + } + + // not a huge fan of how this doesn't obviously assign to _allBlobs + return _allBlobs; +} + +- (void)setAllBlobRefs:(NSArray*)allBlobRefs +{ + _allBlobRefs = allBlobRefs; +} + +- (NSArray*)allBlobRefs +{ + if (!_allBlobRefs) { + [self makeBlobsAndRefs]; + } + + // not a huge fan of how this doesn't obviously assign to _allBlobRefs + return _allBlobRefs; +} + +- (void)makeBlobsAndRefs +{ + LALog(@"making blob refs"); + + NSMutableArray* chunks = [NSMutableArray array]; + NSMutableArray* blobRefs = [NSMutableArray array]; + + float chunkCount = (float)[self size] / (float)ChunkSize; + + NSData* fileData = [self fileData]; + + for (int i = 0; i < chunkCount; i++) { + + // ChunkSize size chunks, unless the last one is less + NSData* chunkData; + if (ChunkSize * (i + 1) <= [self size]) { + chunkData = [fileData subdataWithRange:NSMakeRange(ChunkSize * i, ChunkSize)]; + } else { + chunkData = [fileData subdataWithRange:NSMakeRange(ChunkSize * i, (int)[self size] - (ChunkSize * i))]; + } + + [chunks addObject:chunkData]; + [blobRefs addObject:[LACamliUtil blobRef:chunkData]]; + } + + _allBlobs = chunks; + _allBlobRefs = blobRefs; +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUploadOperation.h b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUploadOperation.h new file mode 100644 index 00000000..256955fb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUploadOperation.h @@ -0,0 +1,29 @@ +// +// LACamliUploadOperation.h +// photobackup +// +// Created by Nick O'Neill on 11/29/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import + +@class LACamliFile, LACamliClient; + +@interface LACamliUploadOperation : NSOperation + +@property LACamliClient* client; +@property LACamliFile* file; +@property NSURLSession* session; +@property UIBackgroundTaskIdentifier taskID; + +@property(readonly) BOOL failedTransfer; +@property(readonly) BOOL isExecuting; +@property(readonly) BOOL isFinished; + +- (id)initWithFile:(LACamliFile*)file andClient:(LACamliClient*)client; +- (BOOL)isConcurrent; + +- (NSString*)name; + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUploadOperation.m b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUploadOperation.m new file mode 100644 index 00000000..7035e82a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUploadOperation.m @@ -0,0 +1,337 @@ +// +// LACamliUploadOperation.m +// photobackup +// +// Created by Nick O'Neill on 11/29/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import "LACamliUploadOperation.h" +#import "LACamliFile.h" +#import "LACamliClient.h" +#import "LACamliUtil.h" + +static NSUInteger const camliVersion = 1; +static NSString* const multipartBoundary = @"Qe43VdbVVaGtkkMd"; + +@implementation LACamliUploadOperation + +- (id)initWithFile:(LACamliFile*)file andClient:(LACamliClient*)client +{ + NSParameterAssert(file); + NSParameterAssert(client); + + if (self = [super init]) { + _file = file; + _client = client; + _isExecuting = NO; + _isFinished = NO; + _failedTransfer = NO; + _session = [NSURLSession sessionWithConfiguration:_client.sessionConfig + delegate:self + delegateQueue:nil]; + } + + return self; +} + +- (BOOL)isConcurrent +{ + return YES; +} + +#pragma mark - convenience + +- (NSString*)name +{ + return _file.blobRef; +} + +#pragma mark - operation flow + +// request stats for each chunk, making sure the server doesn't already have the chunk +- (void)start +{ + [LACamliUtil statusText:@[ + @"performing stat..." + ]]; + + _taskID = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"uploadtask" + expirationHandler:^{ + LALog(@"upload task expired"); + }]; + + if (_client.backgroundID) { + [[UIApplication sharedApplication] endBackgroundTask:_client.backgroundID]; + } + + [self willChangeValueForKey:@"isExecuting"]; + _isExecuting = YES; + [self didChangeValueForKey:@"isExecuting"]; + + NSMutableDictionary* params = [NSMutableDictionary dictionary]; + [params setObject:[NSNumber numberWithInt:camliVersion] + forKey:@"camliversion"]; + + int i = 1; + for (NSString* blobRef in _file.allBlobRefs) { + [params setObject:blobRef + forKey:[NSString stringWithFormat:@"blob%d", i]]; + i++; + } + + NSString* formValues = @""; + for (NSString* key in params) { + formValues = [formValues stringByAppendingString:[NSString stringWithFormat:@"%@=%@&", key, params[key]]]; + } + + LALog(@"uploading to %@", [_client statURL]); + NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:[_client statURL]]; + [req setHTTPMethod:@"POST"]; + [req setHTTPBody:[formValues dataUsingEncoding:NSUTF8StringEncoding]]; + + NSURLSessionDataTask *statTask = [_session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) + { + + if (!error) { + // LALog(@"data: %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); + + // we can remove any chunks that the server claims it already has + NSError* err; + NSMutableDictionary* resObj = [NSJSONSerialization JSONObjectWithData:data + options:0 + error:&err]; + if (err) { + LALog(@"error getting json: %@", err); + } + + if (resObj[@"stat"] != [NSNull null]) { + for (NSDictionary* stat in resObj[@"stat"]) { + for (NSString* blobRef in _file.allBlobRefs) { + if ([stat[@"blobRef"] isEqualToString:blobRef]) { + [_file.uploadMarks replaceObjectAtIndex:[_file.allBlobRefs indexOfObject:blobRef] + withObject:@NO]; + } + } + } + } + + BOOL allUploaded = YES; + for (NSNumber* upload in _file.uploadMarks) { + if ([upload boolValue]) { + allUploaded = NO; + } + } + + // TODO: there's a posibility all chunks have been uploaded but no permanode exists + if (allUploaded) { + LALog(@"everything's been uploaded already for this file"); + [LACamliUtil logText:@[ + @"everything already uploaded for ", + _file.blobRef + ]]; + [self finished]; + return; + } + + [self uploadChunks]; + } else { + if ([error code] == NSURLErrorNotConnectedToInternet || [error code] == NSURLErrorNetworkConnectionLost) { + LALog(@"connection lost or unavailable"); + [LACamliUtil statusText:@[ + @"internet connection appears offline" + ]]; + } else { + LALog(@"failed stat: %@", error); + [LACamliUtil errorText:@[ + @"failed to stat: ", + [error description] + ]]; + [LACamliUtil logText:@[ + [NSString stringWithFormat:@"failed to stat: %@", error] + ]]; + } + + _failedTransfer = YES; + [self finished]; + } + }]; + + [statTask resume]; +} + +- (void)uploadChunks +{ + [LACamliUtil statusText:@[ + @"uploading..." + ]]; + + NSMutableURLRequest* uploadReq = [NSMutableURLRequest requestWithURL:[_client uploadURL]]; + [uploadReq setHTTPMethod:@"POST"]; + [uploadReq setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", multipartBoundary] + forHTTPHeaderField:@"Content-Type"]; + + NSMutableData* uploadData = [self multipartDataForChunks]; + + NSURLSessionUploadTask *upload = [_session uploadTaskWithRequest:uploadReq fromData:uploadData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) + { + + // LALog(@"upload response: %@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]); + + if (error) { + if ([error code] == NSURLErrorNotConnectedToInternet || [error code] == NSURLErrorNetworkConnectionLost) { + LALog(@"connection lost or unavailable"); + [LACamliUtil statusText:@[ + @"internet connection appears offline" + ]]; + } else { + LALog(@"upload error: %@", error); + [LACamliUtil errorText:@[ + @"error uploading: ", + error + ]]; + } + _failedTransfer = YES; + [self finished]; + } else { + [self vivifyChunks]; + } + }]; + + [upload resume]; +} + +// ask the server to vivify the blobrefs into a file +- (void)vivifyChunks +{ + [LACamliUtil statusText:@[ + @"vivify" + ]]; + + NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:[_client uploadURL]]; + [req setHTTPMethod:@"POST"]; + [req setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", multipartBoundary] + forHTTPHeaderField:@"Content-Type"]; + [req addValue:@"1" + forHTTPHeaderField:@"X-Camlistore-Vivify"]; + + NSMutableData* vivifyData = [self multipartVivifyDataForChunks]; + + NSURLSessionUploadTask *vivify = [_session uploadTaskWithRequest:req fromData:vivifyData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) + { + if (error) { + LALog(@"error vivifying: %@", error); + [LACamliUtil errorText:@[ + @"error vivify: ", + [error description] + ]]; + _failedTransfer = YES; + } + + [self finished]; + }]; + + [vivify resume]; +} + +- (void)finished +{ + [LACamliUtil statusText:@[ + @"cleaning up..." + ]]; + + _client.backgroundID = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"queuesync" + expirationHandler:^{ + LALog(@"queue sync task expired"); + }]; + + [[UIApplication sharedApplication] endBackgroundTask:_taskID]; + + LALog(@"finished op %@", _file.blobRef); + + // There's an extra retain on this operation that I cannot find, + // this mitigates the issue so the leak is tiny + _file.allBlobs = nil; + + [self willChangeValueForKey:@"isExecuting"]; + [self willChangeValueForKey:@"isFinished"]; + + _isExecuting = NO; + _isFinished = YES; + + [self didChangeValueForKey:@"isExecuting"]; + [self didChangeValueForKey:@"isFinished"]; +} + +#pragma mark - nsurlsession delegate + +- (void)URLSession:(NSURLSession*)session task:(NSURLSessionTask*)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend +{ + if ([_client.delegate respondsToSelector:@selector(uploadProgress: + forOperation:)]) { + float progress = (float)totalBytesSent / (float)totalBytesExpectedToSend; + + dispatch_async(dispatch_get_main_queue(), ^{ + [_client.delegate uploadProgress:progress forOperation:self]; + }); + } +} + +#pragma mark - multipart bits + +- (NSMutableData*)multipartDataForChunks +{ + NSMutableData* data = [NSMutableData data]; + + for (NSData* chunk in [_file blobsToUpload]) { + [data appendData:[[NSString stringWithFormat:@"--%@\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]]; + // server ignores this filename and mimetype, it doesn't matter what it is + [data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"image.jpg\"\r\n", [LACamliUtil blobRef:chunk]] dataUsingEncoding:NSUTF8StringEncoding]]; + [data appendData:[@"Content-Type: image/jpeg\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; + [data appendData:chunk]; + [data appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]]; + } + + [data appendData:[[NSString stringWithFormat:@"--%@--\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]]; + + return data; +} + +- (NSMutableData*)multipartVivifyDataForChunks +{ + NSMutableData* data = [NSMutableData data]; + + NSMutableDictionary* schemaBlob = [@{ + @"camliVersion" : @1, + @"camliType" : @"file", + @"unixMTime" : [LACamliUtil rfc3339StringFromDate:_file.creation], + @"fileName" : _file.name + } mutableCopy]; + + NSMutableArray* parts = [NSMutableArray array]; + int i = 0; + for (NSString* blobRef in _file.allBlobRefs) { + [parts addObject:@{ + @"blobRef" : blobRef, @"size" : [NSNumber numberWithInteger:[[_file.allBlobs objectAtIndex:i] length]] + }]; + i++; + } + [schemaBlob setObject:parts + forKey:@"parts"]; + + NSData* schemaData = [NSJSONSerialization dataWithJSONObject:schemaBlob + options:NSJSONWritingPrettyPrinted + error:nil]; + + [data appendData:[[NSString stringWithFormat:@"--%@\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]]; + [data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"json\"\r\n", [LACamliUtil blobRef:schemaData]] dataUsingEncoding:NSUTF8StringEncoding]]; + [data appendData:[@"Content-Type: application/json\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; + [data appendData:schemaData]; + [data appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]]; + + [data appendData:[[NSString stringWithFormat:@"--%@--\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]]; + + return data; +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUtil.h b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUtil.h new file mode 100644 index 00000000..52f84005 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUtil.h @@ -0,0 +1,23 @@ +// +// LACamliUtil.h +// photobackup +// +// Created by Nick O'Neill on 11/29/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import + +@interface LACamliUtil : NSObject + ++ (NSString*)base64EncodedStringFromString:(NSString*)string; ++ (NSString*)passwordForUsername:(NSString*)username; ++ (BOOL)savePassword:(NSString*)password forUsername:(NSString*)username; ++ (NSString*)blobRef:(NSData*)data; ++ (NSString*)rfc3339StringFromDate:(NSDate*)date; + ++ (void)logText:(NSArray*)logs; ++ (void)statusText:(NSArray*)statuses; ++ (void)errorText:(NSArray*)errors; + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUtil.m b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUtil.m new file mode 100644 index 00000000..6eaceaf6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LACamliClient/LACamliUtil.m @@ -0,0 +1,174 @@ +// +// LACamliUtil.m +// photobackup +// +// Created by Nick O'Neill on 11/29/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import "LACamliUtil.h" +#import "LAAppDelegate.h" +#import +#import + +@implementation LACamliUtil + +static NSString* const serviceName = @"org.camlistore.credentials"; + +// h/t AFNetworking ++ (NSString*)base64EncodedStringFromString:(NSString*)string +{ + NSData* data = [NSData dataWithBytes:[string UTF8String] + length:[string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]]; + NSUInteger length = [data length]; + NSMutableData* mutableData = [NSMutableData dataWithLength:((length + 2) / 3) * 4]; + + uint8_t* input = (uint8_t*)[data bytes]; + uint8_t* output = (uint8_t*)[mutableData mutableBytes]; + + for (NSUInteger i = 0; i < length; i += 3) { + NSUInteger value = 0; + for (NSUInteger j = i; j < (i + 3); j++) { + value <<= 8; + if (j < length) { + value |= (0xFF & input[j]); + } + } + + static uint8_t const kAFBase64EncodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + NSUInteger idx = (i / 3) * 4; + output[idx + 0] = kAFBase64EncodingTable[(value >> 18) & 0x3F]; + output[idx + 1] = kAFBase64EncodingTable[(value >> 12) & 0x3F]; + output[idx + 2] = (i + 1) < length ? kAFBase64EncodingTable[(value >> 6) & 0x3F] : '='; + output[idx + 3] = (i + 2) < length ? kAFBase64EncodingTable[(value >> 0) & 0x3F] : '='; + } + + return [[NSString alloc] initWithData:mutableData + encoding:NSASCIIStringEncoding]; +} + +#pragma mark - keychain stuff + ++ (NSString*)passwordForUsername:(NSString*)username +{ + NSError* error; + NSString* password = [SSKeychain passwordForService:CamliCredentialsKey + account:username + error:&error]; + + if (!password || error) { + [LACamliUtil errorText:@[ + @"error getting password: ", + [error description] + ]]; + return nil; + } + + return password; +} + ++ (BOOL)savePassword:(NSString*)password forUsername:(NSString*)username +{ + NSError* error; + BOOL setPassword = [SSKeychain setPassword:password + forService:CamliCredentialsKey + account:username + error:&error]; + + if (!setPassword || error) { + [LACamliUtil errorText:@[ + @"error setting password: ", + [error description] + ]]; + + return NO; + } + + return YES; +} + +#pragma mark - hashes + ++ (NSString*)blobRef:(NSData*)data +{ + uint8_t digest[CC_SHA1_DIGEST_LENGTH]; + + CC_SHA1(data.bytes, data.length, digest); + + NSMutableString* output = [NSMutableString stringWithCapacity:(CC_SHA1_DIGEST_LENGTH * 2) + 5]; + [output appendString:@"sha1-"]; + + for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) { + [output appendFormat:@"%02x", digest[i]]; + } + + return output; +} + +#pragma mark - dates + ++ (NSString*)rfc3339StringFromDate:(NSDate*)date +{ + NSDateFormatter* rfc3339DateFormatter = [[NSDateFormatter alloc] init]; + + NSLocale* enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + + [rfc3339DateFormatter setLocale:enUSPOSIXLocale]; + [rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; + [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + + return [rfc3339DateFormatter stringFromDate:date]; +} + +#pragma mark - yucky logging hack + ++ (void)logText:(NSArray*)logs +{ + NSMutableString* logString = [NSMutableString string]; + + for (NSString* log in logs) { + [logString appendString:log]; + } + + LALog(@"LOG: %@", logString); + + [[NSNotificationCenter defaultCenter] postNotificationName:@"logtext" + object:@{ + @"text" : logString + }]; +} + ++ (void)statusText:(NSArray*)statuses +{ + NSMutableString* statusString = [NSMutableString string]; + + for (NSString* status in statuses) { + [statusString appendString:status]; + } + + LALog(@"STATUS: %@", statusString); + + [[NSNotificationCenter defaultCenter] postNotificationName:@"statusText" + object:@{ + @"text" : statusString + }]; +} + ++ (void)errorText:(NSArray*)errors +{ + NSMutableString* errorString = [NSMutableString string]; + + for (NSString* error in errors) { + [errorString appendString:error]; + } + + LALog(@"ERROR: %@", errorString); + + [[NSNotificationCenter defaultCenter] postNotificationName:@"errorText" + object:@{ + @"text" : errorString + }]; +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAViewController.h b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAViewController.h new file mode 100644 index 00000000..e077a8c5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAViewController.h @@ -0,0 +1,22 @@ +// +// LAViewController.h +// photobackup +// +// Created by Nick O'Neill on 10/20/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import +#import "LACamliClient.h" + +@class ProgressViewController; + +@interface LAViewController : UIViewController + +@property IBOutlet UITableView* table; +@property NSMutableArray* operations; +@property ProgressViewController* progress; + +- (void)dismissSettings; + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAViewController.m b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAViewController.m new file mode 100644 index 00000000..b9c2eec3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/LAViewController.m @@ -0,0 +1,207 @@ +// +// LAViewController.m +// photobackup +// +// Created by Nick O'Neill on 10/20/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import "LAViewController.h" +#import "LACamliClient.h" +#import "LAAppDelegate.h" +#import "LACamliUtil.h" +#import "SettingsViewController.h" +#import "LACamliUploadOperation.h" +#import "UploadStatusCell.h" +#import "UploadTaskCell.h" +#import "LACamliFile.h" +#import + +@implementation LAViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + _operations = [NSMutableArray array]; + + self.navigationItem.title = @"camlistore"; + + UIBarButtonItem* reportItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction + target:self + action:@selector(reportBug)]; + + [self.navigationItem setLeftBarButtonItem:reportItem]; + + UIBarButtonItem* settingsItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit + target:self + action:@selector(showSettings)]; + + [self.navigationItem setRightBarButtonItem:settingsItem]; + + [[NSNotificationCenter defaultCenter] addObserverForName:@"statusText" object:nil queue:nil usingBlock:^(NSNotification *note) + { + UploadStatusCell* cell = (UploadStatusCell*)[_table cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 + inSection:0]]; + + dispatch_async(dispatch_get_main_queue(), ^{ + cell.status.text = note.object[@"text"]; + }); + }]; + + [[NSNotificationCenter defaultCenter] addObserverForName:@"errorText" object:nil queue:nil usingBlock:^(NSNotification *note) + { + UploadStatusCell* cell = (UploadStatusCell*)[_table cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 + inSection:0]]; + + dispatch_async(dispatch_get_main_queue(), ^{ + cell.error.text = note.object[@"text"]; + }); + }]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + NSURL* serverURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] stringForKey:CamliServerKey]]; + NSString* username = [[NSUserDefaults standardUserDefaults] stringForKey:CamliUsernameKey]; + + NSString* password = nil; + if (username) { + password = [LACamliUtil passwordForUsername:username]; + } + + if (!serverURL || !username || !password) { + [self showSettings]; + } +} + +- (void)reportBug +{ + [BugshotKit show]; +} + +- (void)showSettings +{ + SettingsViewController* settings = [self.storyboard instantiateViewControllerWithIdentifier:@"settings"]; + [settings setParent:self]; + + [self presentViewController:settings + animated:YES + completion:nil]; +} + +- (void)dismissSettings +{ + [self dismissViewControllerAnimated:YES + completion:nil]; + + [(LAAppDelegate*)[[UIApplication sharedApplication] delegate] loadCredentials]; +} + +#pragma mark - client delegate methods + +- (void)addedUploadOperation:(LACamliUploadOperation*)op +{ + @synchronized(_operations) + { + NSIndexPath* path = [NSIndexPath indexPathForRow:[_operations count] + inSection:1]; + + [_operations addObject:op]; + [_table insertRowsAtIndexPaths:@[ + path + ] + withRowAnimation:UITableViewRowAnimationAutomatic]; + } +} + +- (void)finishedUploadOperation:(LACamliUploadOperation*)op +{ + NSIndexPath* path = [NSIndexPath indexPathForRow:[_operations indexOfObject:op] + inSection:1]; + + @synchronized(_operations) + { + [_operations removeObject:op]; + [_table deleteRowsAtIndexPaths:@[ + path + ] + withRowAnimation:UITableViewRowAnimationAutomatic]; + } +} + +- (void)uploadProgress:(float)pct forOperation:(LACamliUploadOperation*)op +{ + NSIndexPath* path = [NSIndexPath indexPathForRow:[_operations indexOfObject:op] + inSection:1]; + UploadTaskCell* cell = (UploadTaskCell*)[_table cellForRowAtIndexPath:path]; + + cell.progress.progress = pct; +} + +#pragma mark - table view methods + +- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath +{ + if (indexPath.section == 0) { + UploadStatusCell* cell = [tableView dequeueReusableCellWithIdentifier:@"statusCell" + forIndexPath:indexPath]; + + return cell; + } else { + UploadTaskCell* cell = [tableView dequeueReusableCellWithIdentifier:@"taskCell" + forIndexPath:indexPath]; + + cell.progress.progress = 0.0; + + LACamliUploadOperation* op = [_operations objectAtIndex:indexPath.row]; + [cell.displayText setText:[NSString stringWithFormat:@"%@", [op name]]]; + [cell.preview setImage:[op.file thumbnail]]; + + return cell; + } + + return nil; +} + +- (NSString*)tableView:(UITableView*)tableView titleForHeaderInSection:(NSInteger)section +{ + NSString* title = @""; + + if (section == 0) { + title = @"status"; + } else { + title = @"uploads"; + } + + return title; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView +{ + return 2; +} + +- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section +{ + if (section == 0) { + return 1; + } else { + return [_operations count]; + } +} + +#pragma mark - other + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/SettingsViewController.h b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/SettingsViewController.h new file mode 100644 index 00000000..01d4e133 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/SettingsViewController.h @@ -0,0 +1,23 @@ +// +// SettingsViewController.h +// photobackup +// +// Created by Nick O'Neill on 12/16/13. +// Copyright (c) 2013 Nick O'Neill. All rights reserved. +// + +#import + +@class LAViewController; + +@interface SettingsViewController : UIViewController + +@property(weak) LAViewController* parent; +@property IBOutlet UILabel* errors; +@property IBOutlet UITextField* server; +@property IBOutlet UITextField* username; +@property IBOutlet UITextField* password; + +- (IBAction)validate; + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/SettingsViewController.m b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/SettingsViewController.m new file mode 100644 index 00000000..783bcadd --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/SettingsViewController.m @@ -0,0 +1,122 @@ +// +// SettingsViewController.m +// photobackup +// +// Created by Nick O'Neill on 12/16/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import "SettingsViewController.h" +#import "LAViewController.h" +#import "LACamliUtil.h" +#import "LAAppDelegate.h" + +@interface SettingsViewController () + +@end + +@implementation SettingsViewController + +- (id)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil + bundle:nibBundleOrNil]; + if (self) { + // Custom initialization + } + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + NSString* serverUrl = [[NSUserDefaults standardUserDefaults] stringForKey:CamliServerKey]; + if (serverUrl) { + self.server.text = serverUrl; + } + + NSString* username = [[NSUserDefaults standardUserDefaults] stringForKey:CamliUsernameKey]; + if (username) { + self.username.text = username; + + NSString* password = [LACamliUtil passwordForUsername:username]; + if (password) { + self.password.text = password; + } + } +} + +#pragma mark - uitextfield delegate + +- (BOOL)textFieldShouldReturn:(UITextField*)textField +{ + LALog(@"text field return %@", textField); + + [self.server resignFirstResponder]; + [self.username resignFirstResponder]; + [self.password resignFirstResponder]; + + if (textField == self.server) { + [self.username becomeFirstResponder]; + } else if (textField == self.username) { + [self.password becomeFirstResponder]; + } + + return YES; +} + +#pragma mark - done + +- (IBAction)validate +{ + self.errors.text = @""; + + BOOL hasErrors = NO; + + NSURL* serverUrl = [NSURL URLWithString:self.server.text]; + + if (!serverUrl || !serverUrl.scheme || !serverUrl.host) { + hasErrors = YES; + self.errors.text = @"bad url :("; + } + + if (!self.username.text || [self.username.text isEqualToString:@""]) { + hasErrors = YES; + self.errors.text = [self.errors.text stringByAppendingString:@"type a username :("]; + } + + if (!self.password.text || [self.password.text isEqualToString:@""]) { + hasErrors = YES; + self.errors.text = [self.errors.text stringByAppendingString:@"type a password :("]; + } + + if (!hasErrors) { + [self saveValues]; + } +} + +- (void)saveValues +{ + [LACamliUtil savePassword:self.password.text forUsername:self.username.text]; + + [[NSUserDefaults standardUserDefaults] setObject:self.username.text + forKey:CamliUsernameKey]; + [[NSUserDefaults standardUserDefaults] setObject:self.server.text + forKey:CamliServerKey]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + [LACamliUtil errorText:@[ + @"" + ]]; + + [self.parent dismissSettings]; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadStatusCell.h b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadStatusCell.h new file mode 100644 index 00000000..2813ed17 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadStatusCell.h @@ -0,0 +1,16 @@ +// +// UploadStatusCell.h +// photobackup +// +// Created by Nick O'Neill on 1/6/14. +// Copyright (c) 2014 Nick O'Neill. All rights reserved. +// + +#import + +@interface UploadStatusCell : UITableViewCell + +@property IBOutlet UILabel* status; +@property IBOutlet UILabel* error; + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadStatusCell.m b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadStatusCell.m new file mode 100644 index 00000000..d5f43168 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadStatusCell.m @@ -0,0 +1,31 @@ +// +// UploadStatusCell.m +// photobackup +// +// Created by Nick O'Neill on 1/6/14. +// Copyright (c) 2014 Nick O'Neill. All rights reserved. +// + +#import "UploadStatusCell.h" + +@implementation UploadStatusCell + +- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString*)reuseIdentifier +{ + self = [super initWithStyle:style + reuseIdentifier:reuseIdentifier]; + if (self) { + // Initialization code + } + return self; +} + +- (void)setSelected:(BOOL)selected animated:(BOOL)animated +{ + [super setSelected:selected + animated:animated]; + + // Configure the view for the selected state +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadTaskCell.h b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadTaskCell.h new file mode 100644 index 00000000..abeb6807 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadTaskCell.h @@ -0,0 +1,17 @@ +// +// UploadTaskCell.h +// photobackup +// +// Created by Nick O'Neill on 1/6/14. +// Copyright (c) 2014 Nick O'Neill. All rights reserved. +// + +#import + +@interface UploadTaskCell : UITableViewCell + +@property IBOutlet UILabel* displayText; +@property IBOutlet UIImageView* preview; +@property IBOutlet UIProgressView* progress; + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadTaskCell.m b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadTaskCell.m new file mode 100644 index 00000000..fc5ad31e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/UploadTaskCell.m @@ -0,0 +1,31 @@ +// +// UploadTaskCell.m +// photobackup +// +// Created by Nick O'Neill on 1/6/14. +// Copyright (c) 2014 Nick O'Neill. All rights reserved. +// + +#import "UploadTaskCell.h" + +@implementation UploadTaskCell + +- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString*)reuseIdentifier +{ + self = [super initWithStyle:style + reuseIdentifier:reuseIdentifier]; + if (self) { + // Initialization code + } + return self; +} + +- (void)setSelected:(BOOL)selected animated:(BOOL)animated +{ + [super setSelected:selected + animated:animated]; + + // Configure the view for the selected state +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/en.lproj/InfoPlist.strings b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/en.lproj/InfoPlist.strings new file mode 100644 index 00000000..477b28ff --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/main.m b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/main.m new file mode 100644 index 00000000..5a06d486 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/main.m @@ -0,0 +1,18 @@ +// +// main.m +// photobackup +// +// Created by Nick O'Neill on 10/20/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import + +#import "LAAppDelegate.h" + +int main(int argc, char * argv[]) +{ + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([LAAppDelegate class])); + } +} diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/photobackup-Info.plist b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/photobackup-Info.plist new file mode 100644 index 00000000..812e94fa --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/photobackup-Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Camlistore + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + org.camlistore.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 20140224 + LSRequiresIPhoneOS + + UIBackgroundModes + + location + + UIMainStoryboardFile + Main_iPhone + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/photobackup-Prefix.pch b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/photobackup-Prefix.pch new file mode 100644 index 00000000..4a09e3b1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackup/photobackup-Prefix.pch @@ -0,0 +1,23 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#import + +#ifndef __IPHONE_5_0 +#warning "This project uses features only available in iOS SDK 5.0 and later." +#endif + +#ifdef DEBUG +# define LALog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__) +#else +# define LALog(...) +#endif + +#ifdef __OBJC__ + #import + #import + #import "LACamliUtil.h" +#endif diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackupTests/en.lproj/InfoPlist.strings b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackupTests/en.lproj/InfoPlist.strings new file mode 100644 index 00000000..477b28ff --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackupTests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackupTests/photobackupTests-Info.plist b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackupTests/photobackupTests-Info.plist new file mode 100644 index 00000000..35e2b971 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackupTests/photobackupTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + net.launchapps.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackupTests/photobackupTests.m b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackupTests/photobackupTests.m new file mode 100644 index 00000000..e8947f21 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/ios-objc/photobackupTests/photobackupTests.m @@ -0,0 +1,34 @@ +// +// photobackupTests.m +// photobackupTests +// +// Created by Nick O'Neill on 10/20/13. +// Copyright (c) 2013 The Camlistore Authors. All rights reserved. +// + +#import + +@interface photobackupTests : XCTestCase + +@end + +@implementation photobackupTests + +- (void)setUp +{ + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown +{ + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testExample +{ + XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/js/README b/vendor/github.com/camlistore/camlistore/clients/js/README new file mode 100644 index 00000000..b4792a81 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/js/README @@ -0,0 +1,6 @@ +This is a sketch of a client written in JavaScript. + +The idea is twofold: +1) any working server can drop in this directory to get a blobstore + browser UI, while simultaneously testing their API implementation +2) provide an easy library to plug into node.js if needed diff --git a/vendor/github.com/camlistore/camlistore/clients/js/camel.jpg b/vendor/github.com/camlistore/camlistore/clients/js/camel.jpg new file mode 100644 index 0000000000000000000000000000000000000000..34a46f4033848f8d1afb603cbaf16b46c8415ad8 GIT binary patch literal 19234 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 bU=#WMXp@B8dC02e0C2!BuS`(T(?9>&@!Wyp literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/clients/js/client.js b/vendor/github.com/camlistore/camlistore/clients/js/client.js new file mode 100644 index 00000000..aff5b065 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/js/client.js @@ -0,0 +1,38 @@ +var Camli = { + +BlobStore: function() { +} + +}; + +Camli.BlobStore.prototype.blobURL = function(ref) { + return '/camli/' + ref; +}; + +Camli.BlobStore.prototype.xhr = function(url, cb) { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200) { + cb(xhr.responseText); + } + } + // XXX handle error + }; + xhr.open('GET', url, true); + xhr.send(null); +}; + +Camli.BlobStore.prototype.xhrJSON = function(url, cb) { + this.xhr('/camli/enumerate-blobs', function(data) { + cb(JSON.parse(data)); + }); +}; + +Camli.BlobStore.prototype.enumerate = function(cb) { + this.xhrJSON('/camli/enumerate-blobs', cb); +}; + +Camli.BlobStore.prototype.getBlob = function(ref, cb) { + this.xhr(this.blobURL(ref), cb); +}; diff --git a/vendor/github.com/camlistore/camlistore/clients/js/index.html b/vendor/github.com/camlistore/camlistore/clients/js/index.html new file mode 100644 index 00000000..7315a986 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/js/index.html @@ -0,0 +1,61 @@ + + + + + + + + + +

    + + + +
    refsize
    + + + + + diff --git a/vendor/github.com/camlistore/camlistore/clients/js/style.css b/vendor/github.com/camlistore/camlistore/clients/js/style.css new file mode 100644 index 00000000..1031c002 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/js/style.css @@ -0,0 +1,27 @@ +body { + font-family: sans-serif; + font-size: 0.8em; +} +#logo { + background: url(camel.jpg); + width: 320px; + height: 302px; + color: white; + font-size: 400%; + text-align: center; + vertical-align: bottom; + text-shadow: 0 0 10px black; +} +#bloblist { + border: solid 1px gray; +} +#bloblist th { + background: #eee; + padding: 2px 4px; +} +#bloblist td { + padding-right: 1ex; +} +.blobref { + font-family: WebKitWorkaround, monospace; +} diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/.gitignore b/vendor/github.com/camlistore/camlistore/clients/osx/.gitignore new file mode 100644 index 00000000..ed29a66e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/.gitignore @@ -0,0 +1,4 @@ +*.pbxuser +*.xccheckout +build/ +xcuserdata diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/BUILDING b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/BUILDING new file mode 100644 index 00000000..815eea67 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/BUILDING @@ -0,0 +1,8 @@ +1. Install Go. +2. Install xcode. +3. From top-level directory: + $ go run make.go + (this will create bin/camlistored, bin/cammount, and bin/camput) +4. From this directory: + $ xcodebuild -target Camlistore.dmg + (this will create build/Release/Camlistore.app and Camlistore.dmg) diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore.xcodeproj/project.pbxproj b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore.xcodeproj/project.pbxproj new file mode 100644 index 00000000..6d34abe7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore.xcodeproj/project.pbxproj @@ -0,0 +1,591 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + DA31F9381866744D002E3F33 /* TimeTravelWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = DA31F9361866744D002E3F33 /* TimeTravelWindowController.m */; }; + DA31F9391866744D002E3F33 /* TimeTravelWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DA31F9371866744D002E3F33 /* TimeTravelWindowController.xib */; }; + DA71BCB718642A3A000A102C /* Camlicon.icns in Resources */ = {isa = PBXBuildFile; fileRef = DA71BCB618642A3A000A102C /* Camlicon.icns */; }; + DAABA574186435DA000D62B6 /* camlistored in Resources */ = {isa = PBXBuildFile; fileRef = DAABA572186435DA000D62B6 /* camlistored */; }; + DAABA575186435DA000D62B6 /* cammount in Resources */ = {isa = PBXBuildFile; fileRef = DAABA573186435DA000D62B6 /* cammount */; }; + DAABA57718643710000D62B6 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = DAABA57618643710000D62B6 /* Credits.html */; }; + DAD59F8C1877CC250018193C /* camput in Resources */ = {isa = PBXBuildFile; fileRef = DAD59F8B1877CC250018193C /* camput */; }; + DAF109491863EDAF00F6A3F9 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAF109481863EDAF00F6A3F9 /* Cocoa.framework */; }; + DAF109531863EDAF00F6A3F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DAF109511863EDAF00F6A3F9 /* InfoPlist.strings */; }; + DAF109551863EDAF00F6A3F9 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DAF109541863EDAF00F6A3F9 /* main.m */; }; + DAF1095C1863EDAF00F6A3F9 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DAF1095B1863EDAF00F6A3F9 /* AppDelegate.m */; }; + DAF109611863EDAF00F6A3F9 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DAF109601863EDAF00F6A3F9 /* Images.xcassets */; }; + DAF109681863EDAF00F6A3F9 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAF109671863EDAF00F6A3F9 /* XCTest.framework */; }; + DAF109691863EDAF00F6A3F9 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAF109481863EDAF00F6A3F9 /* Cocoa.framework */; }; + DAF109711863EDAF00F6A3F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DAF1096F1863EDAF00F6A3F9 /* InfoPlist.strings */; }; + DAF109731863EDAF00F6A3F9 /* CamlistoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DAF109721863EDAF00F6A3F9 /* CamlistoreTests.m */; }; + DAF1097E1863EEFB00F6A3F9 /* LoginItemManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DAF1097D1863EEFB00F6A3F9 /* LoginItemManager.m */; }; + DAF109831863F1F900F6A3F9 /* menuicon-selected.png in Resources */ = {isa = PBXBuildFile; fileRef = DAF1097F1863F1F900F6A3F9 /* menuicon-selected.png */; }; + DAF109841863F1F900F6A3F9 /* menuicon-selected@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DAF109801863F1F900F6A3F9 /* menuicon-selected@2x.png */; }; + DAF109851863F1F900F6A3F9 /* menuicon.png in Resources */ = {isa = PBXBuildFile; fileRef = DAF109811863F1F900F6A3F9 /* menuicon.png */; }; + DAF109861863F1F900F6A3F9 /* menuicon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DAF109821863F1F900F6A3F9 /* menuicon@2x.png */; }; + DAF1098A1863FDD600F6A3F9 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = DAF1095D1863EDAF00F6A3F9 /* MainMenu.xib */; }; + DAF63B9D1864D0AC0000EAC9 /* FUSEManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DAF63B9C1864D0AC0000EAC9 /* FUSEManager.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + DAC21F0C188B359300EEA8BB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DAF1093D1863EDAF00F6A3F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DAF109441863EDAF00F6A3F9; + remoteInfo = Camlistore; + }; + DAF1096A1863EDAF00F6A3F9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DAF1093D1863EDAF00F6A3F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DAF109441863EDAF00F6A3F9; + remoteInfo = Camlistore; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + DA31F9351866744D002E3F33 /* TimeTravelWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TimeTravelWindowController.h; sourceTree = ""; }; + DA31F9361866744D002E3F33 /* TimeTravelWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TimeTravelWindowController.m; sourceTree = ""; }; + DA31F9371866744D002E3F33 /* TimeTravelWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TimeTravelWindowController.xib; sourceTree = ""; }; + DA71BCB618642A3A000A102C /* Camlicon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = Camlicon.icns; sourceTree = ""; }; + DAABA572186435DA000D62B6 /* camlistored */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = camlistored; path = ../../../bin/camlistored; sourceTree = ""; }; + DAABA573186435DA000D62B6 /* cammount */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = cammount; path = ../../../bin/cammount; sourceTree = ""; }; + DAABA57618643710000D62B6 /* Credits.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = Credits.html; sourceTree = ""; }; + DAD59F8B1877CC250018193C /* camput */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = camput; path = ../../../bin/camput; sourceTree = ""; }; + DAF109451863EDAF00F6A3F9 /* Camlistore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Camlistore.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DAF109481863EDAF00F6A3F9 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + DAF1094B1863EDAF00F6A3F9 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; + DAF1094C1863EDAF00F6A3F9 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; + DAF1094D1863EDAF00F6A3F9 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + DAF109501863EDAF00F6A3F9 /* Camlistore-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Camlistore-Info.plist"; sourceTree = ""; }; + DAF109521863EDAF00F6A3F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + DAF109541863EDAF00F6A3F9 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + DAF109561863EDAF00F6A3F9 /* Camlistore-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Camlistore-Prefix.pch"; sourceTree = ""; }; + DAF1095A1863EDAF00F6A3F9 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + DAF1095B1863EDAF00F6A3F9 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + DAF1095E1863EDAF00F6A3F9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + DAF109601863EDAF00F6A3F9 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + DAF109661863EDAF00F6A3F9 /* CamlistoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CamlistoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DAF109671863EDAF00F6A3F9 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + DAF1096E1863EDAF00F6A3F9 /* CamlistoreTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "CamlistoreTests-Info.plist"; sourceTree = ""; }; + DAF109701863EDAF00F6A3F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + DAF109721863EDAF00F6A3F9 /* CamlistoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CamlistoreTests.m; sourceTree = ""; }; + DAF1097C1863EEFB00F6A3F9 /* LoginItemManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LoginItemManager.h; sourceTree = ""; }; + DAF1097D1863EEFB00F6A3F9 /* LoginItemManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LoginItemManager.m; sourceTree = ""; }; + DAF1097F1863F1F900F6A3F9 /* menuicon-selected.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menuicon-selected.png"; sourceTree = ""; }; + DAF109801863F1F900F6A3F9 /* menuicon-selected@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menuicon-selected@2x.png"; sourceTree = ""; }; + DAF109811863F1F900F6A3F9 /* menuicon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menuicon.png; sourceTree = ""; }; + DAF109821863F1F900F6A3F9 /* menuicon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menuicon@2x.png"; sourceTree = ""; }; + DAF63B9B1864D0AC0000EAC9 /* FUSEManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUSEManager.h; sourceTree = ""; }; + DAF63B9C1864D0AC0000EAC9 /* FUSEManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FUSEManager.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + DAF109421863EDAF00F6A3F9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DAF109491863EDAF00F6A3F9 /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DAF109631863EDAF00F6A3F9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DAF109691863EDAF00F6A3F9 /* Cocoa.framework in Frameworks */, + DAF109681863EDAF00F6A3F9 /* XCTest.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + DAF1093C1863EDAF00F6A3F9 = { + isa = PBXGroup; + children = ( + DAD59F8B1877CC250018193C /* camput */, + DAABA572186435DA000D62B6 /* camlistored */, + DAABA573186435DA000D62B6 /* cammount */, + DAF1094E1863EDAF00F6A3F9 /* Camlistore */, + DAF1096C1863EDAF00F6A3F9 /* CamlistoreTests */, + DAF109471863EDAF00F6A3F9 /* Frameworks */, + DAF109461863EDAF00F6A3F9 /* Products */, + ); + sourceTree = ""; + }; + DAF109461863EDAF00F6A3F9 /* Products */ = { + isa = PBXGroup; + children = ( + DAF109451863EDAF00F6A3F9 /* Camlistore.app */, + DAF109661863EDAF00F6A3F9 /* CamlistoreTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + DAF109471863EDAF00F6A3F9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + DAF109481863EDAF00F6A3F9 /* Cocoa.framework */, + DAF109671863EDAF00F6A3F9 /* XCTest.framework */, + DAF1094A1863EDAF00F6A3F9 /* Other Frameworks */, + ); + name = Frameworks; + sourceTree = ""; + }; + DAF1094A1863EDAF00F6A3F9 /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + DAF1094B1863EDAF00F6A3F9 /* AppKit.framework */, + DAF1094C1863EDAF00F6A3F9 /* CoreData.framework */, + DAF1094D1863EDAF00F6A3F9 /* Foundation.framework */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + DAF1094E1863EDAF00F6A3F9 /* Camlistore */ = { + isa = PBXGroup; + children = ( + DAF1095A1863EDAF00F6A3F9 /* AppDelegate.h */, + DAF1095B1863EDAF00F6A3F9 /* AppDelegate.m */, + DAF1097C1863EEFB00F6A3F9 /* LoginItemManager.h */, + DAF1097D1863EEFB00F6A3F9 /* LoginItemManager.m */, + DAF1095D1863EDAF00F6A3F9 /* MainMenu.xib */, + DAF109601863EDAF00F6A3F9 /* Images.xcassets */, + DAF1094F1863EDAF00F6A3F9 /* Supporting Files */, + DAF63B9B1864D0AC0000EAC9 /* FUSEManager.h */, + DAF63B9C1864D0AC0000EAC9 /* FUSEManager.m */, + DA31F9351866744D002E3F33 /* TimeTravelWindowController.h */, + DA31F9361866744D002E3F33 /* TimeTravelWindowController.m */, + DA31F9371866744D002E3F33 /* TimeTravelWindowController.xib */, + ); + path = Camlistore; + sourceTree = ""; + }; + DAF1094F1863EDAF00F6A3F9 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + DAABA57618643710000D62B6 /* Credits.html */, + DA71BCB618642A3A000A102C /* Camlicon.icns */, + DAF1097F1863F1F900F6A3F9 /* menuicon-selected.png */, + DAF109801863F1F900F6A3F9 /* menuicon-selected@2x.png */, + DAF109811863F1F900F6A3F9 /* menuicon.png */, + DAF109821863F1F900F6A3F9 /* menuicon@2x.png */, + DAF109501863EDAF00F6A3F9 /* Camlistore-Info.plist */, + DAF109511863EDAF00F6A3F9 /* InfoPlist.strings */, + DAF109541863EDAF00F6A3F9 /* main.m */, + DAF109561863EDAF00F6A3F9 /* Camlistore-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + DAF1096C1863EDAF00F6A3F9 /* CamlistoreTests */ = { + isa = PBXGroup; + children = ( + DAF109721863EDAF00F6A3F9 /* CamlistoreTests.m */, + DAF1096D1863EDAF00F6A3F9 /* Supporting Files */, + ); + path = CamlistoreTests; + sourceTree = ""; + }; + DAF1096D1863EDAF00F6A3F9 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + DAF1096E1863EDAF00F6A3F9 /* CamlistoreTests-Info.plist */, + DAF1096F1863EDAF00F6A3F9 /* InfoPlist.strings */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXLegacyTarget section */ + DAC21F08188B358700EEA8BB /* Camlistore.dmg */ = { + isa = PBXLegacyTarget; + buildArgumentsString = "Camlistore/make-dmg.sh"; + buildConfigurationList = DAC21F0B188B358700EEA8BB /* Build configuration list for PBXLegacyTarget "Camlistore.dmg" */; + buildPhases = ( + ); + buildToolPath = /bin/sh; + buildWorkingDirectory = ""; + dependencies = ( + DAC21F0D188B359300EEA8BB /* PBXTargetDependency */, + ); + name = Camlistore.dmg; + passBuildSettingsInEnvironment = 1; + productName = CamlistoreDMG; + }; +/* End PBXLegacyTarget section */ + +/* Begin PBXNativeTarget section */ + DAF109441863EDAF00F6A3F9 /* Camlistore */ = { + isa = PBXNativeTarget; + buildConfigurationList = DAF109761863EDAF00F6A3F9 /* Build configuration list for PBXNativeTarget "Camlistore" */; + buildPhases = ( + DAF109411863EDAF00F6A3F9 /* Sources */, + DAF109421863EDAF00F6A3F9 /* Frameworks */, + DAF109431863EDAF00F6A3F9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Camlistore; + productName = Camlistore; + productReference = DAF109451863EDAF00F6A3F9 /* Camlistore.app */; + productType = "com.apple.product-type.application"; + }; + DAF109651863EDAF00F6A3F9 /* CamlistoreTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DAF109791863EDAF00F6A3F9 /* Build configuration list for PBXNativeTarget "CamlistoreTests" */; + buildPhases = ( + DAF109621863EDAF00F6A3F9 /* Sources */, + DAF109631863EDAF00F6A3F9 /* Frameworks */, + DAF109641863EDAF00F6A3F9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DAF1096B1863EDAF00F6A3F9 /* PBXTargetDependency */, + ); + name = CamlistoreTests; + productName = CamlistoreTests; + productReference = DAF109661863EDAF00F6A3F9 /* CamlistoreTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DAF1093D1863EDAF00F6A3F9 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0500; + ORGANIZATIONNAME = Camlistore; + TargetAttributes = { + DAF109651863EDAF00F6A3F9 = { + TestTargetID = DAF109441863EDAF00F6A3F9; + }; + }; + }; + buildConfigurationList = DAF109401863EDAF00F6A3F9 /* Build configuration list for PBXProject "Camlistore" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = DAF1093C1863EDAF00F6A3F9; + productRefGroup = DAF109461863EDAF00F6A3F9 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + DAF109441863EDAF00F6A3F9 /* Camlistore */, + DAF109651863EDAF00F6A3F9 /* CamlistoreTests */, + DAC21F08188B358700EEA8BB /* Camlistore.dmg */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + DAF109431863EDAF00F6A3F9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DA31F9391866744D002E3F33 /* TimeTravelWindowController.xib in Resources */, + DAABA574186435DA000D62B6 /* camlistored in Resources */, + DAABA575186435DA000D62B6 /* cammount in Resources */, + DAD59F8C1877CC250018193C /* camput in Resources */, + DAF109531863EDAF00F6A3F9 /* InfoPlist.strings in Resources */, + DAF109841863F1F900F6A3F9 /* menuicon-selected@2x.png in Resources */, + DAF109861863F1F900F6A3F9 /* menuicon@2x.png in Resources */, + DA71BCB718642A3A000A102C /* Camlicon.icns in Resources */, + DAABA57718643710000D62B6 /* Credits.html in Resources */, + DAF109611863EDAF00F6A3F9 /* Images.xcassets in Resources */, + DAF109851863F1F900F6A3F9 /* menuicon.png in Resources */, + DAF109831863F1F900F6A3F9 /* menuicon-selected.png in Resources */, + DAF1098A1863FDD600F6A3F9 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DAF109641863EDAF00F6A3F9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DAF109711863EDAF00F6A3F9 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DAF109411863EDAF00F6A3F9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DA31F9381866744D002E3F33 /* TimeTravelWindowController.m in Sources */, + DAF1097E1863EEFB00F6A3F9 /* LoginItemManager.m in Sources */, + DAF1095C1863EDAF00F6A3F9 /* AppDelegate.m in Sources */, + DAF63B9D1864D0AC0000EAC9 /* FUSEManager.m in Sources */, + DAF109551863EDAF00F6A3F9 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DAF109621863EDAF00F6A3F9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DAF109731863EDAF00F6A3F9 /* CamlistoreTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + DAC21F0D188B359300EEA8BB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DAF109441863EDAF00F6A3F9 /* Camlistore */; + targetProxy = DAC21F0C188B359300EEA8BB /* PBXContainerItemProxy */; + }; + DAF1096B1863EDAF00F6A3F9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DAF109441863EDAF00F6A3F9 /* Camlistore */; + targetProxy = DAF1096A1863EDAF00F6A3F9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + DAF109511863EDAF00F6A3F9 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + DAF109521863EDAF00F6A3F9 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + DAF1095D1863EDAF00F6A3F9 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + DAF1095E1863EDAF00F6A3F9 /* Base */, + ); + name = MainMenu.xib; + sourceTree = ""; + }; + DAF1096F1863EDAF00F6A3F9 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + DAF109701863EDAF00F6A3F9 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + DAC21F09188B358700EEA8BB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUGGING_SYMBOLS = YES; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + DAC21F0A188B358700EEA8BB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + DAF109741863EDAF00F6A3F9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.9; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + DAF109751863EDAF00F6A3F9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.9; + SDKROOT = macosx; + }; + name = Release; + }; + DAF109771863EDAF00F6A3F9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + COMBINE_HIDPI_IMAGES = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Camlistore/Camlistore-Prefix.pch"; + INFOPLIST_FILE = "Camlistore/Camlistore-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + DAF109781863EDAF00F6A3F9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + COMBINE_HIDPI_IMAGES = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Camlistore/Camlistore-Prefix.pch"; + INFOPLIST_FILE = "Camlistore/Camlistore-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + DAF1097A1863EDAF00F6A3F9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Camlistore.app/Contents/MacOS/Camlistore"; + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Camlistore/Camlistore-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "CamlistoreTests/CamlistoreTests-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + DAF1097B1863EDAF00F6A3F9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Camlistore.app/Contents/MacOS/Camlistore"; + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Camlistore/Camlistore-Prefix.pch"; + INFOPLIST_FILE = "CamlistoreTests/CamlistoreTests-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + DAC21F0B188B358700EEA8BB /* Build configuration list for PBXLegacyTarget "Camlistore.dmg" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DAC21F09188B358700EEA8BB /* Debug */, + DAC21F0A188B358700EEA8BB /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; + DAF109401863EDAF00F6A3F9 /* Build configuration list for PBXProject "Camlistore" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DAF109741863EDAF00F6A3F9 /* Debug */, + DAF109751863EDAF00F6A3F9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DAF109761863EDAF00F6A3F9 /* Build configuration list for PBXNativeTarget "Camlistore" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DAF109771863EDAF00F6A3F9 /* Debug */, + DAF109781863EDAF00F6A3F9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DAF109791863EDAF00F6A3F9 /* Build configuration list for PBXNativeTarget "CamlistoreTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DAF1097A1863EDAF00F6A3F9 /* Debug */, + DAF1097B1863EDAF00F6A3F9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = DAF1093D1863EDAF00F6A3F9 /* Project object */; +} diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..ef6ea098 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/AppDelegate.h b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/AppDelegate.h new file mode 100644 index 00000000..3cdc4b48 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/AppDelegate.h @@ -0,0 +1,78 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#import + +#import "LoginItemManager.h" +#import "TimeTravelWindowController.h" +#import "FUSEManager.h" + +#define MIN_LIFETIME 10 + +@interface AppDelegate : NSObject { + NSStatusItem *statusBar; + IBOutlet NSMenu *statusMenu; + + IBOutlet NSMenuItem *launchBrowserItem; + IBOutlet NSMenuItem *launchAtStartupItem; + IBOutlet LoginItemManager *loginItems; + IBOutlet FUSEManager *fuseManager; + IBOutlet NSMenuItem *fuseMountItem; + + NSTask *task; + NSPipe *in, *out; + + BOOL hasSeenStart; + time_t startTime; + + BOOL terminatingApp; + int shutdownWaitEvents; + NSTimer *taskKiller; + + NSString *logPath; + FILE *logFile; + + TimeTravelWindowController *timeTraveler; +} + +- (IBAction)browse:(id)sender; + +- (void)launchServer; +- (void)stop; +- (void)openUI; +- (void)taskTerminated:(NSNotification *)note; +- (void)cleanup; + +- (void)updateAddItemButtonState; + +- (IBAction)setLaunchPref:(id)sender; +- (IBAction)changeLoginItems:(id)sender; + +- (IBAction)showAboutPanel:(id)sender; +- (IBAction)showLogs:(id)sender; +- (IBAction)showTechSupport:(id)sender; + +- (void)applicationWillTerminate:(NSNotification *)notification; +- (IBAction)toggleMount:(id)sender; + +- (void) fuseMounted; +- (void) fuseDismounted; + +- (IBAction)openFinder:(id)sender; +- (IBAction)openFinderAsOf:(id)sender; + + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/AppDelegate.m b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/AppDelegate.m new file mode 100644 index 00000000..cc8027c2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/AppDelegate.m @@ -0,0 +1,382 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#import "AppDelegate.h" +#import "TimeTravelWindowController.h" + +#define FORCEKILL_INTERVAL 15.0 // How long to wait for the server task to exit, on quit + +@implementation AppDelegate + +- (IBAction)showAboutPanel:(id)sender +{ + [NSApp activateIgnoringOtherApps:YES]; + [[NSApplication sharedApplication] orderFrontStandardAboutPanel:sender]; +} + +- (void)logMessage:(NSString*)msg +{ + const char *str = [msg cStringUsingEncoding:NSUTF8StringEncoding]; + if (str) { + fwrite(str, strlen(str), 1, logFile); + } +} + +- (void)flushLog +{ + fflush(logFile); +} + +- (NSString *)logFilePath:(NSString*)logName +{ + NSArray *URLs = [[NSFileManager defaultManager] URLsForDirectory:NSLibraryDirectory + inDomains:NSUserDomainMask]; + NSURL *logsURL = [[URLs lastObject] URLByAppendingPathComponent:@"Logs"]; + NSString *logDir = [logsURL path]; + return [logDir stringByAppendingPathComponent:logName]; +} + +- (void)awakeFromNib +{ + hasSeenStart = NO; + + logPath = [self logFilePath:@"Camlistored.log"]; + const char *logPathC = [logPath cStringUsingEncoding:NSUTF8StringEncoding]; + + NSString *oldLogFileString = [self logFilePath:@"Camlistored.log.old"]; + const char *oldLogPath = [oldLogFileString cStringUsingEncoding:NSUTF8StringEncoding]; + rename(logPathC, oldLogPath); // This will fail the first time. + + // Now our logs go to a private file. + logFile = fopen(logPathC, "w"); + + [NSTimer scheduledTimerWithTimeInterval:1.0 + target:self selector:@selector(flushLog) + userInfo:nil + repeats:YES]; + + [[NSUserDefaults standardUserDefaults] + registerDefaults: [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool:YES], @"browseAtStart", + nil, nil]]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + + statusBar=[[NSStatusBar systemStatusBar] statusItemWithLength: 26.0]; + [statusBar setAlternateImage: [NSImage imageNamed:@"menuicon-selected"]]; + [statusBar setImage: [NSImage imageNamed:@"menuicon"]]; + [statusBar setMenu: statusMenu]; + [statusBar setEnabled:YES]; + [statusBar setHighlightMode:YES]; + + // Fix up the masks for all the alt items. + for (int i = 0; i < [statusMenu numberOfItems]; ++i) { + NSMenuItem *itm = [statusMenu itemAtIndex:i]; + if ([itm isAlternate]) { + [itm setKeyEquivalentModifierMask:NSAlternateKeyMask]; + } + } + + [launchBrowserItem setState:([defaults boolForKey:@"browseAtStart"] ? NSOnState : NSOffState)]; + [self updateAddItemButtonState]; + + [self launchServer]; +} + +- (void)stop +{ + NSFileHandle *writer; + writer = [in fileHandleForWriting]; + [writer closeFile]; + [task terminate]; +} + +- (void)launchServer +{ + in = [[NSPipe alloc] init]; + out = [[NSPipe alloc] init]; + task = [[NSTask alloc] init]; + + startTime = time(NULL); + + NSMutableString *launchPath = [NSMutableString string]; + [launchPath appendString:[[NSBundle mainBundle] resourcePath]]; + [task setCurrentDirectoryPath:launchPath]; + + [launchPath appendString:@"/camlistored"]; + + NSDictionary *env = [NSDictionary dictionaryWithObjectsAndKeys: + NSHomeDirectory(), @"HOME", + NSUserName(), @"USER", + nil, nil]; + [task setEnvironment:env]; + + [self logMessage:[NSString stringWithFormat:@"Launching '%@'\n", launchPath]]; + [task setLaunchPath:launchPath]; + [task setArguments:[NSArray arrayWithObjects:@"-openbrowser=false", nil]]; + [task setStandardInput:in]; + [task setStandardOutput:out]; + [task setStandardError:out]; + + NSFileHandle *fh = [out fileHandleForReading]; + NSNotificationCenter *nc; + nc = [NSNotificationCenter defaultCenter]; + + [nc addObserver:self + selector:@selector(dataReady:) + name:NSFileHandleReadCompletionNotification + object:fh]; + + [nc addObserver:self + selector:@selector(taskTerminated:) + name:NSTaskDidTerminateNotification + object:task]; + + [task launch]; + [fh readInBackgroundAndNotify]; + NSLog(@"Launched server task -- pid = %d", task.processIdentifier); +} + +- (void) shutdownEvent { + shutdownWaitEvents--; + NSLog(@"Received a shutdown event. %d to go", shutdownWaitEvents); + if (shutdownWaitEvents == 0) { + NSLog(@"Received last shutdown event. bye"); + [NSApp replyToApplicationShouldTerminate:NSTerminateNow]; + } +} + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { + NSLog(@"Asking if we should terminate..."); + BOOL isRunning = [task isRunning]; + if (isRunning) { + terminatingApp = YES; + [self stopTask]; + shutdownWaitEvents = 1; + if ([fuseManager isMounted]) { + [fuseManager dismount]; + shutdownWaitEvents++; + } + return NSTerminateLater; + } + return NSTerminateNow; +} + +- (void)applicationWillTerminate:(NSNotification *)notification +{ + NSLog(@"Terminating."); +} + +- (void)stopTask +{ + if (taskKiller) { + return; // Already shutting down. + } + NSLog(@"Telling server task to stop..."); + NSFileHandle *writer; + writer = [in fileHandleForWriting]; + [task terminate]; + [writer closeFile]; + taskKiller = [NSTimer scheduledTimerWithTimeInterval:FORCEKILL_INTERVAL + target:self + selector:@selector(killTask) + userInfo:nil + repeats:NO]; +} + +- (void)killTask +{ + NSLog(@"Force terminating task"); + [task terminate]; +} + +- (void)taskTerminated:(NSNotification *)note +{ + int status = [[note object] terminationStatus]; + NSLog(@"Task terminated with status %d", status); + [self cleanup]; + [self logMessage: [NSString stringWithFormat:@"Terminated with status %d\n", + status]]; + + if (terminatingApp) { + // I was just waiting for the task to exit before quitting + [self shutdownEvent]; + } else { + time_t now = time(NULL); + if (now - startTime < MIN_LIFETIME) { + NSInteger b = NSRunAlertPanel(@"Problem Running Camlistore", + @"camlistored doesn't seem to be operating properly. " + @"Check Console logs for more details.", @"Retry", @"Quit", nil); + if (b == NSAlertAlternateReturn) { + [NSApp terminate:self]; + } + } + + // Relaunch the server task... + [NSTimer scheduledTimerWithTimeInterval:1.0 + target:self selector:@selector(launchServer) + userInfo:nil + repeats:NO]; + } +} + +- (void)cleanup +{ + [taskKiller invalidate]; + taskKiller = nil; + + task = nil; + + in = nil; + out = nil; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)openUI +{ + NSDictionary *info = [[NSBundle mainBundle] infoDictionary]; + NSString *homePage = [info objectForKey:@"HomePage"]; + NSURL *url=[NSURL URLWithString:homePage]; + [[NSWorkspace sharedWorkspace] openURL:url]; +} + +- (IBAction)browse:(id)sender +{ + [self openUI]; +} + +- (void)appendData:(NSData *)d +{ + NSString *s = [[NSString alloc] initWithData: d + encoding: NSUTF8StringEncoding]; + if (!hasSeenStart) { + if ([s rangeOfString:@"Available on http"].location != NSNotFound) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + if ([defaults boolForKey:@"browseAtStart"]) { + [self openUI]; + } + hasSeenStart = YES; + } + } + + [self logMessage:s]; +} + +- (void)dataReady:(NSNotification *)n +{ + NSData *d; + d = [[n userInfo] valueForKey:NSFileHandleNotificationDataItem]; + if ([d length]) { + [self appendData:d]; + } + if (task) { + [[out fileHandleForReading] readInBackgroundAndNotify]; + } +} + +- (IBAction)setLaunchPref:(id)sender { + NSCellStateValue stateVal = [sender state]; + stateVal = (stateVal == NSOnState) ? NSOffState : NSOnState; + + NSLog(@"Setting launch pref to %s", stateVal == NSOnState ? "on" : "off"); + + [[NSUserDefaults standardUserDefaults] + setBool:(stateVal == NSOnState) + forKey:@"browseAtStart"]; + + [launchBrowserItem setState:([[NSUserDefaults standardUserDefaults] + boolForKey:@"browseAtStart"] ? NSOnState : NSOffState)]; + + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (void) updateAddItemButtonState +{ + [launchAtStartupItem setState:[loginItems inLoginItems] ? NSOnState : NSOffState]; +} + +- (IBAction)changeLoginItems:(id)sender +{ + if([sender state] == NSOffState) { + [loginItems addToLoginItems:self]; + } else { + [loginItems removeLoginItem:self]; + } + [self updateAddItemButtonState]; +} + + +- (IBAction)showTechSupport:(id)sender +{ + NSDictionary *info = [[NSBundle mainBundle] infoDictionary]; + NSString *homePage = [info objectForKey:@"SupportPage"]; + NSURL *url=[NSURL URLWithString:homePage]; + [[NSWorkspace sharedWorkspace] openURL:url]; + +} + +- (IBAction)showLogs:(id)sender +{ + if (![[NSWorkspace sharedWorkspace] openFile:logPath]) { + NSRunAlertPanel(@"Cannot Find Logfile", + @"I've been looking for logs in all the wrong places.", nil, nil, nil); + return; + } +} + +- (IBAction)toggleMount:(id)sender { + NSLog(@"Toggling mount"); + if ([fuseManager isMounted]) { + [fuseManager dismount]; + } else { + [fuseManager mount]; + } +} + +- (void) fuseDismounted { + NSLog(@"FUSE dismounted"); + if (terminatingApp) { + [self shutdownEvent]; + } +} + +- (void) fuseMounted { + NSLog(@"FUSE mounted"); +} + +- (IBAction)openFinder:(id)sender +{ + if (![[NSWorkspace sharedWorkspace] openFile:[fuseManager mountPath]]) { + NSRunAlertPanel(@"Cannot Open Finder Window", + @"Can't find mount path or something.", nil, nil, nil); + return; + } +} + +- (IBAction)openFinderAsOf:(id)sender +{ + [NSApp activateIgnoringOtherApps:YES]; + + if (timeTraveler == nil) { + timeTraveler = [[TimeTravelWindowController alloc] + initWithWindowNibName:@"TimeTravelWindowController"]; + [timeTraveler setMountPath:[fuseManager mountPath]]; + } + [timeTraveler showWindow:self]; +} + + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Base.lproj/MainMenu.xib b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..6189cfc4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Base.lproj/MainMenu.xib @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Camlicon.icns b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Camlicon.icns new file mode 100644 index 0000000000000000000000000000000000000000..32017dcdd2e6d3f73ff53d26cbfa72369d2453ed GIT binary patch literal 63675 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Camlistore-Info.plist b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Camlistore-Info.plist new file mode 100644 index 00000000..fcd57eaf --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Camlistore-Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + Camlicon.icns + CFBundleIdentifier + org.camlistore.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSApplicationCategoryType + public.app-category.utilities + LSMinimumSystemVersion + ${MACOSX_DEPLOYMENT_TARGET} + NSHumanReadableCopyright + Copyright © 2013 Camlistore. All rights reserved. + NSMainNibFile + MainMenu + SupportPage + http://groups.google.com/group/camlistore + NSPrincipalClass + NSApplication + LSUIElement + + HomePage + http://localhost:3179/ + + diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Camlistore-Prefix.pch b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Camlistore-Prefix.pch new file mode 100644 index 00000000..35d76409 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Camlistore-Prefix.pch @@ -0,0 +1,9 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#ifdef __OBJC__ + #import +#endif diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Credits.html b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Credits.html new file mode 100644 index 00000000..b64159e3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Credits.html @@ -0,0 +1,26 @@ + + + Credits + + + + +

    Camlistore is your personal storage system for life.

    + +

    It's a way to store, sync, share, model and back up content.

    + +

    It stands for "Content-Addressable Multi-Layer Indexed Storage", for + lack of a better name. For more, see:

    + + + +
    + +

    + File a bug +

    + + diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/FUSEManager.h b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/FUSEManager.h new file mode 100644 index 00000000..65ffed30 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/FUSEManager.h @@ -0,0 +1,47 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#import + +#define MIN_FUSE_LIFETIME 10 + +@protocol FUSEManagerDelegate +- (void) fuseMounted; +- (void) fuseDismounted; +@end + +@interface FUSEManager : NSObject { +@private + BOOL shouldBeMounted; + BOOL mounted; + NSString *mountPoint; + + time_t startTime; + NSTask *task; + NSPipe *in, *out; + + IBOutlet id delegate; + IBOutlet NSMenuItem *mountMenu; +} + +- (NSString *)mountPath; +- (BOOL) isMounted; +- (void) mount; +- (void) dismount; + + + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/FUSEManager.m b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/FUSEManager.m new file mode 100644 index 00000000..7489236f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/FUSEManager.m @@ -0,0 +1,234 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#import + +#import "FUSEManager.h" + +@implementation FUSEManager + +- (BOOL) isMounted +{ + return mounted; +} + +- (void)justMounted +{ + mounted = YES; + [delegate fuseMounted]; + [mountMenu setState:NSOnState]; +} + +- (void)justUnmounted +{ + mounted = NO; + [delegate fuseDismounted]; + [mountMenu setState:NSOffState]; +} + +- (NSString*) mountPath +{ + NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES ); + return [NSString stringWithFormat: @"%@/camlistore", [paths objectAtIndex:0]]; +} + +- (void) mount +{ + shouldBeMounted = YES; + + in = [[NSPipe alloc] init]; + out = [[NSPipe alloc] init]; + task = [[NSTask alloc] init]; + + startTime = time(NULL); + + NSMutableString *launchPath = [NSMutableString string]; + [launchPath appendString:[[NSBundle mainBundle] resourcePath]]; + [task setCurrentDirectoryPath:launchPath]; + + [launchPath appendString:@"/cammount"]; + + NSString *mountDir = [self mountPath]; + [[NSFileManager defaultManager] createDirectoryAtPath:mountDir + withIntermediateDirectories:YES + attributes:nil + error:nil]; + + NSDictionary *env = [NSDictionary dictionaryWithObjectsAndKeys: + NSHomeDirectory(), @"HOME", + NSUserName(), @"USER", + @"/bin:/usr/bin:/sbin:/usr/sbin", @"PATH", + nil, nil]; + [task setEnvironment:env]; + + NSLog(@"Launching '%@'\n", launchPath); + [task setLaunchPath:launchPath]; + [task setArguments:[NSArray arrayWithObjects:@"-open", [self mountPath], nil]]; + [task setStandardInput:in]; + [task setStandardOutput:out]; + [task setStandardError:out]; + + NSFileHandle *fh = [out fileHandleForReading]; + NSNotificationCenter *nc; + nc = [NSNotificationCenter defaultCenter]; + + [nc addObserver:self + selector:@selector(dataReady:) + name:NSFileHandleReadCompletionNotification + object:fh]; + + [nc addObserver:self + selector:@selector(taskTerminated:) + name:NSTaskDidTerminateNotification + object:task]; + + [task launch]; + [fh readInBackgroundAndNotify]; + NSLog(@"Launched server task -- pid = %d", task.processIdentifier); + + [self justMounted]; +} + +- (void)dataReady:(NSNotification *)n +{ + NSData *d; + d = [[n userInfo] valueForKey:NSFileHandleNotificationDataItem]; + if ([d length]) { + NSString *s = [[NSString alloc] initWithData: d + encoding: NSUTF8StringEncoding]; + NSLog(@"%@", s); + } + if (task) { + [[out fileHandleForReading] readInBackgroundAndNotify]; + } +} + +- (void)cleanup +{ + task = nil; + + in = nil; + out = nil; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (BOOL)hasFUSE +{ + return [[NSFileManager defaultManager] fileExistsAtPath:@"/Library/Filesystems/osxfusefs.fs"]; +} + +- (BOOL)hasClientConfig +{ + NSString *confFile = [NSString stringWithFormat:@"%@/.config/camlistore/client-config.json", NSHomeDirectory()]; + return [[NSFileManager defaultManager] fileExistsAtPath:confFile]; +} + +- (void)createClientConfig +{ + NSTask *put = [[NSTask alloc] init]; + + NSMutableString *launchPath = [NSMutableString string]; + [launchPath appendString:[[NSBundle mainBundle] resourcePath]]; + [put setCurrentDirectoryPath:launchPath]; + [launchPath appendString:@"/camput"]; + NSDictionary *env = [NSDictionary dictionaryWithObjectsAndKeys: + NSHomeDirectory(), @"HOME", + NSUserName(), @"USER", + @"/bin:/usr/bin:/sbin:/usr/sbin", @"PATH", + nil, nil]; + [put setEnvironment:env]; + [put setLaunchPath:launchPath]; + [put setArguments:[NSArray arrayWithObjects:@"init", nil]]; + [put launch]; + [put waitUntilExit]; +} + +// If YES is returned, try to remount, otherwise stop +- (BOOL)resolveMountProblemAndRemount +{ + time_t now = time(NULL); + if (now - startTime < MIN_FUSE_LIFETIME) { + // See if we can guide the user to a solution + if (![self hasFUSE]) { + NSRunAlertPanel(@"Problem Mounting Camlistore FUSE", + @"You don't seem to have osxfuse installed. " + @"Please go here, install, and try again:\n\n" + @"http://osxfuse.github.io/", @"OK", nil, nil); + return NO; + } else if (![self hasClientConfig]) { + NSInteger b = NSRunAlertPanel(@"Problem Mounting Camlistore FUSE", + @"You don't have a camlistore client config. " + @"Would you like me to make you one?", + @"Make Client Config", @"Don't Mount", nil); + if (b == NSAlertDefaultReturn) { + [self createClientConfig]; + } else { + return NO; + } + } else { + NSInteger b = NSRunAlertPanel(@"Problem Mounting Camlistore FUSE", + @"I'm having trouble mounting the FUSE filesystem. " + @"Check Console logs for more details.", + @"Retry", @"Don't Mount", nil); + return b == NSAlertDefaultReturn; + } + + } + return YES; +} + +- (void)taskTerminated:(NSNotification *)note +{ + int status = [[note object] terminationStatus]; + NSLog(@"Task terminated with status %d", status); + [self cleanup]; + [self justUnmounted]; + NSLog(@"Terminated with status %d\n", status); + + if (shouldBeMounted) { + // Relaunch the server task... + if ([self resolveMountProblemAndRemount]) { + NSLog(@"Remounting"); + [NSTimer scheduledTimerWithTimeInterval:1.0 + target:self selector:@selector(mount) + userInfo:nil + repeats:NO]; + } else { + NSLog(@"Should no longer be mounted"); + shouldBeMounted = NO; + } + } + if (!shouldBeMounted) { + [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation + source:[[self mountPath] stringByDeletingLastPathComponent] + destination:@"" + files:[NSArray arrayWithObject:[[self mountPath] lastPathComponent]] + tag:nil]; + } +} + +- (void) dismount +{ + NSLog(@"Unmounting"); + shouldBeMounted = NO; + NSFileHandle *writer; + writer = [in fileHandleForWriting]; + [writer writeData:[@"q\n" dataUsingEncoding:NSASCIIStringEncoding]]; + [writer closeFile]; +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Images.xcassets/AppIcon.appiconset/Contents.json b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..2db2b1c7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/LoginItemManager.h b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/LoginItemManager.h new file mode 100644 index 00000000..3a60293f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/LoginItemManager.h @@ -0,0 +1,31 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#import + + +@interface LoginItemManager : NSObject { +@private + +} + +- (BOOL)loginItemExistsWithLoginItemReference:(LSSharedFileListRef)theLoginItemsRefs forPath:(CFURLRef)thePath; + +- (BOOL)inLoginItems; +- (void)removeLoginItem:(id)sender; +- (void)addToLoginItems:(id)sender; + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/LoginItemManager.m b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/LoginItemManager.m new file mode 100644 index 00000000..0b62fd69 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/LoginItemManager.m @@ -0,0 +1,89 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#import "LoginItemManager.h" + + +@implementation LoginItemManager + +- (id)init +{ + self = [super init]; + if (self) { + // Initialization code here. + } + + return self; +} + +- (BOOL)loginItemExistsWithLoginItemReference:(LSSharedFileListRef)theLoginItemsRefs forPath:(CFURLRef)thePath { + BOOL exists = NO; + + return exists; +} + +- (BOOL) inLoginItems { + BOOL exists = NO; + UInt32 seedValue; + + LSSharedFileListRef theLoginItemsRefs = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL); + CFURLRef thePath = (CFURLRef)CFBridgingRetain([[NSBundle mainBundle] bundlePath]); + + // We're going to grab the contents of the shared file list (LSSharedFileListItemRef objects) + // and pop it in an array so we can iterate through it to find our item. + NSArray *loginItemsArray = (NSArray *)CFBridgingRelease(LSSharedFileListCopySnapshot(theLoginItemsRefs, &seedValue)); + for (id item in loginItemsArray) { + LSSharedFileListItemRef itemRef = (LSSharedFileListItemRef)CFBridgingRetain(item); + if (LSSharedFileListItemResolve(itemRef, 0, (CFURLRef*) &thePath, NULL) == noErr) { + if ([[(NSURL *)CFBridgingRelease(thePath) path] hasPrefix:[[NSBundle mainBundle] bundlePath]]) + exists = YES; + } + } + return exists; +} + +- (void) removeLoginItem:(id)sender { + UInt32 seedValue; + + LSSharedFileListRef theLoginItemsRefs = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL); + CFURLRef thePath = (CFURLRef)CFBridgingRetain([[NSBundle mainBundle] bundlePath]); + + // We're going to grab the contents of the shared file list (LSSharedFileListItemRef objects) + // and pop it in an array so we can iterate through it to find our item. + NSArray *loginItemsArray = (NSArray *)CFBridgingRelease(LSSharedFileListCopySnapshot(theLoginItemsRefs, &seedValue)); + for (id item in loginItemsArray) { + LSSharedFileListItemRef itemRef = (LSSharedFileListItemRef)CFBridgingRetain(item); + if (LSSharedFileListItemResolve(itemRef, 0, (CFURLRef*) &thePath, NULL) == noErr) { + if ([[(NSURL *)CFBridgingRelease(thePath) path] hasPrefix:[[NSBundle mainBundle] bundlePath]]) { + LSSharedFileListItemRemove(theLoginItemsRefs, itemRef); + } + } + } +} + +- (void)addToLoginItems:(id)sender { + [self removeLoginItem: self]; + + LSSharedFileListRef theLoginItemsRefs = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL); + + // CFURLRef to the insertable item. + CFURLRef url = (CFURLRef)CFBridgingRetain([NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]]); + + // Actual insertion of an item. + LSSharedFileListInsertItemURL(theLoginItemsRefs, kLSSharedFileListItemLast, NULL, NULL, url, NULL, NULL); +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/TimeTravelWindowController.h b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/TimeTravelWindowController.h new file mode 100644 index 00000000..31bf800c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/TimeTravelWindowController.h @@ -0,0 +1,28 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#import + +@interface TimeTravelWindowController : NSWindowController { + IBOutlet NSDate *when; + NSString *mountPath; +} + +- (void)setMountPath:(NSString*)to; + +- (IBAction)openFinder:(id)sender; + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/TimeTravelWindowController.m b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/TimeTravelWindowController.m new file mode 100644 index 00000000..204a5dc8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/TimeTravelWindowController.m @@ -0,0 +1,61 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#import "TimeTravelWindowController.h" + +@implementation TimeTravelWindowController + +- (void)setMountPath:(NSString*)to +{ + mountPath = to; +} + +- (id)initWithWindow:(NSWindow *)window +{ + self = [super initWithWindow:window]; + if (self) { + // Initialization code here. + } + return self; +} + +- (void)windowDidLoad +{ + [super windowDidLoad]; +} + +- (void)loadWindow { + when = [NSDate date]; + [super loadWindow]; +} + +- (IBAction)openFinder:(id)sender +{ + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + [formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"]; + [[self window] orderOut:self]; + + if (![[NSWorkspace sharedWorkspace] openFile:[NSString stringWithFormat:@"%@/at/%@", + mountPath,[formatter stringFromDate:when]]]) { + NSRunAlertPanel(@"Cannot Open Finder Window", + [NSString stringWithFormat:@"Can't open path for %@.", [formatter stringFromDate:when]], + nil, nil, nil); + return; + } +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/TimeTravelWindowController.xib b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/TimeTravelWindowController.xib new file mode 100644 index 00000000..df6b78cf --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/TimeTravelWindowController.xib @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAC5AAAABAAAABCepkign7sVkKCGKqChmveQ +y4kaoNIj9HDSYSYQ1v50INiArZDa/tGg28CQENzes6DdqayQ3r6VoN+JjpDgnneg4WlwkOJ+WaDjSVKQ +5F47oOUpNJDmR1gg5xJREOgnOiDo8jMQ6gccIOrSFRDr5v4g7LH3EO3G4CDukdkQ76/8oPBxuxDxj96g +8n/BkPNvwKD0X6OQ9U+ioPY/hZD3L4Sg+CiiEPkPZqD6CIQQ+viDIPvoZhD82GUg/chIEP64RyD/qCoQ +AJgpIAGIDBACeAsgA3EokARhJ6AFUQqQBkEJoAcw7JAHjUOgCRDOkAmtvyAK8LCQC+CvoAzZzRANwJGg +DrmvEA+priAQmZEQEYmQIBJ5cxATaXIgFFlVEBVJVCAWOTcQFyk2IBgiU5AZCRggGgI1kBryNKAb4heQ +HNIWoB3B+ZAesfigH6HbkCB2KyAhgb2QIlYNICNq2hAkNe8gJUq8ECYV0SAnKp4QJ/7toCkKgBAp3s+g +KupiECu+saAs036QLZ6ToC6zYJAvfnWgMJNCkDFnkiAycySQM0d0IDRTBpA1J1YgNjLokDcHOCA4HAUQ +OOcaIDn75xA6xvwgO9vJEDywGKA9u6sQPo/6oD+bjRBAb9ygQYSpkEJPvqBDZIuQRC+goEVEbZBF89Mg +Ry2KEEfTtSBJDWwQSbOXIErtThBLnLOgTNZqkE18laBOtkyQT1x3oFCWLpBRPFmgUnYQkFMcO6BUVfKQ +VPwdoFY11JBW5TogWB7xEFjFHCBZ/tMQWqT+IFvetRBchOAgXb6XEF5kwiBfnnkQYE3eoGGHlZBiLcCg +Y2d3kGQNoqBlR1mQZe2EoGcnO5BnzWagaQcdkGmtSKBq5v+Qa5ZlIGzQHBBtdkcgbq/+EG9WKSBwj+AQ +cTYLIHJvwhBzFe0gdE+kEHT/CaB2OMCQdt7roHgYopB4vs2gefiEkHqer6B72GaQfH6RoH24SJB+XnOg +f5gqkAABAAECAwEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA +AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA +AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA +AQABAAEAAQAB//+dkAEA//+PgAAE//+dkAEI//+dkAEMUERUAFBTVABQV1QAUFBUAAAAAAEAAAABA + + + + + + + + + + + + + + + + + + + + + +VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAC5AAAABAAAABCepkign7sVkKCGKqChmveQ +y4kaoNIj9HDSYSYQ1v50INiArZDa/tGg28CQENzes6DdqayQ3r6VoN+JjpDgnneg4WlwkOJ+WaDjSVKQ +5F47oOUpNJDmR1gg5xJREOgnOiDo8jMQ6gccIOrSFRDr5v4g7LH3EO3G4CDukdkQ76/8oPBxuxDxj96g +8n/BkPNvwKD0X6OQ9U+ioPY/hZD3L4Sg+CiiEPkPZqD6CIQQ+viDIPvoZhD82GUg/chIEP64RyD/qCoQ +AJgpIAGIDBACeAsgA3EokARhJ6AFUQqQBkEJoAcw7JAHjUOgCRDOkAmtvyAK8LCQC+CvoAzZzRANwJGg +DrmvEA+priAQmZEQEYmQIBJ5cxATaXIgFFlVEBVJVCAWOTcQFyk2IBgiU5AZCRggGgI1kBryNKAb4heQ +HNIWoB3B+ZAesfigH6HbkCB2KyAhgb2QIlYNICNq2hAkNe8gJUq8ECYV0SAnKp4QJ/7toCkKgBAp3s+g +KupiECu+saAs036QLZ6ToC6zYJAvfnWgMJNCkDFnkiAycySQM0d0IDRTBpA1J1YgNjLokDcHOCA4HAUQ +OOcaIDn75xA6xvwgO9vJEDywGKA9u6sQPo/6oD+bjRBAb9ygQYSpkEJPvqBDZIuQRC+goEVEbZBF89Mg +Ry2KEEfTtSBJDWwQSbOXIErtThBLnLOgTNZqkE18laBOtkyQT1x3oFCWLpBRPFmgUnYQkFMcO6BUVfKQ +VPwdoFY11JBW5TogWB7xEFjFHCBZ/tMQWqT+IFvetRBchOAgXb6XEF5kwiBfnnkQYE3eoGGHlZBiLcCg +Y2d3kGQNoqBlR1mQZe2EoGcnO5BnzWagaQcdkGmtSKBq5v+Qa5ZlIGzQHBBtdkcgbq/+EG9WKSBwj+AQ +cTYLIHJvwhBzFe0gdE+kEHT/CaB2OMCQdt7roHgYopB4vs2gefiEkHqer6B72GaQfH6RoH24SJB+XnOg +f5gqkAABAAECAwEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA +AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA +AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA +AQABAAEAAQAB//+dkAEA//+PgAAE//+dkAEI//+dkAEMUERUAFBTVABQV1QAUFBUAAAAAAEAAAABA + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/en.lproj/InfoPlist.strings b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/en.lproj/InfoPlist.strings new file mode 100644 index 00000000..b92732c7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* Localized versions of Info.plist keys */ diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/main.m b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/main.m new file mode 100644 index 00000000..28545437 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/main.m @@ -0,0 +1,22 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#import + +int main(int argc, const char * argv[]) +{ + return NSApplicationMain(argc, argv); +} diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/make-dmg.sh b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/make-dmg.sh new file mode 100644 index 00000000..9048e3cc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/make-dmg.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# make-dmg.sh +# Created by Dustin Sallings on 2014/1/18. +# Copyright (c) 2014 Camlistore. All rights reserved. + +set -ex + +dir="$TARGET_TEMP_DIR/disk" +dmg="$BUILT_PRODUCTS_DIR/$PROJECT_NAME.dmg" + +rm -rf "$dir" +mkdir -p "$dir" +cp -R "$BUILT_PRODUCTS_DIR/$PROJECT_NAME.app" "$dir" +cp -R "$PROJECT_DIR/../../../README" "$dir/README.txt" +cp -R "$PROJECT_DIR/../../../COPYING" "$dir/LICENSE.txt" +ln -s "/Applications" "$dir/Applications" +rm -f "$dmg" +hdiutil create -srcfolder "$dir" -volname "$PROJECT_NAME" "$dmg" +rm -rf "$dir" diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/menuicon-selected.png b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/menuicon-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..ee3afbb41b941f9e660a29c9e72528126561fcfe GIT binary patch literal 4234 zcmZ`*2{=@3{~lw?zOSK7VnobX6WMo?CF|G+gTbJ&4K=nb*|H~72xUow8f6{(8m~#V zBqUq1r6g-Us_%XK_y68=UC%ksxqtWl+n(n+*NHPW)M23Ip#=Z{40^hnR}P=7M}zwK z;oYMDmL&i{tA^6hFxJ!102^a`Tv1*~06>=^=XQ{p=@@4$nIuPg$tnOA+TaPH0l&Ja z%KH%N0LFWfpjLk?ig1~HEB4i;7F$-f-u7=mZe|KbD~jf6S67hE zmL_m$kEE2HU837b++2l*yVQ@1H5o(vMh?PK1>`gYj{|76hR-X8Lb&FD8K%UJ6H*7D z7aN%(QZbTJocS~bj|FDhE4(&~=g-ym+{uE>=JUY&Pc+EnCe|BI;)SLQ6G8d5cZA-# z6<()8lFan*fVppH2M4$A82}{MvmN>tAo*T#Qd;aMR^vVc0n{1o@(mcqgl&2ETmZJn zZ8jbhr{`3}GI|F!_o3T$XPs!e?Id|FqwVtaO{@WVql*5P1Xp;4&uuLVAvikQJ#~e^ zx3U5snRnlA8k;&5YPzq~9kOe_4p}DWswL*ar@WRQ!$_QfO+o9sCy9-j=71{nan-r$ z$3|;lz~^Its>yp_PQ`fHr?tPQ7p6;CUiR{%$n%7r`yjWQ7wPk&DMtQVcJJ!0#l<2n zm54KX9MVXwxuW8tOPYOs^o&_#m;G< z*HNX7X}KgpF&TlPj{L;TQK0rHh4TSZw)zM&uQheBhMMsOcFGQoxD@6z)$Ny%5^m8O zkMcE)l{U`!^VCt>-PpNL^%5*|e4hdr#c~HYh+udPcpg4hd6AD=r?vN;CYIwK6<724 zw0;ezGGlcs*EH`ZfLOFcYdW=RH#OyewiPw+x~6)Hl^_MnY0p%F2f$J-dTElHCf5oUTG0(KB32ee;I16Dn(&y7sA2XEKuT>3J4c2&gg`vcI$m-FZx*TgGk|s_c2h&NH&iaxy z(?duvOZCiL%DN_GlrfqWl;r@;6LvTqrvJTYuLfw+qS|5_QCMk_uRUGWsuZoKg)hi- zy6J>>igUX0Ube$Ts39#Pt(w)SRCTfPmC(b}JnDlFgJ0EDU9M7igM1@#Qb+E=?qK?2 z#|Hlg(gqDu2x&8Sc8+h3XO8&d%JEhS$1UkACmtrIC)Ot>f03E5CC`w@&sU)A4;n-= zw?(~0uV)Hmx@J;jCOWHcn<0r5b1jCt%1L(7uL#pIw|a*l({!R%h_ z$I806keVHjuWcWdKBn{Fc^Y|6@R;*t$#lw|zpAO%XZ8}A z*{gkKu0q5F`Fuk8O0oEW@`H&oC4zGa(aNpRkl4>=mj<(VEHVslYx&*U=IaHXe zu1{P;;@p^ltJ5`nm{h0>G|>U;n?Gbeo;hQ^w9|KaG-)BQ9o;_h zT4`**X7?P^C8kuJgszO@^aUO=o&C+M3d>Z>it4m)PHP@(#mWlGamq=`Y>k19WjNRt zVjs94xHIu(^wYx5=&ljy0_ZW#2igZ9ZIA_(GMy9+>#+b(TMI=?eI!5Zm4l$|5GPQ5 zPa`l|kw%u@j$_A<8!9IA7_uB(V~!2Q27$z#v@|u^>vfa6Uv=vwBynEi4ASn=>ChV1 zxvX_fD@*HAmX#QsIIEp#od_;c;^gP$xF#5gDgE|p;!^C)@_X8uRj9P>ZQ}A9%(J=D zUUGSgLks+>=qXvmhny;M+E)YGT| z&8&92IA^_bJr2DYppn|McL7fwK1a-!W~5{kib$6Zl>UW^s@kd=R2M?TSDv$zRuU$` z+GGkPH0oZUhF8tm3RDMD4bJ+(UF+}M`fl)5H&O7e&mpa8eV!0$18EwZbgjD+Zsa@+A*!yvnsy6LZtOUY<3T6gSIuXb+| zl6T5Gesaj~hh@9YS3eV+?HSJzuEH(DTU zt~xTIl3kLkq)MKW^ea4chKoNw_!z;HjGrfcyjH!E^QLQjv}p<*Hb{{~*+Hdzik+_^ ztoxg>CqWI}fp|OpxQ)D2@UgDREzBWIb47POm8=u3^RTNmIbitW(5PIAT*mpKG_?QRbjtgizyu^?e3XpmARG6+g-%v;7BVm`$;3G>T_>9EQg{NpRJ)u zi@wDD+cSh)jmK~cJ9fLU$;~?4+^vHgUdn{yJFC#fYX|mkl@@W6`vX*^O!g{$As6=D z_nnt#3WRBmL~|dh5cY?+MqNuu8_V}c9m&L-Zpkq<8nA=R?Fyr|W8_luIqfR#k2(Y$ za^iO4M97nkj!(UbS%WjtO(Nf)f6E*w%L@(KCAm5Ew14s(Sjtw>pDbyV-x=9NFSvY| z9SlOP6&1%%mNlwuM(pSBI&XBPE-o8w8D-`rsRUi8+53!zpk3#uzD0f6l~RC*9{y9( z`0B#_0026+qd@_{-{S@VD2q^LR{mC(4HTSx&`^Ynj}sD#Mf)DI0RTm;!r>H+^hbcP zXfJO+1+0?54~D|w{0N2#fPYZ@J(UEkE*pb2d@x9`EK~+6A)rhP27?tbF0Kk!G_`-p z58spo-2MH16=1NSpde_FG}H&<29uPRmxoD6!K9?b4;kWq!QTD|thl$|nV(Mn>qism z=Zr!5`lEcj!AE`(PCfztN&*5$f&RXJ<%#zFJCL{Euc{8~gJBWAFiEHc?0<>;QLg`_ z_kUDJmVcEYv8aEM9a(r6eS!#U-W0CFRYeq!lEk z6eQ&oVSjM`sqlwd1A|2P`(Vs`e7uy6QCOrG`sfub4VCz*{N4VW^r#92BMb_8*xsWi zDoZND{vY-yAASYt=i?RdgD)rbOYl4Rr$EC8?TbPB`5gt2{l)(s`;-5Ve{*k?|KB2(&Ifw?+VwJy!Qs>({)N9bSRq6GA;ZjNQ!2l z9m14uNG9c)E{;n|gTzbBhD*B$Hp6nc%V1A5$dY4OJnPb3K|R}2v(P%@Fi^JREbKLAk@Fn-GHm+#))PgBDryMW1k&-WA;RwmAE;c2*o+9zyea zQLHH%qR4)zicNiOVn1-}d>#cL z2t%>ME3xP4{tHmCng=BVH;2MG>(@^skY}|?J?Dt;Af5X4$rx8lem+x6}5?~R~2RopD3QO z0_NL2Sg8d<66z>>E?43f#?8aA^xrRcxB(4gUoNUZsPB#z?7;A6z)0gz(aF((*G!*U zkx#7lEIHZ_0I=q#U4sKju|DrvmT$rujn869_C_Ah@CFdJ($S^5Qi-=Z)dszbbX5*j z{+=ldxT!Gx`__by_Y*n_UKkdi!PdmbBF=u~r3eHmf;0i?O`0Ip(2Er5 zRRu(PuYxpr;JyF7KHvM#T650Kx4&=iJ$q)>niHn+K#7c)ffxV)kf|ucv@X8vmyHnr z;ynvUOauUkWl{3-8Y=SgAPpBsE0jGF08l1Ned(d2JxLSVj}^rZTw?=q>@#>1frccM zsezhdDghd}Awit(uq%pGSGgZr>NMdfg|b;FP?0=+Vm?l^OR3S(tO-+Cn8Mf*Z54~` zO(&mqB4#F+s>dg-w|vLz02-%HfGuutoB-hrIMwD_v*EStJ#B}$bd)&caGa)DD=VPV z4;b#qDOM^eDPOsx;$V|Mz_R``Z=(jM>)5$}44bGtJ3fF|Vf2oqA1CbsZk%>SJ3Zta z*u5Xm4G9s1&}0y0zG0hdE44q!TfAN0m6*UepTU6Wqkk`)hN;(>PUe`+!T>W26FI(G zXZa8yu{tWrfQ7?b!^1ybsR9J3l1zG+I5X_SAPQ9Un_;`S0K&L7vG*?I5l3P+X#me$ z>-lhCn2LGs)$v5s!tw{JA;&R)Ivywh1|T{`>@_#Qu2g;%dWfl3@8POKQz3z zDUK<1je>apTsmeUF${N@Rh4DaUOXU;vUK6dZ9QUWTPJod>?*ZV0CwH9i4S{-1WKJZc z0ZKB&A+gjiHXv=;`rmf&q{){uz#M(A7lTVKcG`KvlqGnFJ%DQcKTQm?!ls0@|j} zv-fIfpybspw6iz10ldK`EiVXVJ`my!D#8hG?7-xr;p{k9S?yxjvT+L(lB3D9Z`4LA z$2ia9;fH3JCDPG!(9eY;Ftg?l&Bn~{35+IMBtBH7UA$vODHM40B*%QTJQvROM%ta| z&efDizP5*^QKklE3FUYbX6q7+E4oja*AP)Eg<&4^Qq6U+LdOWab)g$^NOtX{&N z4l?1Oh?UleyTwVReveT>Zt{WHy;>PB887)9EwX%v5qM!@U8=r17Up};7p(L`C}A&Q zuB-S(QH)*uTEb%v^|3%_E8x~y4=Vbu8^!V)h{{$QR%?66sow{~!6v1e zTNzNz^y2jFMx{mr|L&#%&?nOf*_7t|jPQ&`voy1;S&N&6F-3zsS^8NL9d|9NoWpmZ za|WDcVW}#yDv@1k+7lzwI;M5~mHni4i#D9XMI!Z`nPp!Ph&PI3ufjH_-5Q2 zH+&_MjmoBI>L1kiACMogop7EA5=e!5hGNMNC*0g`)d%^GUR_(Bt?Apr%n@)s<|-dn z$(u<<^l|r*^<|Ne(r(j^@X@&pI?vWHy2S8X@?%UqoiavrKgZAMuO0WQjYlrIx1rmn zCZs0MYEEub+^2|9is+2Xd$GjOPh$Kmp;RwMue3V$(0t2wD-S9T4TDBPuQ#|i6#0UG zn4RICxgSsMjc+d4pd=8YYgZltTbps3>z}ZIhfLTFM`&>6 zPUYQ$C5c2xji`@Z>G*kt-*B#b)#!Tqd3pf(%oSkrZS~4gABH|CMMTovr}0qiQfgNi zRZ>%UtdO8^KLO5*sF+v8=wm>+QZy_#s2{VtyA&J_P2CTjTmMEpx5+PL__AXClgqn> zf}Z}8Xp?4yGY|6(7N!}>^RVkJ1q-Sr`zEy>3&8Og^`R%ROC7hDB6o~_AWvg}2|;gz z24M+pMqw5zB`VY^bGYiV@4h~IYqA?SUl11^m&GkqFj(*r6;%17a#)VTEWGTtk&qM@ z7ThYFB_LndjT+t5y`CvE7^8a26=7AM`245pcV!HF3dd6fNrmm(X=^29dbeip)y4h! z5V5OYSmZ(Rg5+mcH`J!=mS|VdR z%IBOr9nZdgmw$Y{XhlOPtM&)*F00tix6U6dTWO?{lV0Jzg)3ZV=?7H<`r-N+V_(L? zs9awC0t#d4UhlhS zHfI*=bD3Pk>O5(M(#2<^KgO*Ju>0$;$4&byo>@nQ)X0O+7oYdgTS+l6N~-Mn3Z+)AL{Yxk-@+&$3052D%19yX~sft@TqWR4%9nF#ao@l2FHUJ>$DSpvKBi+nE zo@jdqS8-1%wqFeKi~c1HW&{1AxY#C?Yrz!% zmS2pd*lgU~oW#Lk4-XH147i+Mfn3xz?00M?U_%0ZHu3ipqW}bWwt~dX3@*h7i zq^pGs%E=Ao=m5I(Yi92F$W4lk?K03m&)<2Xo&E{r;QIHhF6IaJG;;zA@(Y0fKam^C z>fiMKm+I2;Uu8&7)PIp(TK`nYFKYGw8vYc@JK8(CXgir%ATM+K zCHW2dll?0U;=hIY&G5HS5`4Lnf7{uA9h+b1#kqkJU##eV&LEUH?fNV|0D#Y?0+ZE& z6TO3fbkxzMlHU5Lnisb+aP1ZgTFcHRGulQ&CzHe+PT8)Lb^GZ^Q0gGRVU2Dpt|)zy7(#S5}_%3QSjv({_xCpFL|-Fw^O z<`zvkw>pfVAM)c*|sE$}H%@W|V%t&$Qtx|G z)qNTz@4FZ{62*y2rH6fuK?mGpn`O$fpoLA3k1wT)xpvgMEm@O~claAh>AQ%!zgYO1(d!@IBSsk**$pUz@)86;+gYO1`Ha^IWPlU|MLp`Qry< z7zDv)jVluGC)N(x-RQYexz3X%3*~t6T=rg3nYoM3z{{8AL-U(c^~nTsj0{uwO`}cD zIX{PTVm>CdCra2a0D%&>Q&E#s3J;Vl=@GM{*TR&A8Aij4^h&$m*^LWRNT9)tZx0y7 zN1P_Adv_|#Wpeetejl`%Z*5sjR~dMnS?;>@0A+7KLhO)i-JeF?%l)!e+SwxVCRPr| z<`E8(euVY>j#M1mb*e|4TwDv&*vDF7c$;Iz23E`alC6@3J7<<1li)YcDECK9&3O<} z@A>s++!0lkmF%JpUnti#A(E1Byy4DIx|k*YOEB@4Ytz^zs-F zE8eU!CRWTRlZCbp4Ge&ai;E$mqFiTZXSgs-@+bW&Baa+^zmt^5<`gwFyJMk)9~TH= zQIRmV!62VS%=RHmTn{H`aVLML%l@H%R<9jtD1@#6^y0=njUfj$buNz#+V)N9KyyF* zNMOEYsrWLzo7ffO?G23TOD4$Ow(Zq06P&Dv*-aljv|^oIgktCzyW-w~m(5C{ht9$M{SJe1*lEe5sq`+jp!GgoC+u8=@k=zB~FE zjvt3)--#Rqe>KA+4STG4=98Dx{j)6Tdw(Zx7Z6bw$l}+4h*)cx#;CErZ}9oZYj>N_ zhiXI6JELNIP5pqe+&)4=xFFvl*A{JJ=ubjQ!qCNBVl(oS5dN?w{JKEEyQZGxEAD~) zPXaJo&u8%01PmoqkHlQjqRdg$a&a@!vV^Aw>{x0mXUlQ5jwM`4CbN^T%|cQHtU9L0 zmWvbzhi3yE*@joI49Ha(f0G=oP2RZ01=1EgwlR~|NkJ%j*aK|~)3~g;NW_!<-2`Ov z{Psd<}kPY35 z+H{z`{wzqZsIs~`r3X7*%*DxB`JAQ4m6Kw#WYk|Nkdpps^e&%4)q2NP+<1NfNcW+( z_RSqn!yBZ*a&H}HpeS2gEtYb)Y8Co(t~&FnTeSW%(g@y*`};rGYp2YG={MxOHxGk#uAbuJK==M7Fo(^ z>_b9gEZLW=$w!^tzu&z*&wah`8)b4;kC~B&5dZ)%L-n=I4xguv z20iWJ-SYi|>i__w23kwY1gfP4GQoN|qcJD|K%Y7DA>RDjFvnd|y?lN5DFKks22UUZ zs8>m!9cUT_4K<;Tj1czIKBmid@}i}ac@vf1T>(cOHm2L*NHW73t4Uk4skRPfG{&7_ zxkPq-EPcNnK0ZA2l059P989hQn0yNdzQ)0*LS=GbY)kXaHm6R%Yu%#eVx?k%Q8i6C zI|KE$wW<5R)vKhXJ=brm+*|^OIuS?28cl?K1`k3K1mv{@X#tEn19Hl@ggGhH$=531 za7!Hk-)^K_l!}y;;>clmQYdm3)0Ls>dzP{~8h5!k+H2ba@;T%kqlnxvBQq&qXfIhiZ;T4wU{*Hoc z7QjExWhxpN1x4naB&VV&vmMU6tCiPwkdmUwEk+af{0&JPRmUGlaE6w7J=CERf_tR7 zCM=flEiS?br(Jg%hsV#}y0)*<5x8fuD!f3-(um1|k7E{!A@v-9O+l-SGnEY~7Jw=b zT6M}qvGFnpuyzbkHTG!zY$V1mvGwzD5vKbK3m6}&Y(Ga5H2|pI__JU?rg5U_JH6!3MMsBx(+WDfYx~$K~J?n>fm{6R!PQ= zhF7j731vQUQi0!RcKMlXqCF=JH3Y6rZyt|x!U{bRIsP)uF>9C^t4q4ZIpI=S^z^yxzp@ z&2&6ePn|Kc`HBS9SQwfc4ZjcP^K2>nU$yDk-1oIH!kI>N333cEbiRdoLU`5x z)ul1E(0In+(SR#-*2{#;V0qLUt$3$AWw zTq>9ivKKm$sAiIUL72_hfKNwr_^N_It$KiZfYviJ=I5UMu)@^3Oe^Dh?O=moh+dL3 zVVyA9S&~$g;GQy12of?*CKK=kdvLah{kaf>?|I*9s7;&Io3DlCmRsiNPE@^CxdYWn zf0BZ{hfGICA#Z<{ePb&0Dlsha zFKO=02LEjR1_Me6Wlgz2;iK?SD&LyXzLs#frp#Nf`=f}Cd-}fnckUBNuQBvuX43wss>t7tY)h1 zBvR6ry3C!0D(@@gl$0*!i}$EL9W7ERaeQ70bIHA0`Tn$RBG_79QQjUZZu$+oo>N2Z#F_8O_ggqH`J%S%};T$Mb~=MLm~tt#XyxE<65l zBYIVJ(ptDYDifLrjqNnLHq<|6?odanA{}?1b`_Q>k|nl3DgOwE7wZl_R%lV~xS1P} zu_MZ)avyd-lVg;_UTjh9n)tjJKl~#67|f&UOZjfght_m=TgTw`NViGH%J+Gr)72#f z;^Tgei@{3iw&kN7#H&QoCd;P4p75R|oyuMRyY(zvLpZ++#E9U5lk>9^HSbqrCh0_i zL@N5A`Qw@J_ZQzYzt3el&bh+bFV2PSxiL}0hfM%Gfn)6by>t34Mp7oN=6Ab{$gwki ztsbqTLn_1jHG85buAE5FyWgIipEScmVzRqOD7&6;z3gS;7IN8bIbT&#HA*#B^>l+@ zLs2kf8?jHl@3%X;PF|VWCGQynF9VAiW*MIXb%B<2s!UQ0r;hmoTbikwiQ)W^UVA~C zehzBQZ(4qLlo@1?+p_QaaDl~SiiH;fYApP3`Qw4&NF8mhR-%4fM{kGT{aB7G9C+PM zy*D}odPX`yIs~071eh4Sa!NPGDh8CN!oknW9wg|8E!gTEy>fSQ;WOjp5?I>iVdcUH zY&E6e9jP?lz8QYw(pg^qGvllWQKw%QPz*~q>}&B9AUTP6cQ0|KO>`!9)pi^8EdfY~ zcoET~O=z`^a)g#b*`brv#v0Y1d|%kFg-sPC$0z4rlrHEg_!AvbwO!SxDTIhF7qyjE z5vhl?$mB|B)xAXzELog>qTZ8Wc)6z{la^jN>?W7F~m2iMBw={C(;P1A{b@%Pp&m^T=9%;KnXWr^DeLBOu(EArjv z=+DMN9~0-t;lbB;M!Pm67VDpFKHI~8@}tLp9G$CMhovFmcEjDJpKMDgU29L)UJ|b_ zG*}|6Z#bMl$2rASNj-m2Z&2o@H<173>6b8`xb*4zFF`LCGe5MCkQ>K6Li(s;Y2MJO zo@L{E71FU~;$EWR@doj6qPT@L|Kv+uqf3Z=i1woXY63~`j^4BO*KxiBO8sQ{=km#N zuVWM2Dkg8Vd+mSvsugs)XwF1Bw{{zN`JBS)=k{&><*eh%!vWF3g>xeN*_(fKTSZ&t z41OGp`s1k_I><0+d@i&v^UV7*;%XTOH#cH7)6y&7x?CY8yk~!up^%NgwWis2XZ3y* z=Uv6?IMuqUsou=e^3t()VT3Zf)mm)9Nd4Y73qp}gse-LiaAt7*V&s18;8|kUL;HnW z=(*};kNP?9%Ke9vB@Y^o1<&l-?m@;j>w+l{4l;RZ?$horfg6Gj>^`c@1&{6b&=s7p zQ|k)6yzjd2xG?!dgwgm?R-szS{=hcbxuAYy;W62PRC&)OF0w`oa*(o9X54a&R6r8d ztX=(;z&l?nW z2RA)toMxx`@aW~d{OGZw2DQzw{hU3=jrN4O1>&eP*t5$k(W zitt;9-;@mA`fwirfa&znpaP^n;sO9@^3dim9L&g2(b3BTjBxToqQL$h-iK%aK-pjM zaO#1=Awd2f7*8KXe-(ir2*tzskr*NX`T@bYs|di1Oh8&*SQJPWECZGhP-O&xK+0Gr zXGJq@-M{FED-{7(9L`%20>R_)V7xTg3+n=rR8UZWNJv4Xq{I&q;ywYMIE25rr_cGH zM*eF@8|CAOMSJ7WUY?*My9lJ0FHS{3;KnFl)90_O4)cTfBfKG!Uv5p?thBzCZBthmrjt{3`sNpylP^jYawR966Bv3;(O^cltw-o7nHuZy;;@`nO&8w(1GvK_L%~ z^ZO(Mc6`U?0;Sf5XY8*Q(L$e@5#Cx`P6l=Q6 z{6UK@EI#(e{2=FG(xjbDAG7GWR)-86(N?`Oq37*e#^Tb_@Ss*E^J`!+@%;szZjHFE zwC#-#TQg2N0aqU?8D2)*)`d+qE&9>$>R(`EgwZAF9}g@HY%sfc-o%5o=^68XGVSRJJY!Y2D_v3kR+BaL5`SE~nD_r#Ty+s2=PMJUThhS{s((?wy?-5uVSTyu={l<(srDAR5Q6VrT7uRCfs|1{)n7nv7Ww_gMWhaDvaSjFZ9&Y!+ydFbwH-df?q;?e9nKs3G~Nt(ngj z*@uWiFEASkL6k$526`$!!7i{pmCI1i%gb{J3E>|Kp?9lJM`H`jsWZc%M#7ns6-i3X zF5#!7H031Lr=4a$P4tkBY6*&`=}p=~BU%uZqtepS-E%lYC|3nlaZoZ;a2T`csEdq( bKi;RwTdFR4c@Qaj^hSm1T-7esbO`?siEck0 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/menuicon@2x.png b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/Camlistore/menuicon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..545a4a7f85b5491e23d21f45d91f65dc7e770adf GIT binary patch literal 6314 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 + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + org.camlistore.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/CamlistoreTests/CamlistoreTests.m b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/CamlistoreTests/CamlistoreTests.m new file mode 100644 index 00000000..d439db03 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/CamlistoreTests/CamlistoreTests.m @@ -0,0 +1,34 @@ +// +// CamlistoreTests.m +// CamlistoreTests +// +// Created by Dustin Sallings on 12/19/13. +// Copyright (c) 2013 Camlistore. All rights reserved. +// + +#import + +@interface CamlistoreTests : XCTestCase + +@end + +@implementation CamlistoreTests + +- (void)setUp +{ + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown +{ + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testExample +{ + XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); +} + +@end diff --git a/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/CamlistoreTests/en.lproj/InfoPlist.strings b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/CamlistoreTests/en.lproj/InfoPlist.strings new file mode 100644 index 00000000..b92732c7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/osx/Camlistore/CamlistoreTests/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* Localized versions of Info.plist keys */ diff --git a/vendor/github.com/camlistore/camlistore/clients/python/camliclient.py b/vendor/github.com/camlistore/camlistore/clients/python/camliclient.py new file mode 100755 index 00000000..1cf15523 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/clients/python/camliclient.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python +# +# Camlistore uploader client for Python. +# +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Command-line example client for Camlistore.""" + +__author__ = 'Brett Slatkin (bslatkin@gmail.com)' + +import logging +import optparse +import os +import re +import sys + +try: + import camli.op +except ImportError: + sys.path.insert(0, '../../lib/python') + import camli.op + + +def upload_files(op, path_list): + """Uploads a list of files. + + Args: + op: The CamliOp to use. + path_list: The list of file paths to upload. + + Returns: + Exit code. + """ + real_path_set = set([os.path.abspath(path) for path in path_list]) + all_blob_files = [open(path, 'rb') for path in real_path_set] + logging.debug('Uploading blob paths: %r', real_path_set) + op.put_blobs(all_blob_files) + return 0 + + +def upload_dir(op, root_path, recursive=True, ignore_patterns=[r'^\..*']): + """Uploads a directory of files recursively. + + Args: + op: The CamliOp to use. + root_path: The path of the directory to upload. + recursively: If the whole directory and its children should be uploaded. + ignore_patterns: Set of ignore regex expressions. + + Returns: + Exit code. + """ + def should_ignore(dirname): + for pattern in ignore_patterns: + if re.match(pattern, dirname): + return True + return False + + def error(e): + raise e + + all_blob_paths = [] + for dirpath, dirnames, filenames in os.walk(root_path, onerror=error): + allowed_dirnames = [] + for name in dirnames: + if not should_ignore(name): + allowed_dirnames.append(name) + for i in xrange(len(dirnames)): + dirnames.pop(0) + if recursive: + dirnames.extend(allowed_dirnames) + + all_blob_paths.extend(os.path.join(dirpath, name) for name in filenames) + + logging.debug('Uploading dir=%r', root_path) + upload_files(op, all_blob_paths) + return 0 + + +def download_files(op, blobref_list, target_dir): + """Downloads blobs to a target directory. + + Args: + op: The CamliOp to use. + blobref_list: The list of blobrefs to download. + target_dir: The directory to save the downloaded blobrefs in. + + Returns: + Exit code. 1 if there were any missing blobrefs. + """ + all_blobs = set(blobref_list) + found_blobs = set() + + def start_out(blobref): + blob_path = os.path.join(target_dir, blobref) + return open(blob_path, 'wb') + + def end_out(blobref, blob_file): + found_blobs.add(blobref) + blob_file.close() + + op.get_blobs(blobref_list, start_out=start_out, end_out=end_out) + missing_blobs = all_blobs - found_blobs + if missing_blobs: + print >>sys.stderr, 'Missing blobrefs: %s' % ', '.join(missing_blobs) + return 1 + else: + return 0 + + +def main(argv): + usage = \ +"""usage: %prog [options] [command] + +Commands: + put ... [filepathN] + \t\t\tupload a set of specific files + putdir + \t\t\tput all blobs present in a directory recursively + get ... [blobrefN] + \t\t\tget and save blobs to a directory, named as their blobrefs; + \t\t\t(!) files already present will be overwritten""" + parser = optparse.OptionParser(usage=usage) + parser.add_option('-a', '--auth', dest='auth', + default='', + help='username:pasword for HTTP basic authentication') + parser.add_option('-s', '--server', dest='server', + default='localhost:3179', + help='hostname:port to connect to') + parser.add_option('-d', '--debug', dest='debug', + action='store_true', + help='print debug logging') + parser.add_option('-i', '--ignore_patterns', dest="ignore_patterns", + default="", + help='regexp patterns to ignore') + + def error_and_exit(message): + print >>sys.stderr, message, '\n' + parser.print_help() + sys.exit(2) + + opts, args = parser.parse_args(argv[1:]) + if not args: + parser.print_help() + sys.exit(2) + + if opts.debug: + logging.getLogger().setLevel(logging.DEBUG) + + op = camli.op.CamliOp(opts.server, auth=opts.auth, basepath="/bs") + command = args[0].lower() + + if command == 'putdir': + if len(args) < 2: + error_and_exit('Must supply at least a directory to put') + return upload_dir(op, args[1], opts.ignore_patterns) + elif command == 'put': + if len(args) < 2: + error_and_exit('Must supply one or more file paths to upload') + return upload_files(op, args[1:]) + elif command == 'get': + if len(args) < 3: + error_and_exit('Must supply one or more blobrefs to download ' + 'and a directory to save them to') + return download_files(op, args[1:-1], args[-1]) + else: + error_and_exit('Unknown command: %s' % command) + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/vendor/github.com/camlistore/camlistore/cmd/camdeploy/camdeploy.go b/vendor/github.com/camlistore/camlistore/cmd/camdeploy/camdeploy.go new file mode 100644 index 00000000..b1636a44 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camdeploy/camdeploy.go @@ -0,0 +1,27 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// The camdeploy program deploys Camlistore on cloud computing platforms such as Google +// Compute Engine or Amazon EC2. +package main + +import ( + "camlistore.org/pkg/cmdmain" +) + +func main() { + cmdmain.Main() +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camdeploy/gce.go b/vendor/github.com/camlistore/camlistore/cmd/camdeploy/gce.go new file mode 100644 index 00000000..690220b0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camdeploy/gce.go @@ -0,0 +1,155 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "strings" + + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/context" + "camlistore.org/pkg/deploy/gce" + "camlistore.org/pkg/oauthutil" + + "golang.org/x/oauth2" +) + +type gceCmd struct { + project string + zone string + machine string + instName string + hostname string + certFile string + keyFile string + sshPub string + verbose bool +} + +func init() { + cmdmain.RegisterCommand("gce", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(gceCmd) + flags.StringVar(&cmd.project, "project", "", "Name of Project.") + flags.StringVar(&cmd.zone, "zone", gce.Zone, "GCE zone.") + flags.StringVar(&cmd.machine, "machine", gce.Machine, "e.g. n1-standard-1, f1-micro, g1-small") + flags.StringVar(&cmd.instName, "instance_name", gce.InstanceName, "Name of VM instance.") + flags.StringVar(&cmd.hostname, "hostname", "", "Hostname for the instance and self-signed certificates. Must be given if generating self-signed certs.") + flags.StringVar(&cmd.certFile, "cert", "", "Certificate file for TLS. A self-signed one will be generated if this flag is omitted.") + flags.StringVar(&cmd.keyFile, "key", "", "Key file for the TLS certificate. Must be given with --cert") + flags.StringVar(&cmd.sshPub, "ssh_public_key", "", "SSH public key file to authorize. Can modify later in Google's web UI anyway.") + flags.BoolVar(&cmd.verbose, "verbose", false, "Be verbose.") + return cmd + }) +} + +const ( + clientIdDat = "client-id.dat" + clientSecretDat = "client-secret.dat" + helpEnableAuth = `Enable authentication: in your project console, navigate to "APIs and auth", "Credentials", click on "Create new Client ID", and pick "Installed application", with type "Other". Copy the CLIENT ID to ` + clientIdDat + `, and the CLIENT SECRET to ` + clientSecretDat +) + +func (c *gceCmd) Describe() string { + return "Deploy Camlistore on Google Compute Engine." +} + +func (c *gceCmd) Usage() { + fmt.Fprintf(os.Stderr, "Usage:\n\n %s\n %s\n\n", + "camdeploy gce --project= --hostname= [options]", + "camdeploy gce --project= --cert= --key= [options]") + flag.PrintDefaults() + fmt.Fprintln(os.Stderr, "\nTo get started:\n") + printHelp() +} + +func printHelp() { + for _, v := range []string{gce.HelpCreateProject, helpEnableAuth, gce.HelpEnableAPIs} { + fmt.Fprintf(os.Stderr, "%v\n", v) + } +} + +func (c *gceCmd) RunCommand(args []string) error { + if c.verbose { + gce.Verbose = true + } + if c.project == "" { + return cmdmain.UsageError("Missing --project flag.") + } + if (c.certFile == "") != (c.keyFile == "") { + return cmdmain.UsageError("--cert and --key must both be given together.") + } + if c.certFile == "" && c.hostname == "" { + return cmdmain.UsageError("Either --hostname, or --cert & --key must provided.") + } + config := gce.NewOAuthConfig(readFile(clientIdDat), readFile(clientSecretDat)) + config.RedirectURL = "urn:ietf:wg:oauth:2.0:oob" + + instConf := &gce.InstanceConf{ + Name: c.instName, + Project: c.project, + Machine: c.machine, + Zone: c.zone, + CertFile: c.certFile, + KeyFile: c.keyFile, + Hostname: c.hostname, + } + if c.sshPub != "" { + instConf.SSHPub = strings.TrimSpace(readFile(c.sshPub)) + } + + depl := &gce.Deployer{ + Client: oauth2.NewClient(oauth2.NoContext, oauth2.ReuseTokenSource(nil, &oauthutil.TokenSource{ + Config: config, + CacheFile: c.project + "-token.json", + AuthCode: func() string { + fmt.Println("Get auth code from:") + fmt.Printf("%v\n", config.AuthCodeURL("my-state", oauth2.AccessTypeOffline, oauth2.ApprovalForce)) + fmt.Println("Enter auth code:") + sc := bufio.NewScanner(os.Stdin) + sc.Scan() + return strings.TrimSpace(sc.Text()) + }, + })), + Conf: instConf, + } + inst, err := depl.Create(context.TODO()) + if err != nil { + return err + } + + log.Printf("Instance is up at %s", inst.NetworkInterfaces[0].AccessConfigs[0].NatIP) + return nil +} + +func readFile(v string) string { + slurp, err := ioutil.ReadFile(v) + if err != nil { + if os.IsNotExist(err) { + msg := fmt.Sprintf("%v does not exist.", v) + if v == clientIdDat || v == clientSecretDat { + msg = fmt.Sprintf("%v\n%s", msg, helpEnableAuth) + } + log.Fatal(msg) + } + log.Fatalf("Error reading %s: %v", v, err) + } + return strings.TrimSpace(string(slurp)) +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camget/.gitignore b/vendor/github.com/camlistore/camlistore/cmd/camget/.gitignore new file mode 100644 index 00000000..c464bd61 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camget/.gitignore @@ -0,0 +1,2 @@ +*.[568] +camget diff --git a/vendor/github.com/camlistore/camlistore/cmd/camget/camget.go b/vendor/github.com/camlistore/camlistore/cmd/camget/camget.go new file mode 100644 index 00000000..36c33ce2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camget/camget.go @@ -0,0 +1,418 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/buildinfo" + "camlistore.org/pkg/cacher" + "camlistore.org/pkg/client" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/index" + "camlistore.org/pkg/legal/legalprint" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/types" +) + +var ( + flagVersion = flag.Bool("version", false, "show version") + flagVerbose = flag.Bool("verbose", false, "be verbose") + flagHTTP = flag.Bool("verbose_http", false, "show HTTP request summaries") + flagCheck = flag.Bool("check", false, "just check for the existence of listed blobs; returning 0 if all are present") + flagOutput = flag.String("o", "-", "Output file/directory to create. Use -f to overwrite.") + flagGraph = flag.Bool("graph", false, "Output a graphviz directed graph .dot file of the provided root schema blob, to be rendered with 'dot -Tsvg -o graph.svg graph.dot'") + flagContents = flag.Bool("contents", false, "If true and the target blobref is a 'bytes' or 'file' schema blob, the contents of that file are output instead.") + flagShared = flag.String("shared", "", "If non-empty, the URL of a \"share\" blob. The URL will be used as the root of future fetches. Only \"haveref\" shares are currently supported.") + flagTrustedCert = flag.String("cert", "", "If non-empty, the fingerprint (20 digits lowercase prefix of the SHA256 of the complete certificate) of the TLS certificate we trust for the share URL. Requires --shared.") + flagInsecureTLS = flag.Bool("insecure", false, "If set, when using TLS, the server's certificates verification is disabled, and they are not checked against the trustedCerts in the client configuration either.") + flagSkipIrregular = flag.Bool("skip_irregular", false, "If true, symlinks, device files, and other special file types are skipped.") +) + +func main() { + client.AddFlags() + flag.Parse() + + if *flagVersion { + fmt.Fprintf(os.Stderr, "camget version: %s\n", buildinfo.Version()) + return + } + + if legalprint.MaybePrint(os.Stderr) { + return + } + + if *flagGraph && flag.NArg() != 1 { + log.Fatalf("The --graph option requires exactly one parameter.") + } + + var cl *client.Client + var items []blob.Ref + + if *flagShared != "" { + if client.ExplicitServer() != "" { + log.Fatal("Can't use --shared with an explicit blobserver; blobserver is implicit from the --shared URL.") + } + if flag.NArg() != 0 { + log.Fatal("No arguments permitted when using --shared") + } + cl1, target, err := client.NewFromShareRoot(*flagShared, + client.OptionInsecure(*flagInsecureTLS), + client.OptionTrustedCert(*flagTrustedCert)) + if err != nil { + log.Fatal(err) + } + cl = cl1 + items = append(items, target) + } else { + if *flagTrustedCert != "" { + log.Fatal("Can't use --cert without --shared.") + } + cl = client.NewOrFail() + for n := 0; n < flag.NArg(); n++ { + arg := flag.Arg(n) + br, ok := blob.Parse(arg) + if !ok { + log.Fatalf("Failed to parse argument %q as a blobref.", arg) + } + items = append(items, br) + } + } + + cl.InsecureTLS = *flagInsecureTLS + tr := cl.TransportForConfig(&client.TransportConfig{ + Verbose: *flagHTTP, + }) + httpStats, _ := tr.(*httputil.StatsTransport) + cl.SetHTTPClient(&http.Client{Transport: tr}) + + diskCacheFetcher, err := cacher.NewDiskCache(cl) + if err != nil { + log.Fatalf("Error setting up local disk cache: %v", err) + } + defer diskCacheFetcher.Clean() + if *flagVerbose { + log.Printf("Using temp blob cache directory %s", diskCacheFetcher.Root) + } + + for _, br := range items { + if *flagGraph { + printGraph(diskCacheFetcher, br) + return + } + if *flagCheck { + // TODO: do HEAD requests checking if the blobs exists. + log.Fatal("not implemented") + return + } + if *flagOutput == "-" { + var rc io.ReadCloser + var err error + if *flagContents { + rc, err = schema.NewFileReader(diskCacheFetcher, br) + if err == nil { + rc.(*schema.FileReader).LoadAllChunks() + } + } else { + rc, err = fetch(diskCacheFetcher, br) + } + if err != nil { + log.Fatal(err) + } + defer rc.Close() + if _, err := io.Copy(os.Stdout, rc); err != nil { + log.Fatalf("Failed reading %q: %v", br, err) + } + } else { + if err := smartFetch(diskCacheFetcher, *flagOutput, br); err != nil { + log.Fatal(err) + } + } + } + + if *flagVerbose { + log.Printf("HTTP requests: %d\n", httpStats.Requests()) + } +} + +func fetch(src blob.Fetcher, br blob.Ref) (r io.ReadCloser, err error) { + if *flagVerbose { + log.Printf("Fetching %s", br.String()) + } + r, _, err = src.Fetch(br) + if err != nil { + return nil, fmt.Errorf("Failed to fetch %s: %s", br, err) + } + return r, err +} + +// A little less than the sniffer will take, so we don't truncate. +const sniffSize = 900 * 1024 + +// smartFetch the things that blobs point to, not just blobs. +func smartFetch(src blob.Fetcher, targ string, br blob.Ref) error { + rc, err := fetch(src, br) + if err != nil { + return err + } + rcc := types.NewOnceCloser(rc) + defer rcc.Close() + + sniffer := index.NewBlobSniffer(br) + _, err = io.CopyN(sniffer, rc, sniffSize) + if err != nil && err != io.EOF { + return err + } + + sniffer.Parse() + b, ok := sniffer.SchemaBlob() + + if !ok { + if *flagVerbose { + log.Printf("Fetching opaque data %v into %q", br, targ) + } + + // opaque data - put it in a file + f, err := os.Create(targ) + if err != nil { + return fmt.Errorf("opaque: %v", err) + } + defer f.Close() + body, _ := sniffer.Body() + r := io.MultiReader(bytes.NewReader(body), rc) + _, err = io.Copy(f, r) + return err + } + rcc.Close() + + switch b.Type() { + case "directory": + dir := filepath.Join(targ, b.FileName()) + if *flagVerbose { + log.Printf("Fetching directory %v into %s", br, dir) + } + if err := os.MkdirAll(dir, b.FileMode()); err != nil { + return err + } + if err := setFileMeta(dir, b); err != nil { + log.Print(err) + } + entries, ok := b.DirectoryEntries() + if !ok { + return fmt.Errorf("bad entries blobref in dir %v", b.BlobRef()) + } + return smartFetch(src, dir, entries) + case "static-set": + if *flagVerbose { + log.Printf("Fetching directory entries %v into %s", br, targ) + } + + // directory entries + const numWorkers = 10 + type work struct { + br blob.Ref + errc chan<- error + } + members := b.StaticSetMembers() + workc := make(chan work, len(members)) + defer close(workc) + for i := 0; i < numWorkers; i++ { + go func() { + for wi := range workc { + wi.errc <- smartFetch(src, targ, wi.br) + } + }() + } + var errcs []<-chan error + for _, mref := range members { + errc := make(chan error, 1) + errcs = append(errcs, errc) + workc <- work{mref, errc} + } + for _, errc := range errcs { + if err := <-errc; err != nil { + return err + } + } + return nil + case "file": + fr, err := schema.NewFileReader(src, br) + if err != nil { + return fmt.Errorf("NewFileReader: %v", err) + } + fr.LoadAllChunks() + defer fr.Close() + + name := filepath.Join(targ, b.FileName()) + + if fi, err := os.Stat(name); err == nil && fi.Size() == fr.Size() { + if *flagVerbose { + log.Printf("Skipping %s; already exists.", name) + } + return nil + } + + if *flagVerbose { + log.Printf("Writing %s to %s ...", br, name) + } + + f, err := os.Create(name) + if err != nil { + return fmt.Errorf("file type: %v", err) + } + defer f.Close() + if _, err := io.Copy(f, fr); err != nil { + return fmt.Errorf("Copying %s to %s: %v", br, name, err) + } + if err := setFileMeta(name, b); err != nil { + log.Print(err) + } + return nil + case "symlink": + if *flagSkipIrregular { + return nil + } + sf, ok := b.AsStaticFile() + if !ok { + return errors.New("blob is not a static file") + } + sl, ok := sf.AsStaticSymlink() + if !ok { + return errors.New("blob is not a symlink") + } + name := filepath.Join(targ, sl.FileName()) + if _, err := os.Lstat(name); err == nil { + if *flagVerbose { + log.Printf("Skipping creating symbolic link %s: A file with that name exists", name) + } + return nil + } + target := sl.SymlinkTargetString() + if target == "" { + return errors.New("symlink without target") + } + + // On Windows, os.Symlink isn't yet implemented as of Go 1.3. + // See https://code.google.com/p/go/issues/detail?id=5750 + err := os.Symlink(target, name) + // We won't call setFileMeta for a symlink because: + // the permissions of a symlink do not matter and Go's + // os.Chtimes always dereferences (does not act on the + // symlink but its target). + return err + case "fifo": + if *flagSkipIrregular { + return nil + } + name := filepath.Join(targ, b.FileName()) + + sf, ok := b.AsStaticFile() + if !ok { + return errors.New("blob is not a static file") + } + _, ok = sf.AsStaticFIFO() + if !ok { + return errors.New("blob is not a static FIFO") + } + + if _, err := os.Lstat(name); err == nil { + log.Printf("Skipping FIFO %s: A file with that name already exists", name) + return nil + } + + err = osutil.Mkfifo(name, 0600) + if err == osutil.ErrNotSupported { + log.Printf("Skipping FIFO %s: Unsupported filetype", name) + return nil + } + if err != nil { + return fmt.Errorf("%s: osutil.Mkfifo(): %v", name, err) + } + + if err := setFileMeta(name, b); err != nil { + log.Print(err) + } + + return nil + + case "socket": + if *flagSkipIrregular { + return nil + } + name := filepath.Join(targ, b.FileName()) + + sf, ok := b.AsStaticFile() + if !ok { + return errors.New("blob is not a static file") + } + _, ok = sf.AsStaticSocket() + if !ok { + return errors.New("blob is not a static socket") + } + + if _, err := os.Lstat(name); err == nil { + log.Printf("Skipping socket %s: A file with that name already exists", name) + return nil + } + + err = osutil.Mksocket(name) + if err == osutil.ErrNotSupported { + log.Printf("Skipping socket %s: Unsupported filetype", name) + return nil + } + if err != nil { + return fmt.Errorf("%s: %v", name, err) + } + + if err := setFileMeta(name, b); err != nil { + log.Print(err) + } + + return nil + + default: + return errors.New("unknown blob type: " + b.Type()) + } + panic("unreachable") +} + +func setFileMeta(name string, blob *schema.Blob) error { + err1 := os.Chmod(name, blob.FileMode()) + var err2 error + if mt := blob.ModTime(); !mt.IsZero() { + err2 = os.Chtimes(name, mt, mt) + } + // TODO: we previously did os.Chown here, but it's rarely wanted, + // then the schema.Blob refactor broke it, so it's gone. + // Add it back later once we care? + for _, err := range []error{err1, err2} { + if err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camget/doc.go b/vendor/github.com/camlistore/camlistore/cmd/camget/doc.go new file mode 100644 index 00000000..9f516644 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camget/doc.go @@ -0,0 +1,39 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +The camget tool fetches blobs, files, and directories. + + +Examples + +Writes to stdout by default: + + camget // dump raw blob + camget -contents // dump file contents + +Like curl, lets you set output file/directory with -o: + + camget -o + (if exists and is directory, must be a directory; + use -f to overwrite any files) + + camget -o + +Camget isn't very fleshed out. In general, using 'cammount' to just +mount a tree is an easier way to get files back. +*/ +package main diff --git a/vendor/github.com/camlistore/camlistore/cmd/camget/graph.go b/vendor/github.com/camlistore/camlistore/cmd/camget/graph.go new file mode 100644 index 00000000..4db9f59f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camget/graph.go @@ -0,0 +1,156 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "io" + "log" + "strings" + "sync" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/index" + "camlistore.org/pkg/schema" +) + +func check(err error) { + if err != nil { + log.Fatal(err) + } +} + +type node struct { + br blob.Ref + g *graph + + size int64 + blob *schema.Blob + edges []blob.Ref +} + +func (n *node) dotName() string { + return strings.Replace(n.br.String(), "-", "_", -1) +} + +func (n *node) dotLabel() string { + name := n.displayName() + if n.blob == nil { + return fmt.Sprintf("%s\n%d bytes", name, n.size) + } + return name + "\n" + n.blob.Type() +} + +func (n *node) color() string { + if n.br == n.g.root { + return "#a0ffa0" + } + if n.blob == nil { + return "#aaaaaa" + } + return "#a0a0ff" +} + +func (n *node) displayName() string { + s := n.br.String() + s = s[strings.Index(s, "-")+1:] + return s[:7] +} + +func (n *node) load() { + defer n.g.wg.Done() + rc, err := fetch(n.g.src, n.br) + check(err) + defer rc.Close() + sniff := index.NewBlobSniffer(n.br) + n.size, err = io.Copy(sniff, rc) + check(err) + sniff.Parse() + blob, ok := sniff.SchemaBlob() + if !ok { + return + } + n.blob = blob + for _, part := range blob.ByteParts() { + n.addEdge(part.BlobRef) + n.addEdge(part.BytesRef) + } +} + +func (n *node) addEdge(dst blob.Ref) { + if !dst.Valid() { + return + } + n.g.startLoadNode(dst) + n.edges = append(n.edges, dst) +} + +type graph struct { + src blob.Fetcher + root blob.Ref + + mu sync.Mutex // guards n + n map[string]*node + + wg sync.WaitGroup +} + +func (g *graph) startLoadNode(br blob.Ref) { + g.mu.Lock() + defer g.mu.Unlock() + key := br.String() + if _, ok := g.n[key]; ok { + return + } + n := &node{ + g: g, + br: br, + } + g.n[key] = n + g.wg.Add(1) + go n.load() +} + +func printGraph(src blob.Fetcher, root blob.Ref) { + g := &graph{ + src: src, + root: root, + n: make(map[string]*node), + } + g.startLoadNode(root) + g.wg.Wait() + fmt.Println("digraph G {") + fmt.Println(" node [fontsize=10,fontname=Arial]") + fmt.Println(" edge [fontsize=10,fontname=Arial]") + + for _, n := range g.n { + fmt.Printf("\n %s [label=%q,style=filled,fillcolor=%q]\n", n.dotName(), n.dotLabel(), n.color()) + for i, e := range n.edges { + // TODO: create an edge type. + // Also, this edgeLabel is specific to file parts. Other schema + // types might not even have a concept of ordering. This is hack. + edgeLabel := fmt.Sprintf("%d", i) + if i == 0 { + edgeLabel = "first" + } else if i == len(n.edges)-1 { + edgeLabel = "last" + } + fmt.Printf(" %s -> %s [label=%q]\n", n.dotName(), g.n[e.String()].dotName(), edgeLabel) + } + } + fmt.Printf("}\n") +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/cammount/.gitignore b/vendor/github.com/camlistore/camlistore/cmd/cammount/.gitignore new file mode 100644 index 00000000..dc711e52 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/cammount/.gitignore @@ -0,0 +1,2 @@ +cammount +_go_.[568] diff --git a/vendor/github.com/camlistore/camlistore/cmd/cammount/cammount.go b/vendor/github.com/camlistore/camlistore/cmd/cammount/cammount.go new file mode 100644 index 00000000..e02604a1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/cammount/cammount.go @@ -0,0 +1,251 @@ +// +build linux darwin + +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "os/signal" + "path/filepath" + "runtime" + "strings" + "syscall" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/cacher" + "camlistore.org/pkg/client" + "camlistore.org/pkg/fs" + "camlistore.org/pkg/legal/legalprint" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/search" + "camlistore.org/third_party/bazil.org/fuse" + fusefs "camlistore.org/third_party/bazil.org/fuse/fs" +) + +var ( + debug = flag.Bool("debug", false, "print debugging messages.") + xterm = flag.Bool("xterm", false, "Run an xterm in the mounted directory. Shut down when xterm ends.") + term = flag.Bool("term", false, "Open a terminal window. Doesn't shut down when exited. Mostly for demos.") + open = flag.Bool("open", false, "Open a GUI window") +) + +func usage() { + fmt.Fprint(os.Stderr, "usage: cammount [opts] [ [||]]\n") + flag.PrintDefaults() + os.Exit(2) +} + +func main() { + var conn *fuse.Conn + + // Scans the arg list and sets up flags + client.AddFlags() + flag.Usage = usage + flag.Parse() + + if legalprint.MaybePrint(os.Stderr) { + return + } + + narg := flag.NArg() + if narg > 2 { + usage() + } + + var mountPoint string + var err error + if narg > 0 { + mountPoint = flag.Arg(0) + } else { + mountPoint, err = ioutil.TempDir("", "cammount") + if err != nil { + log.Fatal(err) + } + log.Printf("No mount point given. Using: %s", mountPoint) + defer os.Remove(mountPoint) + } + + errorf := func(msg string, args ...interface{}) { + fmt.Fprintf(os.Stderr, msg, args...) + fmt.Fprint(os.Stderr, "\n") + usage() + } + + var ( + cl *client.Client + root blob.Ref // nil if only one arg + camfs *fs.CamliFileSystem + ) + if narg == 2 { + rootArg := flag.Arg(1) + // not trying very hard since NewFromShareRoot will do it better with a regex + if strings.HasPrefix(rootArg, "http://") || + strings.HasPrefix(rootArg, "https://") { + if client.ExplicitServer() != "" { + errorf("Can't use an explicit blobserver with a share URL; the blobserver is implicit from the share URL.") + } + var err error + cl, root, err = client.NewFromShareRoot(rootArg) + if err != nil { + log.Fatal(err) + } + } else { + cl = client.NewOrFail() // automatic from flags + cl.SetHTTPClient(&http.Client{Transport: cl.TransportForConfig(nil)}) + + var ok bool + root, ok = blob.Parse(rootArg) + + if !ok { + // not a blobref, check for root name instead + req := &search.WithAttrRequest{N: 1, Attr: "camliRoot", Value: rootArg} + wres, err := cl.GetPermanodesWithAttr(req) + + if err != nil { + log.Fatal("could not query search") + } + + if wres.WithAttr != nil { + root = wres.WithAttr[0].Permanode + } else { + log.Fatalf("root specified is not a blobref or name of a root: %q\n", rootArg) + } + } + } + } else { + cl = client.NewOrFail() // automatic from flags + cl.SetHTTPClient(&http.Client{Transport: cl.TransportForConfig(nil)}) + } + + diskCacheFetcher, err := cacher.NewDiskCache(cl) + if err != nil { + log.Fatalf("Error setting up local disk cache: %v", err) + } + defer diskCacheFetcher.Clean() + if root.Valid() { + var err error + camfs, err = fs.NewRootedCamliFileSystem(cl, diskCacheFetcher, root) + if err != nil { + log.Fatalf("Error creating root with %v: %v", root, err) + } + } else { + camfs = fs.NewDefaultCamliFileSystem(cl, diskCacheFetcher) + } + + if *debug { + fuse.Debug = func(msg interface{}) { log.Print(msg) } + // TODO: set fs's logger + } + + // This doesn't appear to work on OS X: + sigc := make(chan os.Signal, 1) + + conn, err = fuse.Mount(mountPoint, fuse.VolumeName(filepath.Base(mountPoint))) + if err != nil { + if err.Error() == "cannot find load_fusefs" && runtime.GOOS == "darwin" { + log.Fatal("FUSE not available; install from http://osxfuse.github.io/") + } + log.Fatalf("Mount: %v", err) + } + + xtermDone := make(chan bool, 1) + if *xterm { + cmd := exec.Command("xterm") + cmd.Dir = mountPoint + if err := cmd.Start(); err != nil { + log.Printf("Error starting xterm: %v", err) + } else { + go func() { + cmd.Wait() + xtermDone <- true + }() + defer cmd.Process.Kill() + } + } + if *open { + if runtime.GOOS == "darwin" { + go exec.Command("open", mountPoint).Run() + } + } + if *term { + if runtime.GOOS == "darwin" { + if osutil.DirExists("/Applications/iTerm.app/") { + go exec.Command("open", "-a", "iTerm", mountPoint).Run() + } else { + log.Printf("TODO: iTerm not installed. Figure out how to open with Terminal.app instead.") + } + } + } + + signal.Notify(sigc, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) + + doneServe := make(chan error, 1) + go func() { + doneServe <- fusefs.Serve(conn, camfs) + }() + + quitKey := make(chan bool, 1) + go awaitQuitKey(quitKey) + + select { + case err := <-doneServe: + log.Printf("conn.Serve returned %v", err) + + // check if the mount process has an error to report + <-conn.Ready + if err := conn.MountError; err != nil { + log.Printf("conn.MountError: %v", err) + } + case sig := <-sigc: + log.Printf("Signal %s received, shutting down.", sig) + case <-quitKey: + log.Printf("Quit key pressed. Shutting down.") + case <-xtermDone: + log.Printf("xterm done") + } + + time.AfterFunc(2*time.Second, func() { + os.Exit(1) + }) + log.Printf("Unmounting...") + err = fs.Unmount(mountPoint) + log.Printf("Unmount = %v", err) + + log.Printf("cammount FUSE process ending.") +} + +func awaitQuitKey(done chan<- bool) { + var buf [1]byte + for { + _, err := os.Stdin.Read(buf[:]) + if err != nil { + return + } + if buf[0] == 'q' { + done <- true + return + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/cammount/cammount_other.go b/vendor/github.com/camlistore/camlistore/cmd/cammount/cammount_other.go new file mode 100644 index 00000000..4db2d389 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/cammount/cammount_other.go @@ -0,0 +1,27 @@ +// +build !linux,!darwin + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import ( + "log" + "runtime" +) + +func main() { + log.Fatalln("cammount not implemented on", runtime.GOOS) +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/cammount/doc.go b/vendor/github.com/camlistore/camlistore/cmd/cammount/doc.go new file mode 100644 index 00000000..e30e7ea9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/cammount/doc.go @@ -0,0 +1,30 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +The cammount tool mounts a root directory blob onto the given mountpoint. The blobref can be given directly or through a share blob URL. If no root blobref is given, an automatic root is created instead. + + +Usage: + + cammount [opts] [|] + -debug=false: print debugging messages. + -server="": Camlistore server prefix. + If blank, the default from the "server" field of ~/.camlistore/config is used. + Acceptable forms: https://you.example.com, example.com:1345 (https assumed), or + http://you.example.com/alt-root +*/ +package main diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/.gitignore b/vendor/github.com/camlistore/camlistore/cmd/camput/.gitignore new file mode 100644 index 00000000..3189f004 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/.gitignore @@ -0,0 +1,5 @@ +*.8 +*.6 +camput + + diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/androidx.go b/vendor/github.com/camlistore/camlistore/cmd/camput/androidx.go new file mode 100644 index 00000000..8b08aa51 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/androidx.go @@ -0,0 +1,42 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Hacks for running camput as a child process on Android. + +package main + +import ( + "camlistore.org/pkg/client/android" +) + +type allStats struct { + total, skipped, uploaded stats +} + +var lastStatBroadcast allStats + +func printAndroidCamputStatus(t *TreeUpload) { + bcast := allStats{t.total, t.skipped, t.uploaded} + if bcast == lastStatBroadcast { + return + } + lastStatBroadcast = bcast + + android.Printf("STATS nfile=%d nbyte=%d skfile=%d skbyte=%d upfile=%d upbyte=%d\n", + t.total.files, t.total.bytes, + t.skipped.files, t.skipped.bytes, + t.uploaded.files, t.uploaded.bytes) +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/attr.go b/vendor/github.com/camlistore/camlistore/cmd/camput/attr.go new file mode 100644 index 00000000..859617af --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/attr.go @@ -0,0 +1,103 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/schema" +) + +type attrCmd struct { + add bool + del bool + up *Uploader +} + +func init() { + cmdmain.RegisterCommand("attr", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(attrCmd) + flags.BoolVar(&cmd.add, "add", false, `Adds attribute (e.g. "tag")`) + flags.BoolVar(&cmd.del, "del", false, "Deletes named attribute [value]") + return cmd + }) +} + +func (c *attrCmd) Describe() string { + return "Add, set, or delete a permanode's attribute." +} + +func (c *attrCmd) Usage() { + cmdmain.Errorf("Usage: camput [globalopts] attr [attroption] ") +} + +func (c *attrCmd) Examples() []string { + return []string{ + " Set attribute", + "--add Adds attribute (e.g. \"tag\")", + "--del [] Deletes named attribute", + } +} + +func (c *attrCmd) RunCommand(args []string) error { + if err := c.checkArgs(args); err != nil { + return err + } + permanode, attr := args[0], args[1] + value := "" + if len(args) > 2 { + value = args[2] + } + + pn, ok := blob.Parse(permanode) + if !ok { + return fmt.Errorf("Error parsing blobref %q", permanode) + } + claimFunc := func() func(blob.Ref, string, string) *schema.Builder { + switch { + case c.add: + return schema.NewAddAttributeClaim + case c.del: + return schema.NewDelAttributeClaim + default: + return schema.NewSetAttributeClaim + } + }() + bb := claimFunc(pn, attr, value) + put, err := getUploader().UploadAndSignBlob(bb) + handleResult(bb.Type(), put, err) + return nil +} + +func (c *attrCmd) checkArgs(args []string) error { + if c.del { + if c.add { + return cmdmain.UsageError("Add and del options are exclusive") + } + if len(args) < 2 { + return cmdmain.UsageError("Attr -del takes at least 2 args: []") + } + return nil + } + if len(args) != 3 { + return cmdmain.UsageError("Attr takes 3 args: ") + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/blobs.go b/vendor/github.com/camlistore/camlistore/cmd/camput/blobs.go new file mode 100644 index 00000000..b7242b5c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/blobs.go @@ -0,0 +1,157 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "crypto/sha1" + "errors" + "flag" + "fmt" + "io" + "os" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/client" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/constants" +) + +type blobCmd struct{} + +func init() { + cmdmain.RegisterCommand("blob", func(flags *flag.FlagSet) cmdmain.CommandRunner { + return new(blobCmd) + }) +} + +func (c *blobCmd) Describe() string { + return "Upload raw blob(s)." +} + +func (c *blobCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, "Usage: camput [globalopts] blob \n camput [globalopts] blob -\n") +} + +func (c *blobCmd) Examples() []string { + return []string{ + " (raw, without any metadata)", + "- (read from stdin)", + } +} + +func (c *blobCmd) RunCommand(args []string) error { + if len(args) == 0 { + return errors.New("No files given.") + } + + up := getUploader() + for _, arg := range args { + var ( + handle *client.UploadHandle + err error + ) + if arg == "-" { + handle, err = stdinBlobHandle() + } else { + handle, err = fileBlobHandle(up, arg) + } + if err != nil { + return err + } + put, err := up.Upload(handle) + handleResult("blob", put, err) + continue + } + return nil +} + +func stdinBlobHandle() (uh *client.UploadHandle, err error) { + var buf bytes.Buffer + size, err := io.CopyN(&buf, cmdmain.Stdin, constants.MaxBlobSize+1) + if err == io.EOF { + err = nil + } + if err != nil { + return + } + if size > constants.MaxBlobSize { + err = fmt.Errorf("blob size cannot be bigger than %d", constants.MaxBlobSize) + } + file := buf.Bytes() + h := blob.NewHash() + size, err = io.Copy(h, bytes.NewReader(file)) + if err != nil { + return + } + return &client.UploadHandle{ + BlobRef: blob.RefFromHash(h), + Size: uint32(size), + Contents: io.LimitReader(bytes.NewReader(file), size), + }, nil +} + +func fileBlobHandle(up *Uploader, path string) (uh *client.UploadHandle, err error) { + fi, err := up.stat(path) + if err != nil { + return + } + if fi.Mode()&os.ModeType != 0 { + return nil, fmt.Errorf("%q is not a regular file", path) + } + file, err := up.open(path) + if err != nil { + return + } + ref, size, err := blobDetails(file) + if err != nil { + return nil, err + } + return &client.UploadHandle{ + BlobRef: ref, + Size: size, + Contents: io.LimitReader(file, int64(size)), + }, nil +} + +func blobDetails(contents io.ReadSeeker) (bref blob.Ref, size uint32, err error) { + s1 := sha1.New() + if _, err = contents.Seek(0, 0); err != nil { + return + } + defer func() { + if _, seekErr := contents.Seek(0, 0); seekErr != nil { + if err == nil { + err = seekErr + } else { + err = fmt.Errorf("%s, cannot seek back: %v", err, seekErr) + } + } + }() + sz, err := io.CopyN(s1, contents, constants.MaxBlobSize+1) + if err == nil || err == io.EOF { + bref, err = blob.RefFromHash(s1), nil + } else { + err = fmt.Errorf("error reading contents: %v", err) + return + } + if sz > constants.MaxBlobSize { + err = fmt.Errorf("blob size cannot be bigger than %d", constants.MaxBlobSize) + } + size = uint32(sz) + return +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/cache.go b/vendor/github.com/camlistore/camlistore/cmd/camput/cache.go new file mode 100644 index 00000000..fc446dca --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/cache.go @@ -0,0 +1,50 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "os" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/client" +) + +// A HaveCache tracks whether a remove blobserver has a blob or not. +type HaveCache interface { + StatBlobCache(br blob.Ref) (size uint32, ok bool) + NoteBlobExists(br blob.Ref, size uint32) + Close() error +} + +// UploadCache is the "stat cache" for regular files. Given a current +// working directory, possibly relative filename, stat info, and +// whether that file was uploaded with a permanode (-filenodes), +// returns what the ultimate put result (the top-level "file" schema +// blob) for that regular file was. +type UploadCache interface { + // CachedPutResult looks in the cache for the put result for the file + // that was uploaded. If withPermanode, it is only a hit if a planned + // permanode for the file was created and uploaded too, and vice-versa. + // The returned PutResult is always for the "file" schema blob. + CachedPutResult(pwd, filename string, fi os.FileInfo, withPermanode bool) (*client.PutResult, error) + // AddCachedPutResult stores in the cache the put result for the file that + // was uploaded. If withPermanode, it means a planned permanode was created + // for this file when it was uploaded (with -filenodes), and the cache entry + // will reflect that. + AddCachedPutResult(pwd, filename string, fi os.FileInfo, pr *client.PutResult, withPermanode bool) + Close() error +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/camput.go b/vendor/github.com/camlistore/camlistore/cmd/camput/camput.go new file mode 100644 index 00000000..fd80a8c2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/camput.go @@ -0,0 +1,187 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "sync" + + "camlistore.org/pkg/blobserver/dir" + "camlistore.org/pkg/client" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/syncutil" +) + +const buffered = 16 // arbitrary + +var ( + flagProxyLocal = false + flagHTTP = flag.Bool("verbose_http", false, "show HTTP request summaries") + flagHaveCache = true + flagBlobDir = flag.String("blobdir", "", "If non-empty, the local directory to put blobs, instead of sending them over the network. If the string \"discard\", no blobs are written or sent over the network anywhere.") +) + +var ( + uploaderOnce sync.Once + uploader *Uploader // initialized by getUploader +) + +var debugFlagOnce sync.Once + +func registerDebugFlags() { + flag.BoolVar(&flagProxyLocal, "proxy_local", false, "If true, the HTTP_PROXY environment is also used for localhost requests. This can be helpful during debugging.") + flag.BoolVar(&flagHaveCache, "havecache", true, "Use the 'have cache', a cache keeping track of what blobs the remote server should already have from previous uploads.") +} + +func init() { + if debug, _ := strconv.ParseBool(os.Getenv("CAMLI_DEBUG")); debug { + debugFlagOnce.Do(registerDebugFlags) + } + cmdmain.ExtraFlagRegistration = client.AddFlags + cmdmain.PreExit = func() { + if up := uploader; up != nil { + up.Close() + stats := up.Stats() + if *cmdmain.FlagVerbose { + log.Printf("Client stats: %s", stats.String()) + if up.transport != nil { + log.Printf(" #HTTP reqs: %d", up.transport.Requests()) + } + } + } + + // So multiple cmd/camput TestFoo funcs run, each with + // an fresh (and not previously closed) Uploader: + uploader = nil + uploaderOnce = sync.Once{} + } +} + +func getUploader() *Uploader { + uploaderOnce.Do(initUploader) + return uploader +} + +func initUploader() { + up := newUploader() + if flagHaveCache && *flagBlobDir == "" { + gen, err := up.StorageGeneration() + if err != nil { + log.Printf("WARNING: not using local server inventory cache; failed to retrieve server's storage generation: %v", err) + } else { + up.haveCache = NewKvHaveCache(gen) + up.Client.SetHaveCache(up.haveCache) + } + } + uploader = up +} + +func handleResult(what string, pr *client.PutResult, err error) error { + if err != nil { + cmdmain.Errorf("Error putting %s: %s\n", what, err) + cmdmain.ExitWithFailure = true + return err + } + fmt.Fprintln(cmdmain.Stdout, pr.BlobRef.String()) + return nil +} + +func getenvEitherCase(k string) string { + if v := os.Getenv(strings.ToUpper(k)); v != "" { + return v + } + return os.Getenv(strings.ToLower(k)) +} + +// proxyFromEnvironment is similar to http.ProxyFromEnvironment but it skips +// $NO_PROXY blacklist so it proxies every requests, including localhost +// requests. +func proxyFromEnvironment(req *http.Request) (*url.URL, error) { + proxy := getenvEitherCase("HTTP_PROXY") + if proxy == "" { + return nil, nil + } + proxyURL, err := url.Parse(proxy) + if err != nil || proxyURL.Scheme == "" { + if u, err := url.Parse("http://" + proxy); err == nil { + proxyURL = u + err = nil + } + } + if err != nil { + return nil, fmt.Errorf("invalid proxy address %q: %v", proxy, err) + } + return proxyURL, nil +} + +func newUploader() *Uploader { + var cc *client.Client + var httpStats *httputil.StatsTransport + if d := *flagBlobDir; d != "" { + ss, err := dir.New(d) + if err != nil && d == "discard" { + ss = discardStorage{} + err = nil + } + if err != nil { + log.Fatalf("Error using dir %s as storage: %v", d, err) + } + cc = client.NewStorageClient(ss) + } else { + cc = client.NewOrFail() + proxy := http.ProxyFromEnvironment + if flagProxyLocal { + proxy = proxyFromEnvironment + } + tr := cc.TransportForConfig( + &client.TransportConfig{ + Proxy: proxy, + Verbose: *flagHTTP, + }) + httpStats, _ = tr.(*httputil.StatsTransport) + cc.SetHTTPClient(&http.Client{Transport: tr}) + } + if *cmdmain.FlagVerbose { + cc.SetLogger(log.New(cmdmain.Stderr, "", log.LstdFlags)) + } else { + cc.SetLogger(nil) + } + + pwd, err := os.Getwd() + if err != nil { + log.Fatalf("os.Getwd: %v", err) + } + + return &Uploader{ + Client: cc, + transport: httpStats, + pwd: pwd, + fdGate: syncutil.NewGate(100), // gate things that waste fds, assuming a low system limit + } +} + +func main() { + cmdmain.Main() +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/camput_test.go b/vendor/github.com/camlistore/camlistore/cmd/camput/camput_test.go new file mode 100644 index 00000000..2af08960 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/camput_test.go @@ -0,0 +1,247 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "camlistore.org/pkg/cmdmain" +) + +// env is the environment that a camput test runs within. +type env struct { + // stdin is the standard input, or /dev/null if nil + stdin io.Reader + + // Timeout optionally specifies the timeout on the command. + Timeout time.Duration + + // TODO(bradfitz): vfs files. +} + +func (e *env) timeout() time.Duration { + if e.Timeout != 0 { + return e.Timeout + } + return 15 * time.Second + +} +func (e *env) Run(args ...string) (out, err []byte, exitCode int) { + outbuf := new(bytes.Buffer) + errbuf := new(bytes.Buffer) + os.Args = append(os.Args[:1], args...) + cmdmain.Stdout, cmdmain.Stderr = outbuf, errbuf + if e.stdin == nil { + cmdmain.Stdin = strings.NewReader("") + } else { + cmdmain.Stdin = e.stdin + } + exitc := make(chan int, 1) + cmdmain.Exit = func(code int) { + exitc <- code + runtime.Goexit() + } + go func() { + cmdmain.Main() + cmdmain.Exit(0) + }() + select { + case exitCode = <-exitc: + case <-time.After(e.timeout()): + panic("timeout running command") + } + out = outbuf.Bytes() + err = errbuf.Bytes() + return +} + +// TestUsageOnNoargs tests that we output a usage message when given no args, and return +// with a non-zero exit status. +func TestUsageOnNoargs(t *testing.T) { + var e env + out, err, code := e.Run() + if code != 1 { + t.Errorf("exit code = %d; want 1", code) + } + if len(out) != 0 { + t.Errorf("wanted nothing on stdout; got:\n%s", out) + } + if !bytes.Contains(err, []byte("Usage: camput")) { + t.Errorf("stderr doesn't contain usage. Got:\n%s", err) + } +} + +// TestCommandUsage tests that we output a command-specific usage message and return +// with a non-zero exit status. +func TestCommandUsage(t *testing.T) { + var e env + out, err, code := e.Run("attr") + if code != 1 { + t.Errorf("exit code = %d; want 1", code) + } + if len(out) != 0 { + t.Errorf("wanted nothing on stdout; got:\n%s", out) + } + sub := "Attr takes 3 args: " + if !bytes.Contains(err, []byte(sub)) { + t.Errorf("stderr doesn't contain substring %q. Got:\n%s", sub, err) + } +} + +func TestUploadingChangingDirectory(t *testing.T) { + // TODO(bradfitz): + // $ mkdir /tmp/somedir + // $ cp dev-camput /tmp/somedir + // $ ./dev-camput -file /tmp/somedir/ 2>&1 | tee /tmp/somedir/log + // ... verify it doesn't hang. + t.Logf("TODO") +} + +func testWithTempDir(t *testing.T, fn func(tempDir string)) { + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Errorf("error creating temp dir: %v", err) + return + } + defer os.RemoveAll(tempDir) + + confDir := filepath.Join(tempDir, "conf") + mustMkdir(t, confDir, 0700) + defer os.Setenv("CAMLI_CONFIG_DIR", os.Getenv("CAMLI_CONFIG_DIR")) + os.Setenv("CAMLI_CONFIG_DIR", confDir) + if err := ioutil.WriteFile(filepath.Join(confDir, "client-config.json"), []byte("{}"), 0644); err != nil { + t.Fatal(err) + } + + debugFlagOnce.Do(registerDebugFlags) + + fn(tempDir) +} + +// Tests that uploads of deep directory trees don't deadlock. +// See commit ee4550bff453526ebae460da1ad59f6e7f3efe77 for backstory +func TestUploadDirectories(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + testWithTempDir(t, func(tempDir string) { + uploadRoot := filepath.Join(tempDir, "to_upload") // read from here + mustMkdir(t, uploadRoot, 0700) + + blobDestDir := filepath.Join(tempDir, "blob_dest") // write to here + mustMkdir(t, blobDestDir, 0700) + + // There are 10 stat cache workers. Simulate a slow lookup in + // the file-based ones (similar to reality), so the + // directory-based nodes make it to the upload worker first + // (where it would currently/previously deadlock waiting on + // children that are starved out) See + // ee4550bff453526ebae460da1ad59f6e7f3efe77. + testHookStatCache = func(el interface{}, ok bool) { + if !ok { + return + } + if ok && strings.HasSuffix(el.(*node).fullPath, ".txt") { + time.Sleep(50 * time.Millisecond) + } + } + defer func() { testHookStatCache = nil }() + + dirIter := uploadRoot + for i := 0; i < 2; i++ { + dirPath := filepath.Join(dirIter, "dir") + mustMkdir(t, dirPath, 0700) + for _, baseFile := range []string{"file.txt", "FILE.txt"} { + filePath := filepath.Join(dirPath, baseFile) + if err := ioutil.WriteFile(filePath, []byte("some file contents "+filePath), 0600); err != nil { + t.Fatalf("error writing to %s: %v", filePath, err) + } + t.Logf("Wrote file %s", filePath) + } + dirIter = dirPath + } + + // Now set statCacheWorkers greater than uploadWorkers, so the + // sleep above can re-arrange the order that files get + // uploaded in, so the directory comes before the file. This + // was the old deadlock. + defer setAndRestore(&uploadWorkers, 1)() + defer setAndRestore(&dirUploadWorkers, 1)() + defer setAndRestore(&statCacheWorkers, 5)() + + e := &env{ + Timeout: 5 * time.Second, + } + stdout, stderr, exit := e.Run( + "--blobdir="+blobDestDir, + "--havecache=false", + "--verbose=false", // useful to set true for debugging + "file", + uploadRoot) + if exit != 0 { + t.Fatalf("Exit status %d: stdout=[%s], stderr=[%s]", exit, stdout, stderr) + } + }) +} + +func TestCamputBlob(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + testWithTempDir(t, func(tempDir string) { + blobDestDir := filepath.Join(tempDir, "blob_dest") // write to here + mustMkdir(t, blobDestDir, 0700) + + e := &env{ + Timeout: 5 * time.Second, + stdin: strings.NewReader("foo"), + } + stdout, stderr, exit := e.Run( + "--blobdir="+blobDestDir, + "--havecache=false", + "--verbose=false", // useful to set true for debugging + "blob", "-") + if exit != 0 { + t.Fatalf("Exit status %d: stdout=[%s], stderr=[%s]", exit, stdout, stderr) + } + if got, want := string(stdout), "sha1-0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33\n"; got != want { + t.Errorf("Stdout = %q; want %q", got, want) + } + }) +} + +func mustMkdir(t *testing.T, fn string, mode int) { + if err := os.Mkdir(fn, 0700); err != nil { + t.Errorf("error creating dir %s: %v", fn, err) + } +} + +func setAndRestore(dst *int, v int) func() { + old := *dst + *dst = v + return func() { *dst = old } +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/delete.go b/vendor/github.com/camlistore/camlistore/cmd/camput/delete.go new file mode 100644 index 00000000..4efa0b7f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/delete.go @@ -0,0 +1,70 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/schema" +) + +type deleteCmd struct { + up *Uploader +} + +func init() { + cmdmain.RegisterCommand("delete", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(deleteCmd) + return cmd + }) +} + +func (c *deleteCmd) Describe() string { + return "Create and upload a delete claim." +} + +func (c *deleteCmd) Usage() { + cmdmain.Errorf("Usage: camput [globalopts] delete [blobref2]...") +} + +func (c *deleteCmd) RunCommand(args []string) error { + if len(args) < 1 { + return cmdmain.UsageError("Need at least one blob to delete.") + } + if err := delete(args); err != nil { + return err + } + return nil +} + +func delete(args []string) error { + for _, arg := range args { + br, ok := blob.Parse(arg) + if !ok { + return fmt.Errorf("Error parsing blobref %q", arg) + } + bb := schema.NewDeleteClaim(br) + put, err := getUploader().UploadAndSignBlob(bb) + if err := handleResult(bb.Type(), put, err); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/discard.go b/vendor/github.com/camlistore/camlistore/cmd/camput/discard.go new file mode 100644 index 00000000..5bee98cf --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/discard.go @@ -0,0 +1,48 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "io" + "io/ioutil" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/context" +) + +type discardStorage struct { + blobserver.NoImplStorage +} + +func (discardStorage) ReceiveBlob(br blob.Ref, r io.Reader) (sb blob.SizedRef, err error) { + n, err := io.Copy(ioutil.Discard, r) + return blob.SizedRef{br, uint32(n)}, err +} + +func (discardStorage) StatBlobs(dest chan<- blob.SizedRef, blobs []blob.Ref) error { + return nil +} + +func (discardStorage) EnumerateBlobs(ctx *context.Context, dest chan<- blob.SizedRef, after string, limit int) error { + defer close(dest) + return nil +} + +func (discardStorage) RemoveBlobs(blobs []blob.Ref) error { + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/doc.go b/vendor/github.com/camlistore/camlistore/cmd/camput/doc.go new file mode 100644 index 00000000..991c0a5f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/doc.go @@ -0,0 +1,74 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +The camput tool mainly pushes blobs, files, and directories. It can also perform various related tasks, such as setting tags, creating permanodes, and creating share blobs. + + +Usage: + + camput [globalopts] [commandopts] [commandargs] + +Modes: + + delete: Create and upload a delete claim. + attr: Add, set, or delete a permanode's attribute. + file: Upload file(s). + init: Initialize the camput configuration file. With no option, it tries to use the GPG key found in the default identity secret ring. + permanode: Create and upload a permanode. + rawobj: Upload a custom JSON schema blob. + share: Grant access to a resource by making a "share" blob. + blob: Upload raw blob(s). + +Examples: + + camput file [opts] (raw, without any metadata) + camput blob - (read from stdin) + + camput permanode (create a new permanode) + camput permanode -name="Some Name" -tag=foo,bar (with attributes added) + + camput init + camput init --gpgkey=XXXXX + + camput share [opts] + + camput rawobj (debug command) + + camput attr Set attribute + camput attr --add Adds attribute (e.g. "tag") + camput attr --del [] Deletes named attribute [value + +For mode-specific help: + + camput -help + +Global options: + -help=false: print usage + -secret-keyring="~/.gnupg/secring.gpg": GnuPG secret keyring file to use. + -server="": Camlistore server prefix. If blank, the default from the "server" field of + ~/.camlistore/config is used. + Acceptable forms: https://you.example.com, example.com:1345 (https assumed), + or http://you.example.com/alt-root + -verbose=false: extra debug logging + -verbose_http=false: show HTTP request summaries + -version=false: show version +*/ +package main diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/files.go b/vendor/github.com/camlistore/camlistore/cmd/camput/files.go new file mode 100644 index 00000000..5290136d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/files.go @@ -0,0 +1,1198 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "crypto/sha1" + "errors" + "flag" + "fmt" + "hash" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "camlistore.org/internal/chanworker" + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + statspkg "camlistore.org/pkg/blobserver/stats" + "camlistore.org/pkg/client" + "camlistore.org/pkg/client/android" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/schema" +) + +type fileCmd struct { + title string + tag string + + makePermanode bool // make new, unique permanode of the root (dir or file) + filePermanodes bool // make planned permanodes for each file (based on their digest) + vivify bool + exifTime bool // use metadata (such as in EXIF) to find the creation time of the file + capCtime bool // use mtime as creation time of the file, if it would be bigger than modification time + diskUsage bool // show "du" disk usage only (dry run mode), don't actually upload + argsFromInput bool // Android mode: filenames piped into stdin, one at a time. + deleteAfterUpload bool // with fileNodes, deletes the input file once uploaded + contentsOnly bool // do not store any of the file's attributes, only its contents. + + statcache bool + + // Go into in-memory stats mode only; doesn't actually upload. + memstats bool + histo string // optional histogram output filename +} + +var flagUseSQLiteChildCache bool // Use sqlite for the statcache and havecache. + +var ( + uploadWorkers = 5 // concurrent upload workers (negative means unbounded: memory hog) + dirUploadWorkers = 3 // concurrent directory uploading workers + statCacheWorkers = 5 // concurrent statcache workers +) + +func init() { + cmdmain.RegisterCommand("file", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(fileCmd) + flags.BoolVar(&cmd.makePermanode, "permanode", false, "Create an associate a new permanode for the uploaded file or directory.") + flags.BoolVar(&cmd.filePermanodes, "filenodes", false, "Create (if necessary) content-based permanodes for each uploaded file.") + flags.BoolVar(&cmd.deleteAfterUpload, "delete_after_upload", false, "If using -filenodes, deletes files once they're uploaded, or if they've already been uploaded.") + flags.BoolVar(&cmd.vivify, "vivify", false, + "If true, ask the server to create and sign permanode(s) associated with each uploaded"+ + " file. This permits the server to have your signing key. Used mostly with untrusted"+ + " or at-risk clients, such as phones.") + flags.BoolVar(&cmd.exifTime, "exiftime", false, "Try to use metadata (such as EXIF) to get a stable creation time. If found, used as the replacement for the modtime. Mainly useful with vivify or filenodes.") + flags.StringVar(&cmd.title, "title", "", "Optional title attribute to set on permanode when using -permanode.") + flags.StringVar(&cmd.tag, "tag", "", "Optional tag(s) to set on permanode when using -permanode or -filenodes. Single value or comma separated.") + + flags.BoolVar(&cmd.diskUsage, "du", false, "Dry run mode: only show disk usage information, without upload or statting dest. Used for testing skipDirs configs, mostly.") + + if debug, _ := strconv.ParseBool(os.Getenv("CAMLI_DEBUG")); debug { + flags.BoolVar(&cmd.statcache, "statcache", true, "(debug flag) Use the stat cache, assuming unchanged files already uploaded in the past are still there. Fast, but potentially dangerous.") + flags.BoolVar(&cmd.memstats, "debug-memstats", false, "(debug flag) Enter debug in-memory mode; collecting stats only. Doesn't upload anything.") + flags.StringVar(&cmd.histo, "debug-histogram-file", "", "(debug flag) Optional file to create and write the blob size for each file uploaded. For use with GNU R and hist(read.table(\"filename\")$V1). Requires debug-memstats.") + flags.BoolVar(&cmd.capCtime, "capctime", false, "(debug flag) For file blobs use file modification time as creation time if it would be bigger (newer) than modification time. For stable filenode creation (you can forge mtime, but can't forge ctime).") + flags.BoolVar(&flagUseSQLiteChildCache, "sqlitecache", false, "(debug flag) Use sqlite for the statcache and havecache instead of a flat cache.") + flags.BoolVar(&cmd.contentsOnly, "contents_only", false, "(debug flag) Do not store any of the file's attributes. We write only the file's contents (the blobRefs for its parts) to the created file schema.") + } else { + cmd.statcache = true + } + if android.IsChild() { + flags.BoolVar(&cmd.argsFromInput, "stdinargs", false, "If true, filenames to upload are sent one-per-line on stdin. EOF means to quit the process with exit status 0.") + // limit number of goroutines to limit memory + uploadWorkers = 2 + dirUploadWorkers = 2 + statCacheWorkers = 2 + } + flagCacheLog = flags.Bool("logcache", false, "log caching details") + + return cmd + }) +} + +func (c *fileCmd) Describe() string { + return "Upload file(s)." +} + +func (c *fileCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, "Usage: camput [globalopts] file [fileopts] \n") +} + +func (c *fileCmd) Examples() []string { + return []string{ + "[opts] 0 { + return errors.New("args not supported with -argsfrominput") + } + tu := up.NewRootlessTreeUpload() + tu.Start() + br := bufio.NewReader(os.Stdin) + for { + path, err := br.ReadString('\n') + if path = strings.TrimSpace(path); path != "" { + tu.Enqueue(path) + } + if err == io.EOF { + android.PreExit() + os.Exit(0) + } + if err != nil { + log.Fatal(err) + } + } + } + + if len(args) == 0 { + return cmdmain.UsageError("No files or directories given.") + } + if up.statCache != nil { + defer up.statCache.Close() + } + for _, filename := range args { + fi, err := os.Stat(filename) + if err != nil { + return err + } + // Skip ignored files or base directories. Failing to skip the + // latter results in a panic. + if up.Client.IsIgnoredFile(filename) { + log.Printf("Client configured to ignore %s; skipping.", filename) + continue + } + if fi.IsDir() { + if up.fileOpts.wantVivify() { + vlog.Printf("Directories not supported in vivify mode; skipping %v\n", filename) + continue + } + t := up.NewTreeUpload(filename) + t.Start() + lastPut, err = t.Wait() + } else { + lastPut, err = up.UploadFile(filename) + if err == nil && c.deleteAfterUpload { + if err := os.Remove(filename); err != nil { + log.Printf("Error deleting %v: %v", filename, err) + } else { + log.Printf("Deleted %v", filename) + } + } + } + if handleResult("file", lastPut, err) != nil { + return err + } + } + + if permaNode != nil && lastPut != nil { + put, err := up.UploadAndSignBlob(schema.NewSetAttributeClaim(permaNode.BlobRef, "camliContent", lastPut.BlobRef.String())) + if handleResult("claim-permanode-content", put, err) != nil { + return err + } + if c.title != "" { + put, err := up.UploadAndSignBlob(schema.NewSetAttributeClaim(permaNode.BlobRef, "title", c.title)) + handleResult("claim-permanode-title", put, err) + } + if c.tag != "" { + tags := strings.Split(c.tag, ",") + for _, tag := range tags { + m := schema.NewAddAttributeClaim(permaNode.BlobRef, "tag", tag) + put, err := up.UploadAndSignBlob(m) + handleResult("claim-permanode-tag", put, err) + } + } + handleResult("permanode", permaNode, nil) + } + return nil +} + +func (c *fileCmd) initCaches(up *Uploader) { + if !c.statcache || *flagBlobDir != "" { + return + } + gen, err := up.StorageGeneration() + if err != nil { + log.Printf("WARNING: not using local caches; failed to retrieve server's storage generation: %v", err) + return + } + if c.statcache { + up.statCache = NewKvStatCache(gen) + } +} + +// DumpStats creates the destFile and writes a line per received blob, +// with its blob size. +func DumpStats(sr *statspkg.Receiver, destFile string) { + sr.Lock() + defer sr.Unlock() + + f, err := os.Create(destFile) + if err != nil { + log.Fatal(err) + } + + var sum int64 + for _, size := range sr.Have { + fmt.Fprintf(f, "%d\n", size) + } + fmt.Printf("In-memory blob stats: %d blobs, %d bytes\n", len(sr.Have), sum) + + err = f.Close() + if err != nil { + log.Fatal(err) + } +} + +type stats struct { + files, bytes int64 +} + +func (s *stats) incr(n *node) { + s.files++ + if !n.fi.IsDir() { + s.bytes += n.fi.Size() + } +} + +func (up *Uploader) lstat(path string) (os.FileInfo, error) { + // TODO(bradfitz): use VFS + return os.Lstat(path) +} + +func (up *Uploader) stat(path string) (os.FileInfo, error) { + if up.fs == nil { + return os.Stat(path) + } + f, err := up.fs.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + return f.Stat() +} + +func (up *Uploader) open(path string) (http.File, error) { + if up.fs == nil { + return os.Open(path) + } + return up.fs.Open(path) +} + +func (n *node) directoryStaticSet() (*schema.StaticSet, error) { + ss := new(schema.StaticSet) + for _, c := range n.children { + pr, err := c.PutResult() + if err != nil { + return nil, fmt.Errorf("Error populating directory static set for child %q: %v", c.fullPath, err) + } + ss.Add(pr.BlobRef) + } + return ss, nil +} + +func (up *Uploader) uploadNode(n *node) (*client.PutResult, error) { + fi := n.fi + mode := fi.Mode() + if mode&os.ModeType == 0 { + return up.uploadNodeRegularFile(n) + } + bb := schema.NewCommonFileMap(n.fullPath, fi) + switch { + case mode&os.ModeSymlink != 0: + // TODO(bradfitz): use VFS here; not os.Readlink + target, err := os.Readlink(n.fullPath) + if err != nil { + return nil, err + } + bb.SetSymlinkTarget(target) + case mode&os.ModeDevice != 0: + // including mode & os.ModeCharDevice + fallthrough + case mode&os.ModeSocket != 0: + bb.SetType("socket") + case mode&os.ModeNamedPipe != 0: // fifo + bb.SetType("fifo") + default: + return nil, fmt.Errorf("camput.files: unsupported file type %v for file %v", mode, n.fullPath) + case fi.IsDir(): + ss, err := n.directoryStaticSet() + if err != nil { + return nil, err + } + sspr, err := up.UploadBlob(ss) + if err != nil { + return nil, err + } + bb.PopulateDirectoryMap(sspr.BlobRef) + } + + mappr, err := up.UploadBlob(bb) + if err == nil { + if !mappr.Skipped { + vlog.Printf("Uploaded %q, %s for %s", bb.Type(), mappr.BlobRef, n.fullPath) + } + } else { + vlog.Printf("Error uploading map for %s (%s, %s): %v", n.fullPath, bb.Type(), bb.Blob().BlobRef(), err) + } + return mappr, err + +} + +// statReceiver returns the StatReceiver used for checking for and uploading blobs. +// +// The optional provided node is only used for conditionally printing out status info to stdout. +func (up *Uploader) statReceiver(n *node) blobserver.StatReceiver { + statReceiver := up.altStatReceiver + if statReceiver == nil { + // TODO(mpl): simplify the altStatReceiver situation as well, + // see TODO in cmd/camput/uploader.go + statReceiver = up.Client + } + if android.IsChild() && n != nil && n.fi.Mode()&os.ModeType == 0 { + return android.StatusReceiver{Sr: statReceiver, Path: n.fullPath} + } + return statReceiver +} + +func (up *Uploader) noStatReceiver(r blobserver.BlobReceiver) blobserver.StatReceiver { + return noStatReceiver{r} +} + +// A haveCacheStatReceiver relays Receive calls to the embedded +// BlobReceiver and treats all Stat calls like the blob doesn't exist. +// +// This is used by the client once it's already asked the server that +// it doesn't have the whole file in some chunk layout already, so we +// know we're just writing new stuff. For resuming in the middle of +// larger uploads, it turns out that the pkg/client.Client.Upload +// already checks the have cache anyway, so going right to mid-chunk +// receives is fine. +// +// TODO(bradfitz): this probabaly all needs an audit/rationalization/tests +// to make sure all the players are agreeing on the responsibilities. +// And maybe the Android stats are wrong, too. (see pkg/client/android's +// StatReceiver) +type noStatReceiver struct { + blobserver.BlobReceiver +} + +func (noStatReceiver) StatBlobs(dest chan<- blob.SizedRef, blobs []blob.Ref) error { + return nil +} + +var atomicDigestOps int64 // number of files digested + +// wholeFileDigest returns the sha1 digest of the regular file's absolute +// path given in fullPath. +func (up *Uploader) wholeFileDigest(fullPath string) (blob.Ref, error) { + // TODO(bradfitz): cache this. + file, err := up.open(fullPath) + if err != nil { + return blob.Ref{}, err + } + defer file.Close() + td := &trackDigestReader{r: file} + _, err = io.Copy(ioutil.Discard, td) + atomic.AddInt64(&atomicDigestOps, 1) + if err != nil { + return blob.Ref{}, err + } + return blob.MustParse(td.Sum()), nil +} + +var noDupSearch, _ = strconv.ParseBool(os.Getenv("CAMLI_NO_FILE_DUP_SEARCH")) + +// fileMapFromDuplicate queries the server's search interface for an +// existing file with an entire contents of sum (a blobref string). +// If the server has it, it's validated, and then fileMap (which must +// already be partially populated) has its "parts" field populated, +// and then fileMap is uploaded (if necessary) and a PutResult with +// its blobref is returned. If there's any problem, or a dup doesn't +// exist, ok is false. +// If required, Vivify is also done here. +func (up *Uploader) fileMapFromDuplicate(bs blobserver.StatReceiver, fileMap *schema.Builder, sum string) (pr *client.PutResult, ok bool) { + if noDupSearch { + return + } + _, err := up.Client.SearchRoot() + if err != nil { + return + } + dupFileRef, err := up.Client.SearchExistingFileSchema(blob.MustParse(sum)) + if err != nil { + log.Printf("Warning: error searching for already-uploaded copy of %s: %v", sum, err) + return nil, false + } + if !dupFileRef.Valid() { + return nil, false + } + if *cmdmain.FlagVerbose { + log.Printf("Found dup of contents %s in file schema %s", sum, dupFileRef) + } + dupMap, err := up.Client.FetchSchemaBlob(dupFileRef) + if err != nil { + log.Printf("Warning: error fetching %v: %v", dupFileRef, err) + return nil, false + } + + fileMap.PopulateParts(dupMap.PartsSize(), dupMap.ByteParts()) + + json, err := fileMap.JSON() + if err != nil { + return nil, false + } + uh := client.NewUploadHandleFromString(json) + if up.fileOpts.wantVivify() { + uh.Vivify = true + } + if !uh.Vivify && uh.BlobRef == dupFileRef { + // Unchanged (same filename, modtime, JSON serialization, etc) + return &client.PutResult{BlobRef: dupFileRef, Size: uint32(len(json)), Skipped: true}, true + } + pr, err = up.Upload(uh) + if err != nil { + log.Printf("Warning: error uploading file map after finding server dup of %v: %v", sum, err) + return nil, false + } + return pr, true +} + +func (up *Uploader) uploadNodeRegularFile(n *node) (*client.PutResult, error) { + var filebb *schema.Builder + if up.fileOpts.contentsOnly { + filebb = schema.NewFileMap("") + } else { + filebb = schema.NewCommonFileMap(n.fullPath, n.fi) + } + filebb.SetType("file") + + up.fdGate.Start() + defer up.fdGate.Done() + + file, err := up.open(n.fullPath) + if err != nil { + return nil, err + } + defer file.Close() + if !up.fileOpts.contentsOnly { + if up.fileOpts.exifTime { + ra, ok := file.(io.ReaderAt) + if !ok { + return nil, errors.New("Error asserting local file to io.ReaderAt") + } + modtime, err := schema.FileTime(ra) + if err != nil { + log.Printf("warning: getting time from EXIF failed for %v: %v", n.fullPath, err) + } else { + filebb.SetModTime(modtime) + } + } + if up.fileOpts.capCtime { + filebb.CapCreationTime() + } + } + + var ( + size = n.fi.Size() + fileContents io.Reader = io.LimitReader(file, size) + br blob.Ref // of file schemaref + sum string // sha1 hashsum of the file to upload + pr *client.PutResult // of the final "file" schema blob + ) + + const dupCheckThreshold = 256 << 10 + if size > dupCheckThreshold { + sumRef, err := up.wholeFileDigest(n.fullPath) + if err == nil { + sum = sumRef.String() + ok := false + pr, ok = up.fileMapFromDuplicate(up.statReceiver(n), filebb, sum) + if ok { + br = pr.BlobRef + android.NoteFileUploaded(n.fullPath, !pr.Skipped) + if up.fileOpts.wantVivify() { + // we can return early in that case, because the other options + // are disallowed in the vivify case. + return pr, nil + } + } + } + } + + if up.fileOpts.wantVivify() { + // If vivify wasn't already done in fileMapFromDuplicate. + err := schema.WriteFileChunks(up.noStatReceiver(up.statReceiver(n)), filebb, fileContents) + if err != nil { + return nil, err + } + json, err := filebb.JSON() + if err != nil { + return nil, err + } + br = blob.SHA1FromString(json) + h := &client.UploadHandle{ + BlobRef: br, + Size: uint32(len(json)), + Contents: strings.NewReader(json), + Vivify: true, + } + pr, err = up.Upload(h) + if err != nil { + return nil, err + } + android.NoteFileUploaded(n.fullPath, true) + return pr, nil + } + + if !br.Valid() { + // br still zero means fileMapFromDuplicate did not find the file on the server, + // and the file has not just been uploaded subsequently to a vivify request. + // So we do the full file + file schema upload here. + if sum == "" && up.fileOpts.wantFilePermanode() { + fileContents = &trackDigestReader{r: fileContents} + } + br, err = schema.WriteFileMap(up.noStatReceiver(up.statReceiver(n)), filebb, fileContents) + if err != nil { + return nil, err + } + } + + // The work for those planned permanodes (and the claims) is redone + // everytime we get here (i.e past the stat cache). However, they're + // caught by the have cache, so they won't be reuploaded for nothing + // at least. + if up.fileOpts.wantFilePermanode() { + if td, ok := fileContents.(*trackDigestReader); ok { + sum = td.Sum() + } + // claimTime is both the time of the "claimDate" in the + // JSON claim, as well as the date in the OpenPGP + // header. + // TODO(bradfitz): this is a little clumsy to do by hand. + // There should probably be a method on *Uploader to do this + // from an unsigned schema map. Maybe ditch the schema.Claimer + // type and just have the Uploader override the claimDate. + claimTime, ok := filebb.ModTime() + if !ok { + return nil, fmt.Errorf("couldn't get modtime for file %v", n.fullPath) + } + err = up.uploadFilePermanode(sum, br, claimTime) + if err != nil { + return nil, fmt.Errorf("Error uploading permanode for node %v: %v", n, err) + } + } + + // TODO(bradfitz): faking a PutResult here to return + // is kinda gross. should instead make a + // blobserver.Storage wrapper type (wrapping + // statReceiver) that can track some of this? or make + // schemaWriteFileMap return it? + json, _ := filebb.JSON() + pr = &client.PutResult{BlobRef: br, Size: uint32(len(json)), Skipped: false} + return pr, nil +} + +// uploadFilePermanode creates and uploads the planned permanode (with sum as a +// fixed key) associated with the file blobref fileRef. +// It also sets the optional tags for this permanode. +func (up *Uploader) uploadFilePermanode(sum string, fileRef blob.Ref, claimTime time.Time) error { + // Use a fixed time value for signing; not using modtime + // so two identical files don't have different modtimes? + // TODO(bradfitz): consider this more? + permaNodeSigTime := time.Unix(0, 0) + permaNode, err := up.UploadPlannedPermanode(sum, permaNodeSigTime) + if err != nil { + return fmt.Errorf("Error uploading planned permanode: %v", err) + } + handleResult("node-permanode", permaNode, nil) + + contentAttr := schema.NewSetAttributeClaim(permaNode.BlobRef, "camliContent", fileRef.String()) + contentAttr.SetClaimDate(claimTime) + signer, err := up.Signer() + if err != nil { + return err + } + signed, err := contentAttr.SignAt(signer, claimTime) + if err != nil { + return fmt.Errorf("Failed to sign content claim: %v", err) + } + put, err := up.uploadString(signed) + if err != nil { + return fmt.Errorf("Error uploading permanode's attribute: %v", err) + } + + handleResult("node-permanode-contentattr", put, nil) + if tags := up.fileOpts.tags(); len(tags) > 0 { + errch := make(chan error) + for _, tag := range tags { + go func(tag string) { + m := schema.NewAddAttributeClaim(permaNode.BlobRef, "tag", tag) + m.SetClaimDate(claimTime) + signed, err := m.SignAt(signer, claimTime) + if err != nil { + errch <- fmt.Errorf("Failed to sign tag claim: %v", err) + return + } + put, err := up.uploadString(signed) + if err != nil { + errch <- fmt.Errorf("Error uploading permanode's tag attribute %v: %v", tag, err) + return + } + handleResult("node-permanode-tag", put, nil) + errch <- nil + }(tag) + } + + for _ = range tags { + if e := <-errch; e != nil && err == nil { + err = e + } + } + if err != nil { + return err + } + } + return nil +} + +func (up *Uploader) UploadFile(filename string) (*client.PutResult, error) { + fullPath, err := filepath.Abs(filename) + if err != nil { + return nil, err + } + fi, err := up.lstat(fullPath) + if err != nil { + return nil, err + } + + if fi.IsDir() { + panic("must use UploadTree now for directories") + } + n := &node{ + fullPath: fullPath, + fi: fi, + } + + withPermanode := up.fileOpts.wantFilePermanode() + if up.statCache != nil && !up.fileOpts.wantVivify() { + // Note: ignoring cache hits if wantVivify, otherwise + // a non-vivify put followed by a vivify one wouldn't + // end up doing the vivify. + if cachedRes, err := up.statCache.CachedPutResult( + up.pwd, n.fullPath, n.fi, withPermanode); err == nil { + return cachedRes, nil + } + } + + pr, err := up.uploadNode(n) + if err == nil && up.statCache != nil { + up.statCache.AddCachedPutResult( + up.pwd, n.fullPath, n.fi, pr, withPermanode) + } + + return pr, err +} + +// NewTreeUpload returns a TreeUpload. It doesn't begin uploading any files until a +// call to Start +func (up *Uploader) NewTreeUpload(dir string) *TreeUpload { + tu := up.NewRootlessTreeUpload() + tu.rootless = false + tu.base = dir + return tu +} + +func (up *Uploader) NewRootlessTreeUpload() *TreeUpload { + return &TreeUpload{ + rootless: true, + base: "", + up: up, + donec: make(chan bool, 1), + errc: make(chan error, 1), + stattedc: make(chan *node, buffered), + } +} + +func (t *TreeUpload) Start() { + go t.run() +} + +type node struct { + tu *TreeUpload // nil if not doing a tree upload + fullPath string + fi os.FileInfo + children []*node + + // cond (and its &mu Lock) guard err and res. + cond sync.Cond // with L being &mu + mu sync.Mutex + err error + res *client.PutResult + + sumBytes int64 // cached value, if non-zero. also guarded by mu. +} + +func (n *node) String() string { + if n == nil { + return "" + } + return fmt.Sprintf("[node %s, isDir=%v, nchild=%d]", n.fullPath, n.fi.IsDir(), len(n.children)) +} + +func (n *node) SetPutResult(res *client.PutResult, err error) { + n.mu.Lock() + defer n.mu.Unlock() + if res == nil && err == nil { + panic("SetPutResult called with (nil, nil)") + } + if n.res != nil || n.err != nil { + panic("SetPutResult called twice on node " + n.fullPath) + } + n.res, n.err = res, err + n.cond.Signal() +} + +func (n *node) PutResult() (*client.PutResult, error) { + n.mu.Lock() + defer n.mu.Unlock() + for n.err == nil && n.res == nil { + n.cond.Wait() + } + return n.res, n.err +} + +func (n *node) SumBytes() (v int64) { + n.mu.Lock() + defer n.mu.Unlock() + if n.sumBytes != 0 { + return n.sumBytes + } + for _, c := range n.children { + v += c.SumBytes() + } + if n.fi.Mode()&os.ModeType == 0 { + v += n.fi.Size() + } + n.sumBytes = v + return +} + +/* +A TreeUpload holds the state of an ongoing recursive directory tree +upload. Call Wait to get the final result. + +Uploading a directory tree involves several concurrent processes, each +which may involve multiple goroutines: + +1) one process stats all files and walks all directories as fast as possible + to calculate how much total work there will be. this goroutine also + filters out directories to be skipped. (caches, temp files, skipDirs, etc) + + 2) one process works though the files that were discovered and checks + the statcache to see what actually needs to be uploaded. + The statcache is + full path => {last os.FileInfo signature, put result from last time} + and is used to avoid re-reading/digesting the file even locally, + trusting that it's already on the server. + + 3) one process uploads files & metadata. This process checks the "havecache" + to see which blobs are already on the server. For awhile the local havecache + (if configured) and the remote blobserver "stat" RPC are raced to determine + if the local havecache is even faster. If not, it's not consulted. But if the + latency of remote stats is high enough, checking locally is preferred. +*/ +type TreeUpload struct { + // If DiskUsageMode is set true before Start, only + // per-directory disk usage stats are output, like the "du" + // command. + DiskUsageMode bool + + // Immutable: + rootless bool // if true, "base" will be empty. + base string // base directory + up *Uploader + stattedc chan *node // from stat-the-world goroutine to run() + + donec chan bool // closed when run() finishes + err error + errc chan error // with 1 buffer item + + // Owned by run goroutine: + total stats // total bytes on disk + skipped stats // not even tried to upload (trusting stat cache) + uploaded stats // uploaded (even if server said it already had it and bytes weren't sent) + + finalPutRes *client.PutResult // set after run() returns +} + +// Enqueue starts uploading path (a file, directory, etc). +func (t *TreeUpload) Enqueue(path string) { + t.statPath(path, nil) +} + +// fi is optional (will be statted if nil) +func (t *TreeUpload) statPath(fullPath string, fi os.FileInfo) (nod *node, err error) { + defer func() { + if err == nil && nod != nil { + t.stattedc <- nod + } + }() + if t.up.Client.IsIgnoredFile(fullPath) { + return nil, nil + } + if fi == nil { + fi, err = t.up.lstat(fullPath) + if err != nil { + return nil, err + } + } + n := &node{ + tu: t, + fullPath: fullPath, + fi: fi, + } + n.cond.L = &n.mu + + if !fi.IsDir() { + return n, nil + } + f, err := t.up.open(fullPath) + if err != nil { + return nil, err + } + fis, err := f.Readdir(-1) + f.Close() + if err != nil { + return nil, err + } + sort.Sort(byTypeAndName(fis)) + for _, fi := range fis { + depn, err := t.statPath(filepath.Join(fullPath, filepath.Base(fi.Name())), fi) + if err != nil { + return nil, err + } + if depn != nil { + n.children = append(n.children, depn) + } + } + return n, nil +} + +// testHookStatCache, if non-nil, runs first in the checkStatCache worker. +var testHookStatCache func(el interface{}, ok bool) + +func (t *TreeUpload) run() { + defer close(t.donec) + + // Kick off scanning all files, eventually learning the root + // node (which references all its children). + var root *node // nil until received and set in loop below. + rootc := make(chan *node, 1) + if !t.rootless { + go func() { + n, err := t.statPath(t.base, nil) + if err != nil { + log.Fatalf("Error scanning files under %s: %v", t.base, err) + } + close(t.stattedc) + rootc <- n + }() + } + + var lastStat, lastUpload string + dumpStats := func() { + if android.IsChild() { + printAndroidCamputStatus(t) + return + } + statStatus := "" + if root == nil { + statStatus = fmt.Sprintf("last stat: %s", lastStat) + } + blobStats := t.up.Stats() + log.Printf("FILES: Total: %+v Skipped: %+v Uploaded: %+v %s BLOBS: %s Digested: %d last upload: %s", + t.total, t.skipped, t.uploaded, + statStatus, + blobStats.String(), + atomic.LoadInt64(&atomicDigestOps), + lastUpload) + } + + // Channels for stats & progress bars. These are never closed: + uploadedc := make(chan *node) // at least tried to upload; server might have had blob + skippedc := make(chan *node) // didn't even hit blobserver; trusted our stat cache + + uploadsdonec := make(chan bool) + var upload chan<- interface{} + withPermanode := t.up.fileOpts.wantFilePermanode() + if t.DiskUsageMode { + upload = chanworker.NewWorker(1, func(el interface{}, ok bool) { + if !ok { + uploadsdonec <- true + return + } + n := el.(*node) + if n.fi.IsDir() { + fmt.Printf("%d\t%s\n", n.SumBytes()>>10, n.fullPath) + } + }) + } else { + dirUpload := chanworker.NewWorker(dirUploadWorkers, func(el interface{}, ok bool) { + if !ok { + log.Printf("done uploading directories - done with all uploads.") + uploadsdonec <- true + return + } + n := el.(*node) + put, err := t.up.uploadNode(n) + if err != nil { + log.Fatalf("Error uploading %s: %v", n.fullPath, err) + } + n.SetPutResult(put, nil) + uploadedc <- n + }) + + upload = chanworker.NewWorker(uploadWorkers, func(el interface{}, ok bool) { + if !ok { + log.Printf("done with all uploads.") + close(dirUpload) + return + } + n := el.(*node) + if n.fi.IsDir() { + dirUpload <- n + return + } + put, err := t.up.uploadNode(n) + if err != nil { + log.Fatalf("Error uploading %s: %v", n.fullPath, err) + } + n.SetPutResult(put, nil) + if c := t.up.statCache; c != nil { + c.AddCachedPutResult( + t.up.pwd, n.fullPath, n.fi, put, withPermanode) + } + uploadedc <- n + }) + } + + checkStatCache := chanworker.NewWorker(statCacheWorkers, func(el interface{}, ok bool) { + if hook := testHookStatCache; hook != nil { + hook(el, ok) + } + if !ok { + if t.up.statCache != nil { + log.Printf("done checking stat cache") + } + close(upload) + return + } + n := el.(*node) + if t.DiskUsageMode || t.up.statCache == nil { + upload <- n + return + } + if !n.fi.IsDir() { + cachedRes, err := t.up.statCache.CachedPutResult( + t.up.pwd, n.fullPath, n.fi, withPermanode) + if err == nil { + n.SetPutResult(cachedRes, nil) + cachelog.Printf("Cache HIT on %q -> %v", n.fullPath, cachedRes) + android.NoteFileUploaded(n.fullPath, false) + skippedc <- n + return + } + } + upload <- n + }) + + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + stattedc := t.stattedc +Loop: + for { + select { + case <-uploadsdonec: + break Loop + case n := <-rootc: + root = n + case n := <-uploadedc: + t.uploaded.incr(n) + lastUpload = n.fullPath + case n := <-skippedc: + t.skipped.incr(n) + case n, ok := <-stattedc: + if !ok { + log.Printf("done statting:") + dumpStats() + close(checkStatCache) + stattedc = nil + continue + } + lastStat = n.fullPath + t.total.incr(n) + checkStatCache <- n + case <-ticker.C: + dumpStats() + } + } + + log.Printf("tree upload finished. final stats:") + dumpStats() + + if root == nil { + panic("unexpected nil root node") + } + var err error + log.Printf("Waiting on root node %q", root.fullPath) + t.finalPutRes, err = root.PutResult() + log.Printf("Waited on root node %q: %v", root.fullPath, t.finalPutRes) + if err != nil { + t.err = err + } +} + +func (t *TreeUpload) Wait() (*client.PutResult, error) { + <-t.donec + // If an error is waiting and we don't otherwise have one, use it: + if t.err == nil { + select { + case t.err = <-t.errc: + default: + } + } + if t.err == nil && t.finalPutRes == nil { + panic("Nothing ever set t.finalPutRes, but no error set") + } + return t.finalPutRes, t.err +} + +type byTypeAndName []os.FileInfo + +func (s byTypeAndName) Len() int { return len(s) } +func (s byTypeAndName) Less(i, j int) bool { + // files go before directories + if s[i].IsDir() { + if !s[j].IsDir() { + return false + } + } else if s[j].IsDir() { + return true + } + return s[i].Name() < s[j].Name() +} +func (s byTypeAndName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// trackDigestReader is an io.Reader wrapper which records the digest of what it reads. +type trackDigestReader struct { + r io.Reader + h hash.Hash +} + +func (t *trackDigestReader) Read(p []byte) (n int, err error) { + if t.h == nil { + t.h = sha1.New() + } + n, err = t.r.Read(p) + t.h.Write(p[:n]) + return +} + +func (t *trackDigestReader) Sum() string { + return fmt.Sprintf("sha1-%x", t.h.Sum(nil)) +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/init.go b/vendor/github.com/camlistore/camlistore/cmd/camput/init.go new file mode 100644 index 00000000..1460a946 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/init.go @@ -0,0 +1,259 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path/filepath" + "strings" + + "camlistore.org/pkg/auth" + "camlistore.org/pkg/blob" + "camlistore.org/pkg/client" + "camlistore.org/pkg/client/android" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/jsonsign" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/types/clientconfig" +) + +type initCmd struct { + newKey bool // whether to create a new GPG ring and key. + noconfig bool // whether to generate a client config file. + keyId string // GPG key ID to use. + secretRing string // GPG secret ring file to use. + userPass string // username and password to use when asking a server for the config. + insecureTLS bool // TLS certificate verification disabled +} + +func init() { + cmdmain.RegisterCommand("init", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(initCmd) + flags.BoolVar(&cmd.newKey, "newkey", false, + "Automatically generate a new identity in a new secret ring at the default location (~/.config/camlistore/identity-secring.gpg on linux).") + flags.StringVar(&cmd.keyId, "gpgkey", "", "GPG key ID to use for signing (overrides $GPGKEY environment)") + flags.BoolVar(&cmd.noconfig, "noconfig", false, "Stop after creating the public key blob, and do not try and create a config file.") + flags.StringVar(&cmd.userPass, "userpass", "", "username:password to use when asking a server for a client configuration. Requires --server global option.") + flags.BoolVar(&cmd.insecureTLS, "insecure", false, "If set, when getting configuration from a server (with --server and --userpass) over TLS, the server's certificate verification is disabled. Needed when the server is using a self-signed certificate.") + return cmd + }) +} + +func (c *initCmd) Describe() string { + return "Initialize the camput configuration file. With no option, it tries to use the GPG key found in the default identity secret ring." +} + +func (c *initCmd) Usage() { + usage := "Usage: camput [--server host] init [opts]\n\nExamples:\n" + for _, v := range c.usageExamples() { + usage += v + "\n" + } + fmt.Fprintf(cmdmain.Stderr, usage) +} + +func (c *initCmd) usageExamples() []string { + var examples []string + for _, v := range c.Examples() { + examples = append(examples, "camput init "+v) + } + return append(examples, + "camput --server=https://localhost:3179 init --userpass=foo:bar --insecure=true") +} + +func (c *initCmd) Examples() []string { + // TODO(mpl): I can't add the correct -userpass example to that list, because + // it requires the global --server flag, which has to be passed before the + // "init" subcommand. We should have a way to override that. + // Or I could just add a -server flag to the init subcommand, but it sounds + // like a lame hack. + return []string{ + "", + "--gpgkey=XXXXX", + "--newkey #Creates a new identity", + } +} + +// initSecretRing sets c.secretRing. It tries, in this order, the --secret-keyring flag, +// the CAMLI_SECRET_RING env var, then defaults to the operating system dependent location +// otherwise. +// It returns an error if the file does not exist. +func (c *initCmd) initSecretRing() error { + if secretRing, ok := osutil.ExplicitSecretRingFile(); ok { + c.secretRing = secretRing + } else { + if android.OnAndroid() { + panic("on android, so CAMLI_SECRET_RING should have been defined, or --secret-keyring used.") + } + c.secretRing = osutil.SecretRingFile() + } + if _, err := os.Stat(c.secretRing); err != nil { + hint := "\nA GPG key is required, please use 'camput init --newkey'.\n\nOr if you know what you're doing, you can set the global camput flag --secret-keyring, or the CAMLI_SECRET_RING env var, to use your own GPG ring. And --gpgkey= or GPGKEY to select which key ID to use." + return fmt.Errorf("Could not use secret ring file %v: %v.\n%v", c.secretRing, err, hint) + } + return nil +} + +// initKeyId sets c.keyId. It checks, in this order, the --gpgkey flag, the GPGKEY env var, +// and in the default identity secret ring. +func (c *initCmd) initKeyId() error { + if k := c.keyId; k != "" { + return nil + } + if k := os.Getenv("GPGKEY"); k != "" { + c.keyId = k + return nil + } + + k, err := jsonsign.KeyIdFromRing(c.secretRing) + if err != nil { + hint := "You can set --gpgkey= or the GPGKEY env var to select which key ID to use.\n" + return fmt.Errorf("No suitable gpg key was found in %v: %v.\n%v", c.secretRing, err, hint) + } + c.keyId = k + log.Printf("Re-using identity with keyId %q found in file %s", c.keyId, c.secretRing) + return nil +} + +func (c *initCmd) getPublicKeyArmored() ([]byte, error) { + entity, err := jsonsign.EntityFromSecring(c.keyId, c.secretRing) + if err != nil { + return nil, fmt.Errorf("Could not find keyId %v in ring %v: %v", c.keyId, c.secretRing, err) + } + pubArmor, err := jsonsign.ArmoredPublicKey(entity) + if err != nil { + return nil, fmt.Errorf("failed to export armored public key ID %q from %v: %v", c.keyId, c.secretRing, err) + } + return []byte(pubArmor), nil +} + +func (c *initCmd) clientConfigFromServer() (*clientconfig.Config, error) { + if c.noconfig { + log.Print("--userpass and --noconfig are mutually exclusive") + return nil, cmdmain.ErrUsage + } + server := client.ExplicitServer() + if server == "" { + log.Print("--userpass requires --server") + return nil, cmdmain.ErrUsage + } + fields := strings.Split(c.userPass, ":") + if len(fields) != 2 { + log.Printf("wrong userpass; wanted username:password, got %q", c.userPass) + return nil, cmdmain.ErrUsage + } + + cl := client.NewFromParams(server, auth.NewBasicAuth(fields[0], fields[1])) + cl.InsecureTLS = c.insecureTLS + cl.SetHTTPClient(&http.Client{Transport: cl.TransportForConfig(nil)}) + var cc clientconfig.Config + + helpRoot, err := cl.HelpRoot() + if err != nil { + return nil, err + } + + if err := cl.GetJSON(helpRoot+"?clientConfig=true", &cc); err != nil { + return nil, err + } + return &cc, nil +} + +func (c *initCmd) writeConfig(cc *clientconfig.Config) error { + configFilePath := osutil.UserClientConfigPath() + if _, err := os.Stat(configFilePath); err == nil { + return fmt.Errorf("Config file %q already exists; quitting without touching it.", configFilePath) + } + if err := os.MkdirAll(filepath.Dir(configFilePath), 0700); err != nil { + return err + } + + jsonBytes, err := json.MarshalIndent(cc, "", " ") + if err != nil { + log.Fatalf("JSON serialization error: %v", err) + } + if err := ioutil.WriteFile(configFilePath, jsonBytes, 0600); err != nil { + return fmt.Errorf("could not write client config file %v: %v", configFilePath, err) + } + log.Printf("Wrote %q; modify as necessary.", configFilePath) + return nil + +} + +func (c *initCmd) RunCommand(args []string) error { + if len(args) > 0 { + return cmdmain.ErrUsage + } + + if c.newKey && c.keyId != "" { + log.Fatal("--newkey and --gpgkey are mutually exclusive") + } + + if c.userPass != "" { + cc, err := c.clientConfigFromServer() + if err != nil { + return err + } + return c.writeConfig(cc) + } + + var err error + if c.newKey { + c.secretRing = osutil.DefaultSecretRingFile() + c.keyId, err = jsonsign.GenerateNewSecRing(c.secretRing) + if err != nil { + return err + } + } else { + if err := c.initSecretRing(); err != nil { + return err + } + if err := c.initKeyId(); err != nil { + return err + } + } + + pubArmor, err := c.getPublicKeyArmored() + if err != nil { + return err + } + + bref := blob.SHA1FromString(string(pubArmor)) + + log.Printf("Your Camlistore identity (your GPG public key's blobref) is: %s", bref.String()) + + if c.noconfig { + return nil + } + + return c.writeConfig(&clientconfig.Config{ + Servers: map[string]*clientconfig.Server{ + "localhost": { + Server: "http://localhost:3179", + IsDefault: true, + Auth: "localhost", + }, + }, + Identity: c.keyId, + IgnoredFiles: []string{".DS_Store"}, + }) +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/kvcache.go b/vendor/github.com/camlistore/camlistore/cmd/camput/kvcache.go new file mode 100644 index 00000000..15a1b1d1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/kvcache.go @@ -0,0 +1,398 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "hash/crc32" + "log" + "net/url" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/client" + "camlistore.org/pkg/kvutil" + "camlistore.org/pkg/osutil" + "camlistore.org/third_party/github.com/cznic/kv" +) + +var errCacheMiss = errors.New("not in cache") + +// KvHaveCache is a HaveCache on top of a single +// mutable database file on disk using github.com/cznic/kv. +// It stores the blobref in binary as the key, and +// the blobsize in binary as the value. +// Access to the cache is restricted to one process +// at a time with a lock file. Close should be called +// to remove the lock. +type KvHaveCache struct { + filename string + db *kv.DB +} + +func NewKvHaveCache(gen string) *KvHaveCache { + cleanCacheDir() + fullPath := filepath.Join(osutil.CacheDir(), "camput.havecache."+escapeGen(gen)+".kv") + db, err := kvutil.Open(fullPath, nil) + if err != nil { + log.Fatalf("Could not create/open new have cache at %v, %v", fullPath, err) + } + return &KvHaveCache{ + filename: fullPath, + db: db, + } +} + +// Close should be called to commit all the writes +// to the db and to unlock the file. +func (c *KvHaveCache) Close() error { + return c.db.Close() +} + +func (c *KvHaveCache) StatBlobCache(br blob.Ref) (size uint32, ok bool) { + if !br.Valid() { + return + } + binBr, _ := br.MarshalBinary() + binVal, err := c.db.Get(nil, binBr) + if err != nil { + log.Fatalf("Could not query have cache %v for %v: %v", c.filename, br, err) + } + if binVal == nil { + cachelog.Printf("have cache MISS on %v", br) + return + } + val, err := strconv.ParseUint(string(binVal), 10, 32) + if err != nil { + log.Fatalf("Could not decode have cache binary value for %v: %v", br, err) + } + if val < 0 { + log.Fatalf("Error decoding have cache binary value for %v: size=%d", br, val) + } + cachelog.Printf("have cache HIT on %v", br) + return uint32(val), true +} + +func (c *KvHaveCache) NoteBlobExists(br blob.Ref, size uint32) { + if !br.Valid() { + return + } + if size < 0 { + log.Fatalf("Got a negative blob size to note in have cache for %v", br) + } + binBr, _ := br.MarshalBinary() + binVal := []byte(strconv.Itoa(int(size))) + cachelog.Printf("Adding to have cache %v: %q", br, binVal) + _, _, err := c.db.Put(nil, binBr, + func(binBr, old []byte) ([]byte, bool, error) { + // We do not overwrite dups + if old != nil { + return nil, false, nil + } + return binVal, true, nil + }) + if err != nil { + log.Fatalf("Could not write %v in have cache: %v", br, err) + } +} + +// KvStatCache is an UploadCache on top of a single +// mutable database file on disk using github.com/cznic/kv. +// It stores a binary combination of an os.FileInfo fingerprint and +// a client.Putresult as the key, and the blobsize in binary as +// the value. +// Access to the cache is restricted to one process +// at a time with a lock file. Close should be called +// to remove the lock. +type KvStatCache struct { + filename string + db *kv.DB +} + +func NewKvStatCache(gen string) *KvStatCache { + fullPath := filepath.Join(osutil.CacheDir(), "camput.statcache."+escapeGen(gen)+".kv") + db, err := kvutil.Open(fullPath, nil) + if err != nil { + log.Fatalf("Could not create/open new stat cache at %v, %v", fullPath, err) + } + return &KvStatCache{ + filename: fullPath, + db: db, + } +} + +// Close should be called to commit all the writes +// to the db and to unlock the file. +func (c *KvStatCache) Close() error { + return c.db.Close() +} + +func (c *KvStatCache) CachedPutResult(pwd, filename string, fi os.FileInfo, withPermanode bool) (*client.PutResult, error) { + fullPath := fullpath(pwd, filename) + cacheKey := &statCacheKey{ + Filepath: fullPath, + Permanode: withPermanode, + } + binKey, err := cacheKey.marshalBinary() + binVal, err := c.db.Get(nil, binKey) + if err != nil { + log.Fatalf("Could not query stat cache %v for %q: %v", binKey, fullPath, err) + } + if binVal == nil { + cachelog.Printf("stat cache MISS on %q", binKey) + return nil, errCacheMiss + } + val := &statCacheValue{} + if err = val.unmarshalBinary(binVal); err != nil { + return nil, fmt.Errorf("Bogus stat cached value for %q: %v", binKey, err) + } + fp := fileInfoToFingerprint(fi) + if val.Fingerprint != fp { + cachelog.Printf("cache MISS on %q: stats not equal:\n%#v\n%#v", binKey, val.Fingerprint, fp) + return nil, errCacheMiss + } + cachelog.Printf("stat cache HIT on %q", binKey) + return &val.Result, nil +} + +func (c *KvStatCache) AddCachedPutResult(pwd, filename string, fi os.FileInfo, pr *client.PutResult, withPermanode bool) { + fullPath := fullpath(pwd, filename) + cacheKey := &statCacheKey{ + Filepath: fullPath, + Permanode: withPermanode, + } + val := &statCacheValue{fileInfoToFingerprint(fi), *pr} + + binKey, err := cacheKey.marshalBinary() + if err != nil { + log.Fatalf("Could not add %q to stat cache: %v", binKey, err) + } + binVal, err := val.marshalBinary() + if err != nil { + log.Fatalf("Could not add %q to stat cache: %v", binKey, err) + } + cachelog.Printf("Adding to stat cache %q: %q", binKey, binVal) + _, _, err = c.db.Put(nil, binKey, + func(binKey, old []byte) ([]byte, bool, error) { + // We do not overwrite dups + if old != nil { + return nil, false, nil + } + return binVal, true, nil + }) + if err != nil { + log.Fatalf("Could not add %q to stat cache: %v", binKey, err) + } +} + +type statCacheKey struct { + Filepath string + Permanode bool // whether -filenodes is being used. +} + +// marshalBinary returns a more compact binary +// representation of the contents of sk. +func (sk *statCacheKey) marshalBinary() ([]byte, error) { + if sk == nil { + return nil, errors.New("Can not marshal from a nil stat cache key") + } + data := make([]byte, 0, len(sk.Filepath)+3) + data = append(data, 1) // version number + data = append(data, sk.Filepath...) + data = append(data, '|') + if sk.Permanode { + data = append(data, 1) + } + return data, nil +} + +type statFingerprint string + +type statCacheValue struct { + Fingerprint statFingerprint + Result client.PutResult +} + +// marshalBinary returns a more compact binary +// representation of the contents of scv. +func (scv *statCacheValue) marshalBinary() ([]byte, error) { + if scv == nil { + return nil, errors.New("Can not marshal from a nil stat cache value") + } + binBr, _ := scv.Result.BlobRef.MarshalBinary() + // Blob size fits on 4 bytes when binary encoded + data := make([]byte, 0, len(scv.Fingerprint)+1+4+1+len(binBr)) + buf := bytes.NewBuffer(data) + _, err := buf.WriteString(string(scv.Fingerprint)) + if err != nil { + return nil, fmt.Errorf("Could not write fingerprint %v: %v", scv.Fingerprint, err) + } + err = buf.WriteByte('|') + if err != nil { + return nil, fmt.Errorf("Could not write '|': %v", err) + } + err = binary.Write(buf, binary.BigEndian, int32(scv.Result.Size)) + if err != nil { + return nil, fmt.Errorf("Could not write blob size %d: %v", scv.Result.Size, err) + } + err = buf.WriteByte('|') + if err != nil { + return nil, fmt.Errorf("Could not write '|': %v", err) + } + _, err = buf.Write(binBr) + if err != nil { + return nil, fmt.Errorf("Could not write binary blobref %q: %v", binBr, err) + } + return buf.Bytes(), nil +} + +var pipe = []byte("|") + +func (scv *statCacheValue) unmarshalBinary(data []byte) error { + if scv == nil { + return errors.New("Can't unmarshalBinary into a nil stat cache value") + } + if scv.Fingerprint != "" { + return errors.New("Can't unmarshalBinary into a non empty stat cache value") + } + + parts := bytes.SplitN(data, pipe, 3) + if len(parts) != 3 { + return fmt.Errorf("Bogus stat cache value; was expecting fingerprint|blobSize|blobRef, got %q", data) + } + fingerprint := string(parts[0]) + buf := bytes.NewReader(parts[1]) + var size int32 + err := binary.Read(buf, binary.BigEndian, &size) + if err != nil { + return fmt.Errorf("Could not decode blob size from stat cache value part %q: %v", parts[1], err) + } + br := new(blob.Ref) + if err := br.UnmarshalBinary(parts[2]); err != nil { + return fmt.Errorf("Could not unmarshalBinary for %q: %v", parts[2], err) + } + + scv.Fingerprint = statFingerprint(fingerprint) + scv.Result = client.PutResult{ + BlobRef: *br, + Size: uint32(size), + Skipped: true, + } + return nil +} + +func fullpath(pwd, filename string) string { + var fullPath string + if filepath.IsAbs(filename) { + fullPath = filepath.Clean(filename) + } else { + fullPath = filepath.Join(pwd, filename) + } + return fullPath +} + +func escapeGen(gen string) string { + // Good enough: + return url.QueryEscape(gen) +} + +var cleanSysStat func(v interface{}) interface{} + +func fileInfoToFingerprint(fi os.FileInfo) statFingerprint { + // We calculate the CRC32 of the underlying system stat structure to get + // ctime, owner, group, etc. This is overkill (e.g. we don't care about + // the inode or device number probably), but works. + sysHash := uint32(0) + if sys := fi.Sys(); sys != nil { + if clean := cleanSysStat; clean != nil { + // TODO: don't clean bad fields, but provide a + // portable way to extract all good fields. + // This is a Linux+Mac-specific hack for now. + sys = clean(sys) + } + c32 := crc32.NewIEEE() + fmt.Fprintf(c32, "%#v", sys) + sysHash = c32.Sum32() + } + return statFingerprint(fmt.Sprintf("%dB/%dMOD/sys-%d", fi.Size(), fi.ModTime().UnixNano(), sysHash)) +} + +// Delete stranded lock files and all but the oldest 5 +// havecache/statcache files, unless they're newer than 30 days. +func cleanCacheDir() { + dir := osutil.CacheDir() + f, err := os.Open(dir) + if err != nil { + return + } + defer f.Close() + fis, err := f.Readdir(-1) + if err != nil { + return + } + var haveCache, statCache []os.FileInfo + seen := make(map[string]bool) + for _, fi := range fis { + seen[fi.Name()] = true + } + + for name := range seen { + if strings.HasSuffix(name, ".lock") && !seen[strings.TrimSuffix(name, ".lock")] { + os.Remove(filepath.Join(dir, name)) + } + } + + for _, fi := range fis { + if strings.HasSuffix(fi.Name(), ".lock") { + continue + } + if strings.HasPrefix(fi.Name(), "camput.havecache.") { + haveCache = append(haveCache, fi) + continue + } + if strings.HasPrefix(fi.Name(), "camput.statcache.") { + statCache = append(statCache, fi) + continue + } + } + for _, list := range [][]os.FileInfo{haveCache, statCache} { + if len(list) <= 5 { + continue + } + sort.Sort(byModtime(list)) + list = list[:len(list)-5] + for _, fi := range list { + if fi.ModTime().Before(time.Now().Add(-30 * 24 * time.Hour)) { + os.Remove(filepath.Join(dir, fi.Name())) + } + } + } +} + +type byModtime []os.FileInfo + +func (s byModtime) Len() int { return len(s) } +func (s byModtime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byModtime) Less(i, j int) bool { return s[i].ModTime().Before(s[j].ModTime()) } diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/logging.go b/vendor/github.com/camlistore/camlistore/cmd/camput/logging.go new file mode 100644 index 00000000..fa194736 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/logging.go @@ -0,0 +1,42 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "log" + + "camlistore.org/pkg/cmdmain" +) + +type Logger interface { + Printf(format string, args ...interface{}) +} + +type flagLogger struct { + flagPtr **bool +} + +var flagCacheLog *bool + +var vlog = &flagLogger{&cmdmain.FlagVerbose} +var cachelog = &flagLogger{&flagCacheLog} + +func (fl *flagLogger) Printf(format string, args ...interface{}) { + if fl.flagPtr != nil && **fl.flagPtr { + log.Printf(format, args...) + } +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/permanode.go b/vendor/github.com/camlistore/camlistore/cmd/camput/permanode.go new file mode 100644 index 00000000..0be79e68 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/permanode.go @@ -0,0 +1,106 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "errors" + "flag" + "fmt" + "strings" + "time" + + "camlistore.org/pkg/client" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/schema" +) + +type permanodeCmd struct { + title string + tag string + key string // else random + sigTime string +} + +func init() { + cmdmain.RegisterCommand("permanode", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(permanodeCmd) + flags.StringVar(&cmd.title, "title", "", "Optional 'title' attribute to set on new permanode") + flags.StringVar(&cmd.tag, "tag", "", "Optional tag(s) to set on new permanode; comma separated.") + flags.StringVar(&cmd.key, "key", "", "Optional key to create deterministic ('planned') permanodes. Must also use --sigtime.") + flags.StringVar(&cmd.sigTime, "sigtime", "", "Optional time to put in the OpenPGP signature packet instead of the current time. Required when producing a deterministic permanode (with --key). In format YYYY-MM-DD HH:MM:SS") + return cmd + }) +} + +func (c *permanodeCmd) Describe() string { + return "Create and upload a permanode." +} + +func (c *permanodeCmd) Usage() { + cmdmain.Errorf("Usage: camput [globalopts] permanode [permanodeopts]\n") +} + +func (c *permanodeCmd) Examples() []string { + return []string{ + " (create a new permanode)", + `-title="Some Title" -tag=foo,bar (with attributes added)`, + } +} + +func (c *permanodeCmd) RunCommand(args []string) error { + if len(args) > 0 { + return errors.New("Permanode command doesn't take any additional arguments") + } + + var ( + permaNode *client.PutResult + err error + up = getUploader() + ) + if (c.key != "") != (c.sigTime != "") { + return errors.New("Both --key and --sigtime must be used to produce deterministic permanodes.") + } + if c.key == "" { + // Normal case, with a random permanode. + permaNode, err = up.UploadNewPermanode() + } else { + const format = "2006-01-02 15:04:05" + sigTime, err := time.Parse(format, c.sigTime) + if err != nil { + return fmt.Errorf("Error parsing time %q; expecting time of form %q", c.sigTime, format) + } + permaNode, err = up.UploadPlannedPermanode(c.key, sigTime) + } + if handleResult("permanode", permaNode, err) != nil { + return err + } + + if c.title != "" { + put, err := up.UploadAndSignBlob(schema.NewSetAttributeClaim(permaNode.BlobRef, "title", c.title)) + handleResult("claim-permanode-title", put, err) + } + if c.tag != "" { + tags := strings.Split(c.tag, ",") + m := schema.NewSetAttributeClaim(permaNode.BlobRef, "tag", tags[0]) + for _, tag := range tags { + m = schema.NewAddAttributeClaim(permaNode.BlobRef, "tag", tag) + put, err := up.UploadAndSignBlob(m) + handleResult("claim-permanode-tag", put, err) + } + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/rawobj.go b/vendor/github.com/camlistore/camlistore/cmd/camput/rawobj.go new file mode 100644 index 00000000..559d3315 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/rawobj.go @@ -0,0 +1,82 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "errors" + "flag" + "strings" + + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/schema" +) + +type rawCmd struct { + vals string // pipe separated key=value "camliVersion=1|camliType=foo", etc + signed bool +} + +func init() { + cmdmain.RegisterCommand("rawobj", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(rawCmd) + flags.StringVar(&cmd.vals, "vals", "", "Pipe-separated key=value properties") + flags.BoolVar(&cmd.signed, "signed", true, "whether to sign the JSON object") + return cmd + }) +} + +func (c *rawCmd) Describe() string { + return "Upload a custom JSON schema blob." +} + +func (c *rawCmd) Usage() { + cmdmain.Errorf("Usage: camput [globalopts] rawobj [rawopts]\n") +} + +func (c *rawCmd) Examples() []string { + return []string{"(debug command)"} +} + +func (c *rawCmd) RunCommand(args []string) error { + if len(args) > 0 { + return errors.New("Raw Object command doesn't take any additional arguments") + } + + if c.vals == "" { + return errors.New("No values") + } + + bb := schema.NewBuilder() + for _, kv := range strings.Split(c.vals, "|") { + kv := strings.SplitN(kv, "=", 2) + bb.SetRawStringField(kv[0], kv[1]) + } + + up := getUploader() + if c.signed { + put, err := up.UploadAndSignBlob(bb) + handleResult("raw-object-signed", put, err) + return err + } + cj, err := bb.JSON() + if err != nil { + return err + } + put, err := up.uploadString(cj) + handleResult("raw-object-unsigned", put, err) + return err +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/remove.go b/vendor/github.com/camlistore/camlistore/cmd/camput/remove.go new file mode 100644 index 00000000..f5db0d03 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/remove.go @@ -0,0 +1,56 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/cmdmain" +) + +type removeCmd struct{} + +func init() { + cmdmain.RegisterCommand("remove", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(removeCmd) + return cmd + }) +} + +func (c *removeCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, `Usage: camput remove + +This command is for debugging only. You're not expected to use it in practice. +`) +} + +func (c *removeCmd) RunCommand(args []string) error { + if len(args) == 0 { + return cmdmain.ErrUsage + } + refs := make([]blob.Ref, 0, len(args)) + for _, s := range args { + br, ok := blob.Parse(s) + if !ok { + return fmt.Errorf("Invalid blobref %q", s) + } + refs = append(refs, br) + } + return getUploader().RemoveBlobs(refs) +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/share.go b/vendor/github.com/camlistore/camlistore/cmd/camput/share.go new file mode 100644 index 00000000..7cf362d4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/share.go @@ -0,0 +1,93 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/search" +) + +type shareCmd struct { + search string + transitive bool + duration time.Duration // zero means forever +} + +func init() { + cmdmain.RegisterCommand("share", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(shareCmd) + flags.StringVar(&cmd.search, "search", "", "share a search result, rather than a single blob. Should be the JSON representation of a search.SearchQuery (see https://camlistore.org/pkg/search/#SearchQuery for details). Exclusive with, and overrides the parameter.") + flags.BoolVar(&cmd.transitive, "transitive", false, "share everything reachable from the given blobref") + flags.DurationVar(&cmd.duration, "duration", 0, "how long the share claim is valid for. The default of 0 means forever. For valid formats, see http://golang.org/pkg/time/#ParseDuration") + return cmd + }) +} + +func (c *shareCmd) Describe() string { + return `Grant access to a resource or search by making a "share" blob.` +} + +func (c *shareCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, `Usage: camput share [opts] [] +`) +} + +func (c *shareCmd) Examples() []string { + return []string{ + "-transitive sha1-83896fcb182db73b653181652129d739280766b5", + `-search='{"expression":"tag:blogphotos is:image","limit":42}'`, + } +} + +func (c *shareCmd) RunCommand(args []string) error { + unsigned := schema.NewShareRef(schema.ShareHaveRef, c.transitive) + + if c.search != "" { + if len(args) != 0 { + return cmdmain.UsageError("when using the -search flag, share takes zero arguments") + } + var q search.SearchQuery + if err := json.Unmarshal([]byte(c.search), &q); err != nil { + return cmdmain.UsageError(fmt.Sprintf("invalid search: %s", err)) + } + unsigned.SetShareSearch(&q) + } else { + if len(args) != 1 { + return cmdmain.UsageError("share takes at most one argument") + } + target, ok := blob.Parse(args[0]) + if !ok { + return cmdmain.UsageError("invalid blobref") + } + unsigned.SetShareTarget(target) + } + + if c.duration != 0 { + unsigned.SetShareExpiration(time.Now().Add(c.duration)) + } + + pr, err := getUploader().UploadAndSignBlob(unsigned) + handleResult("share", pr, err) + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/stat_darwin.go b/vendor/github.com/camlistore/camlistore/cmd/camput/stat_darwin.go new file mode 100644 index 00000000..688b8f64 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/stat_darwin.go @@ -0,0 +1,18 @@ +//+build darwin + +package main + +import ( + "syscall" +) + +func init() { + cleanSysStat = func(si interface{}) interface{} { + st, ok := si.(*syscall.Stat_t) + if !ok { + return si + } + st.Atimespec = syscall.Timespec{} + return st + } +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/stat_linux.go b/vendor/github.com/camlistore/camlistore/cmd/camput/stat_linux.go new file mode 100644 index 00000000..318a1eba --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/stat_linux.go @@ -0,0 +1,22 @@ +//+build linux + +// TODO: move this to somewhere generic in osutil; use it for all +// posix-y operation systems? Or rather, don't clean bad fields, but +// provide a portable way to extract all good fields. + +package main + +import ( + "syscall" +) + +func init() { + cleanSysStat = func(si interface{}) interface{} { + st, ok := si.(*syscall.Stat_t) + if !ok { + return si + } + st.Atim = syscall.Timespec{} + return st + } +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camput/uploader.go b/vendor/github.com/camlistore/camlistore/cmd/camput/uploader.go new file mode 100644 index 00000000..030f479b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camput/uploader.go @@ -0,0 +1,95 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "net/http" + "strings" + + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/client" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/syncutil" +) + +// TODO(mpl): move Uploader to pkg/client, or maybe its own pkg, and clean up files.go + +type Uploader struct { + *client.Client + + // fdGate guards gates the creation of file descriptors. + fdGate *syncutil.Gate + + fileOpts *fileOptions // per-file options; may be nil + + // for debugging; normally nil, but overrides Client if set + // TODO(bradfitz): clean this up? embed a StatReceiver instead + // of a Client? + altStatReceiver blobserver.StatReceiver + + transport *httputil.StatsTransport // for HTTP statistics + pwd string + statCache UploadCache + haveCache HaveCache + + fs http.FileSystem // virtual filesystem to read from; nil means OS filesystem. +} + +// possible options when uploading a file +type fileOptions struct { + permanode bool // create a content-based permanode for each uploaded file + // tag is an optional tag or comma-delimited tags to apply to + // the above permanode. + tag string + // perform for the client the actions needing gpg signing when uploading a file. + vivify bool + exifTime bool // use the time in exif metadata as the modtime if possible. + capCtime bool // use mtime as ctime if ctime > mtime + contentsOnly bool // do not store any of the file's attributes, only its contents. +} + +func (o *fileOptions) tags() []string { + if o == nil || o.tag == "" { + return nil + } + return strings.Split(o.tag, ",") +} + +func (o *fileOptions) wantFilePermanode() bool { + return o != nil && o.permanode +} + +func (o *fileOptions) wantVivify() bool { + return o != nil && o.vivify +} + +func (o *fileOptions) wantCapCtime() bool { + return o != nil && o.capCtime +} + +func (up *Uploader) uploadString(s string) (*client.PutResult, error) { + return up.Upload(client.NewUploadHandleFromString(s)) +} + +func (up *Uploader) Close() error { + var grp syncutil.Group + if up.haveCache != nil { + grp.Go(up.haveCache.Close) + } + grp.Go(up.Client.Close) + return grp.Err() +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/.gitignore b/vendor/github.com/camlistore/camlistore/cmd/camtool/.gitignore new file mode 100644 index 00000000..bb08a8d5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/.gitignore @@ -0,0 +1 @@ +camtool diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/camtool.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/camtool.go new file mode 100644 index 00000000..148dec8a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/camtool.go @@ -0,0 +1,53 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "log" + "net/http" + + "camlistore.org/pkg/client" + "camlistore.org/pkg/cmdmain" +) + +func main() { + cmdmain.Main() +} + +const serverFlagHelp = "Format is is either a URL prefix (with optional path), a host[:port], a config file server alias, or blank to use the Camlistore client config's default server." + +// newClient returns a Camlistore client for the server. +// The server may be: +// * blank, to use the default in the config file +// * an alias, to use that named alias in the config file +// * host:port +// * https?://host[:port][/path] +func newClient(server string) *client.Client { + var cl *client.Client + if server == "" { + cl = client.NewOrFail() + } else { + cl = client.New(server) + if err := cl.SetupAuth(); err != nil { + log.Fatalf("Could not setup auth for connecting to %v: %v", server, err) + } + } + cl.SetHTTPClient(&http.Client{ + Transport: cl.TransportForConfig(nil), + }) + return cl +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/claims.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/claims.go new file mode 100644 index 00000000..09fa4ea6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/claims.go @@ -0,0 +1,79 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/search" +) + +type claimsCmd struct { + server string + attr string +} + +func init() { + cmdmain.RegisterCommand("claims", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(claimsCmd) + flags.StringVar(&cmd.server, "server", "", "Server to fetch claims from. "+serverFlagHelp) + flags.StringVar(&cmd.attr, "attr", "", "Filter claims about a specific attribute. If empty, all claims are returned.") + return cmd + }) +} + +func (c *claimsCmd) Describe() string { + return "Ask the search system to list the claims that modify a permanode." +} + +func (c *claimsCmd) Usage() { + fmt.Fprintf(os.Stderr, "Usage: camtool [globalopts] claims [--depth=n] [--attr=s] permanodeBlobRef\n") +} + +func (c *claimsCmd) Examples() []string { + return []string{} +} + +func (c *claimsCmd) RunCommand(args []string) error { + if len(args) != 1 { + return cmdmain.UsageError("requires 1 blobref") + } + br, ok := blob.Parse(args[0]) + if !ok { + return cmdmain.UsageError("invalid blobref") + } + cl := newClient(c.server) + res, err := cl.GetClaims(&search.ClaimsRequest{ + Permanode: br, + AttrFilter: c.attr, + }) + if err != nil { + return err + } + resj, err := json.MarshalIndent(res, "", " ") + if err != nil { + return err + } + resj = append(resj, '\n') + _, err = os.Stdout.Write(resj) + return err +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/dbinit.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/dbinit.go new file mode 100644 index 00000000..61ca30ca --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/dbinit.go @@ -0,0 +1,300 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "database/sql" + "errors" + "flag" + "fmt" + "os" + "strings" + + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/sorted/mongo" + "camlistore.org/pkg/sorted/mysql" + "camlistore.org/pkg/sorted/postgres" + "camlistore.org/pkg/sorted/sqlite" + + _ "camlistore.org/third_party/github.com/go-sql-driver/mysql" + _ "camlistore.org/third_party/github.com/lib/pq" + "camlistore.org/third_party/labix.org/v2/mgo" +) + +type dbinitCmd struct { + user string + password string + host string + dbName string + dbType string + sslMode string // Postgres SSL mode configuration + + wipe bool + keep bool + wal bool // Write-Ahead Logging for SQLite +} + +func init() { + cmdmain.RegisterCommand("dbinit", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(dbinitCmd) + flags.StringVar(&cmd.user, "user", "root", "Admin user.") + flags.StringVar(&cmd.password, "password", "", "Admin password.") + flags.StringVar(&cmd.host, "host", "localhost", "host[:port]") + flags.StringVar(&cmd.dbName, "dbname", "", "Database to wipe or create. For sqlite, this is the db filename.") + flags.StringVar(&cmd.dbType, "dbtype", "mysql", "Which RDMS to use; possible values: mysql, postgres, sqlite, mongo.") + flags.StringVar(&cmd.sslMode, "sslmode", "require", "Configure SSL mode for postgres. Possible values: require, verify-full, disable.") + + flags.BoolVar(&cmd.wipe, "wipe", false, "Wipe the database and re-create it?") + flags.BoolVar(&cmd.keep, "ignoreexists", false, "Do nothing if database already exists.") + // Defaults to true, because it fixes http://camlistore.org/issues/114 + flags.BoolVar(&cmd.wal, "wal", true, "Enable Write-Ahead Logging with SQLite, for better concurrency. Requires SQLite >= 3.7.0.") + + return cmd + }) +} + +func (c *dbinitCmd) Describe() string { + return "Set up the database for the indexer." +} + +func (c *dbinitCmd) Usage() { + fmt.Fprintf(os.Stderr, "Usage: camtool [globalopts] dbinit [dbinitopts] \n") +} + +func (c *dbinitCmd) Examples() []string { + return []string{ + "-user root -password root -host localhost -dbname camliprod -wipe", + } +} + +func (c *dbinitCmd) RunCommand(args []string) error { + if c.dbName == "" { + return cmdmain.UsageError("--dbname flag required") + } + + if c.dbType != "mysql" && c.dbType != "postgres" && c.dbType != "mongo" { + if c.dbType == "sqlite" { + if !WithSQLite { + return ErrNoSQLite + } + c.wal = c.wal && sqlite.IsWALCapable() + if !c.wal { + fmt.Print("WARNING: An SQLite indexer without Write Ahead Logging will most likely fail. See http://camlistore.org/issues/114\n") + } + } else { + return cmdmain.UsageError(fmt.Sprintf("--dbtype flag: got %v, want %v", c.dbType, `"mysql" or "postgres" or "sqlite", or "mongo"`)) + } + } + + var rootdb *sql.DB + var err error + switch c.dbType { + case "postgres": + conninfo := fmt.Sprintf("user=%s dbname=%s host=%s password=%s sslmode=%s", c.user, "postgres", c.host, c.password, c.sslMode) + rootdb, err = sql.Open("postgres", conninfo) + case "mysql": + rootdb, err = sql.Open("mysql", c.user+":"+c.password+"@/mysql") + } + if err != nil { + exitf("Error connecting to the root %s database: %v", c.dbType, err) + } + + dbname := c.dbName + exists := c.dbExists(rootdb) + if exists { + if c.keep { + return nil + } + if !c.wipe { + return cmdmain.UsageError(fmt.Sprintf("Database %q already exists, but --wipe not given. Stopping.", dbname)) + } + if c.dbType == "mongo" { + return c.wipeMongo() + } + if c.dbType != "sqlite" { + do(rootdb, "DROP DATABASE "+dbname) + } + } + switch c.dbType { + case "sqlite": + _, err := os.Create(dbname) + if err != nil { + exitf("Error creating file %v for sqlite db: %v", dbname, err) + } + case "mongo": + return nil + case "postgres": + // because we want string comparison to work as on MySQL and SQLite. + // in particular we want: 'foo|bar' < 'foo}' (which is not the case with an utf8 collation apparently). + do(rootdb, "CREATE DATABASE "+dbname+" LC_COLLATE = 'C' TEMPLATE = template0") + default: + do(rootdb, "CREATE DATABASE "+dbname) + } + + var db *sql.DB + switch c.dbType { + case "postgres": + conninfo := fmt.Sprintf("user=%s dbname=%s host=%s password=%s sslmode=%s", c.user, dbname, c.host, c.password, c.sslMode) + db, err = sql.Open("postgres", conninfo) + case "sqlite": + db, err = sql.Open("sqlite3", dbname) + default: + db, err = sql.Open("mysql", c.user+":"+c.password+"@/"+dbname) + } + if err != nil { + return fmt.Errorf("Connecting to the %s %s database: %v", dbname, c.dbType, err) + } + + switch c.dbType { + case "postgres": + for _, tableSql := range postgres.SQLCreateTables() { + do(db, tableSql) + } + for _, statement := range postgres.SQLDefineReplace() { + do(db, statement) + } + doQuery(db, fmt.Sprintf(`SELECT replaceintometa('version', '%d')`, postgres.SchemaVersion())) + case "mysql": + if err := mysql.CreateDB(db, dbname); err != nil { + exitf("%v", err) + } + for _, tableSQL := range mysql.SQLCreateTables() { + do(db, tableSQL) + } + do(db, fmt.Sprintf(`REPLACE INTO meta VALUES ('version', '%d')`, mysql.SchemaVersion())) + case "sqlite": + for _, tableSql := range sqlite.SQLCreateTables() { + do(db, tableSql) + } + if c.wal { + do(db, sqlite.EnableWAL()) + } + do(db, fmt.Sprintf(`REPLACE INTO meta VALUES ('version', '%d')`, sqlite.SchemaVersion())) + } + return nil +} + +func do(db *sql.DB, sql string) { + _, err := db.Exec(sql) + if err != nil { + exitf("Error %q running SQL: %q", err, sql) + } +} + +func doQuery(db *sql.DB, sql string) { + r, err := db.Query(sql) + if err == nil { + r.Close() + return + } + exitf("Error %q running SQL: %q", err, sql) +} + +func (c *dbinitCmd) dbExists(db *sql.DB) bool { + query := "SHOW DATABASES" + switch c.dbType { + case "postgres": + query = "SELECT datname FROM pg_database" + case "mysql": + query = "SHOW DATABASES" + case "sqlite": + // There is no point in using sql.Open because it apparently does + // not return an error when the file does not exist. + fi, err := os.Stat(c.dbName) + return err == nil && fi.Size() > 0 + case "mongo": + session, err := c.mongoSession() + if err != nil { + exitf("%v", err) + } + defer session.Close() + n, err := session.DB(c.dbName).C(mongo.CollectionName).Find(nil).Limit(1).Count() + if err != nil { + exitf("%v", err) + } + return n != 0 + } + rows, err := db.Query(query) + check(err) + defer rows.Close() + for rows.Next() { + var db string + check(rows.Scan(&db)) + if db == c.dbName { + return true + } + } + return false +} + +func check(err error) { + if err == nil { + return + } + exitf("SQL error: %v", err) +} + +func exitf(format string, args ...interface{}) { + if !strings.HasSuffix(format, "\n") { + format = format + "\n" + } + cmdmain.Errorf(format, args...) + cmdmain.Exit(1) +} + +var WithSQLite = false + +var ErrNoSQLite = errors.New("the command was not built with SQLite support. See https://code.google.com/p/camlistore/wiki/SQLite" + compileHint()) + +func compileHint() string { + if _, err := os.Stat("/etc/apt"); err == nil { + return " (Required: apt-get install libsqlite3-dev)" + } + return "" +} + +// mongoSession returns an *mgo.Session or nil if c.dbtype is +// not "mongo" or if there was an error. +func (c *dbinitCmd) mongoSession() (*mgo.Session, error) { + if c.dbType != "mongo" { + return nil, nil + } + url := "" + if c.user == "" || c.password == "" { + url = c.host + } else { + url = c.user + ":" + c.password + "@" + c.host + "/" + c.dbName + } + return mgo.Dial(url) +} + +// wipeMongo erases all documents from the mongo collection +// if c.dbType is "mongo". +func (c *dbinitCmd) wipeMongo() error { + if c.dbType != "mongo" { + return nil + } + session, err := c.mongoSession() + if err != nil { + return err + } + defer session.Close() + if _, err := session.DB(c.dbName).C(mongo.CollectionName).RemoveAll(nil); err != nil { + return err + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/debug.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/debug.go new file mode 100644 index 00000000..583fc419 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/debug.go @@ -0,0 +1,82 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "camlistore.org/pkg/cmdmain" +) + +var debugSubModes = map[string]*debugSubMode{ + "splits": &debugSubMode{ + doc: "Show splits of provided file.", + fun: showSplits, + }, + "mime": &debugSubMode{ + doc: "Show MIME type of provided file.", + fun: showMIME, + }, + "exif": &debugSubMode{ + doc: "Show EXIF dump of provided file.", + fun: showEXIF, + }, +} + +type debugSubMode struct { + doc string + fun func(string) +} + +type debugCmd struct{} + +func init() { + cmdmain.RegisterCommand("debug", func(flags *flag.FlagSet) cmdmain.CommandRunner { + return new(debugCmd) + }) +} + +func (c *debugCmd) Describe() string { + return "Show misc meta-info from the given file." +} + +func (c *debugCmd) Usage() { + var subModes, docs string + for k, v := range debugSubModes { + subModes += k + "|" + docs += fmt.Sprintf(" %s: %s\n", k, v.doc) + } + subModes = strings.TrimRight(subModes, "|") + fmt.Fprintf(os.Stderr, + "Usage: camtool [globalopts] debug %s file\n%s", + subModes, docs) +} + +func (c *debugCmd) RunCommand(args []string) error { + if args == nil || len(args) != 2 { + return cmdmain.UsageError("Incorrect number of arguments.") + } + subMode, ok := debugSubModes[args[0]] + if !ok { + return cmdmain.UsageError(fmt.Sprintf("Invalid submode: %v", args[0])) + } + subMode.fun(args[1]) + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/describe.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/describe.go new file mode 100644 index 00000000..0cd389e3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/describe.go @@ -0,0 +1,88 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/search" + "camlistore.org/pkg/types" +) + +type desCmd struct { + server string + depth int +} + +func init() { + cmdmain.RegisterCommand("describe", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(desCmd) + flags.StringVar(&cmd.server, "server", "", "Server to query. "+serverFlagHelp) + flags.IntVar(&cmd.depth, "depth", 1, "Depth to follow in describe request") + return cmd + }) +} + +func (c *desCmd) Describe() string { + return "Ask the search system to describe one or more blobs." +} + +func (c *desCmd) Usage() { + fmt.Fprintf(os.Stderr, "Usage: camtool [globalopts] describe [--depth=n] blobref [blobref, blobref...]\n") +} + +func (c *desCmd) Examples() []string { + return []string{} +} + +func (c *desCmd) RunCommand(args []string) error { + if len(args) == 0 { + return cmdmain.UsageError("requires blobref") + } + var blobs []blob.Ref + for _, arg := range args { + br, ok := blob.Parse(arg) + if !ok { + return cmdmain.UsageError(fmt.Sprintf("invalid blobref %q", arg)) + } + blobs = append(blobs, br) + } + var at time.Time // TODO: implement. from "2 days ago" "-2d", "-2h", "2013-02-05", etc + + cl := newClient(c.server) + res, err := cl.Describe(&search.DescribeRequest{ + BlobRefs: blobs, + Depth: c.depth, + At: types.Time3339(at), + }) + if err != nil { + return err + } + resj, err := json.MarshalIndent(res, "", " ") + if err != nil { + return err + } + resj = append(resj, '\n') + _, err = os.Stdout.Write(resj) + return err +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/disco.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/disco.go new file mode 100644 index 00000000..0dead988 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/disco.go @@ -0,0 +1,63 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + "io" + "os" + + "camlistore.org/pkg/cmdmain" +) + +type discoCmd struct { + server string +} + +func init() { + cmdmain.RegisterCommand("discovery", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(discoCmd) + flags.StringVar(&cmd.server, "server", "", "Server to do discovery against. "+serverFlagHelp) + return cmd + }) +} + +func (c *discoCmd) Describe() string { + return "Perform configuration discovery against a server." +} + +func (c *discoCmd) Usage() { + fmt.Fprintf(os.Stderr, "Usage: camtool [globalopts] discovery") +} + +func (c *discoCmd) Examples() []string { + return []string{} +} + +func (c *discoCmd) RunCommand(args []string) error { + if len(args) > 0 { + return cmdmain.UsageError("doesn't take args") + } + cl := newClient(c.server) + disco, err := cl.DiscoveryDoc() + if err != nil { + return err + } + _, err = io.Copy(os.Stdout, disco) + return err +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/doc.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/doc.go new file mode 100644 index 00000000..c1183dcf --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/doc.go @@ -0,0 +1,57 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +The camtool binary is a collection of commands to help with the use of +a camlistore server. Notably, it can initialize a database for the +indexer, and it can sync blobs between blobservers. + +Usage: + + camtool [globalopts] [commandopts] [commandargs] + +Modes: + + env: Return Camlistore environment information + googinit: Init Google Drive or Google Cloud Storage. + list: List blobs on a server. + claims: Ask the search system to list the claims that modify a permanode. + dumpconfig: Dump the low-level server config from its simple config. + describe: Ask the search system to describe one or more blobs. + discovery: Perform configuration discovery against a server. + reindex-diskpacked: Rebuild the index of the diskpacked blob store + index: Synchronize blobs for all discovered blobs storage - indexer pairs. + sync: Synchronize blobs from a source to a destination. + dbinit: Set up the database for the indexer. + debug: Show misc meta-info from the given file. + +Examples: + + camtool sync --all + camtool sync --src http://localhost:3179/bs/ --dest http://localhost:3179/index-mem/ + + camtool dbinit -user root -password root -host localhost -dbname camliprod -wipe + +For mode-specific help: + + camtool -help + +Global options: + -help=false: print usage + -verbose=false: extra debug logging + -version=false: show version +*/ +package main diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/dp_idx_rebuild.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/dp_idx_rebuild.go new file mode 100644 index 00000000..45edbf5b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/dp_idx_rebuild.go @@ -0,0 +1,128 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "errors" + "flag" + "fmt" + "log" + "os" + + "camlistore.org/pkg/blobserver/diskpacked" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/serverinit" +) + +type reindexdpCmd struct { + overwrite, verbose bool +} + +func init() { + cmdmain.RegisterCommand("reindex-diskpacked", + func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(reindexdpCmd) + flags.BoolVar(&cmd.overwrite, "overwrite", false, + "Whether to overwrite the existing index. If false, only check.") + return cmd + }) +} + +func (c *reindexdpCmd) Describe() string { + return "Rebuild the index of the diskpacked blob store" +} + +func (c *reindexdpCmd) Usage() { + fmt.Fprintln(os.Stderr, "Usage: camtool [globalopts] reindex-diskpacked [reindex-opts]") + fmt.Fprintln(os.Stderr, " camtool reindex-diskpacked [--overwrite] # dir from server config") + fmt.Fprintln(os.Stderr, " camtool reindex-diskpacked [--overwrite] /path/to/directory") +} + +func (c *reindexdpCmd) RunCommand(args []string) error { + var path string + var indexConf jsonconfig.Obj + + switch len(args) { + case 0: + case 1: + path = args[0] + default: + return errors.New("More than 1 argument not allowed") + } + cfg, err := serverinit.LoadFile(osutil.UserServerConfigPath()) + if err != nil { + return err + } + prefixes, ok := cfg.Obj["prefixes"].(map[string]interface{}) + if !ok { + return fmt.Errorf("No 'prefixes' object in low-level (or converted) config file %s", osutil.UserServerConfigPath()) + } + paths, confs := []string{}, []jsonconfig.Obj{} + for prefix, vei := range prefixes { + pmap, ok := vei.(map[string]interface{}) + if !ok { + log.Printf("prefix %q value is a %T, not an object", prefix, vei) + continue + } + pconf := jsonconfig.Obj(pmap) + handlerType := pconf.RequiredString("handler") + handlerArgs := pconf.OptionalObject("handlerArgs") + // no pconf.Validate, as this is a recover tool + if handlerType != "storage-diskpacked" { + continue + } + log.Printf("handlerArgs of %q: %v", prefix, handlerArgs) + if handlerArgs == nil { + log.Printf("no handlerArgs for %q", prefix) + continue + } + aconf := jsonconfig.Obj(handlerArgs) + apath := aconf.RequiredString("path") + // no aconv.Validate, as this is a recover tool + if apath == "" { + log.Printf("path is missing for %q", prefix) + continue + } + if path != "" && path != apath { + continue + } + paths = append(paths, apath) + confs = append(confs, aconf) + } + if len(paths) == 0 { + return fmt.Errorf("Server config file %s doesn't specify a disk-packed storage handler.", + osutil.UserServerConfigPath()) + } + if len(paths) > 1 { + return fmt.Errorf("Ambiguity. Server config file %s d specify more than 1 disk-packed storage handler. Please specify one of: %v", osutil.UserServerConfigPath(), paths) + } + path = paths[0] + if path == "" { + return errors.New("no path is given/found") + } + // If no index is specified, the default will be used (as on the regular path). + if mi := confs[0]["metaIndex"]; mi != nil { + if mi, ok := mi.(map[string]interface{}); ok { + indexConf = jsonconfig.Obj(mi) + } + } + log.Printf("indexConf: %v", indexConf) + + return diskpacked.Reindex(path, c.overwrite, indexConf) +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/dumpconfig.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/dumpconfig.go new file mode 100644 index 00000000..c82caada --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/dumpconfig.go @@ -0,0 +1,67 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/json" + "errors" + "flag" + "os" + + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/osutil" + _ "camlistore.org/pkg/osutil/gce" + "camlistore.org/pkg/serverinit" +) + +type dumpconfigCmd struct{} + +func init() { + cmdmain.RegisterCommand("dumpconfig", func(flags *flag.FlagSet) cmdmain.CommandRunner { + return new(dumpconfigCmd) + }) +} + +func (c *dumpconfigCmd) Describe() string { + return "Dump the low-level server config from its simple config." +} + +func (c *dumpconfigCmd) Usage() { +} + +func (c *dumpconfigCmd) RunCommand(args []string) error { + var file string + switch { + case len(args) == 0: + file = osutil.UserServerConfigPath() + case len(args) == 1: + file = args[0] + default: + return errors.New("More than 1 argument not allowed") + } + cfg, err := serverinit.LoadFile(file) + if err != nil { + return err + } + cfg.Obj["handlerConfig"] = true + ll, err := json.MarshalIndent(cfg.Obj, "", " ") + if err != nil { + return err + } + _, err = os.Stdout.Write(ll) + return err +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/env.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/env.go new file mode 100644 index 00000000..dc5e015e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/env.go @@ -0,0 +1,77 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/osutil" +) + +var envMap = map[string]func() string{ + "configdir": osutil.CamliConfigDir, + "clientconfig": osutil.UserClientConfigPath, + "serverconfig": osutil.UserServerConfigPath, + "camsrcroot": srcRoot, +} + +type envCmd struct{} + +func init() { + cmdmain.RegisterCommand("env", func(flags *flag.FlagSet) cmdmain.CommandRunner { + return new(envCmd) + }) +} + +func (c *envCmd) Describe() string { + return "Return Camlistore environment information" +} + +func (c *envCmd) Usage() { + fmt.Fprintf(os.Stderr, "camtool env [key]\n") +} + +func (c *envCmd) RunCommand(args []string) error { + if len(args) == 0 { + for k, fn := range envMap { + fmt.Printf("%s: %s\n", k, fn()) + } + return nil + } + if len(args) > 1 { + return cmdmain.UsageError("only 0 or 1 arguments allowed") + } + fn := envMap[args[0]] + if fn == nil { + return fmt.Errorf("unknown environment key %q", args[0]) + } + fmt.Println(fn()) + return nil +} + +func srcRoot() string { + for _, dir := range filepath.SplitList(os.Getenv("GOPATH")) { + if d := filepath.Join(dir, "src", "camlistore.org"); osutil.DirExists(d) { + return d + } + } + return "" +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/exif.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/exif.go new file mode 100644 index 00000000..01d14e43 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/exif.go @@ -0,0 +1,49 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "log" + "os" + + "camlistore.org/third_party/github.com/rwcarlsen/goexif/exif" +) + +func showEXIF(file string) { + f, err := os.Open(file) + if err != nil { + panic(err.Error()) + } + defer f.Close() + ex, err := exif.Decode(f) + if err != nil { + if exif.IsCriticalError(err) { + log.Fatalf("exif.Decode, critical error: %v", err) + } + log.Printf("exif.Decode, warning: %v", err) + } + fmt.Printf("%v\n", ex) + if exif.IsExifError(err) { + // the error happened while decoding the EXIF sub-IFD, so as DateTime is + // part of it, we have to assume (until there's a better "decode effort" + // strategy in goexif) that it's not usable. + return + } + ct, err := ex.DateTime() + fmt.Printf("exif.DateTime = %v, %v\n", ct, err) +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/googinit.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/googinit.go new file mode 100644 index 00000000..76128863 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/googinit.go @@ -0,0 +1,131 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "encoding/json" + "flag" + "fmt" + "strings" + + "camlistore.org/pkg/blobserver/google/drive" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/constants/google" + "camlistore.org/pkg/googlestorage" + "camlistore.org/pkg/oauthutil" + + "golang.org/x/oauth2" +) + +type googinitCmd struct { + storageType string +} + +func init() { + cmdmain.RegisterCommand("googinit", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(googinitCmd) + flags.StringVar(&cmd.storageType, "type", "", "Storage type: drive or cloud") + return cmd + }) +} + +func (c *googinitCmd) Describe() string { + return "Init Google Drive or Google Cloud Storage." +} + +func (c *googinitCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, "Usage: camtool [globalopts] googinit [commandopts] \n") +} + +func (c *googinitCmd) RunCommand(args []string) error { + var ( + err error + clientId string + clientSecret string + oauthConfig *oauth2.Config + ) + + if c.storageType != "drive" && c.storageType != "cloud" { + return cmdmain.UsageError("Invalid storage type: must be drive for Google Drive or cloud for Google Cloud Storage.") + } + + clientId, clientSecret = getClientInfo() + + switch c.storageType { + case "drive": + oauthConfig = &oauth2.Config{ + Scopes: []string{drive.Scope}, + Endpoint: google.Endpoint, + ClientID: clientId, + ClientSecret: clientSecret, + RedirectURL: oauthutil.TitleBarRedirectURL, + } + case "cloud": + oauthConfig = &oauth2.Config{ + Scopes: []string{googlestorage.Scope}, + Endpoint: google.Endpoint, + ClientID: clientId, + ClientSecret: clientSecret, + RedirectURL: oauthutil.TitleBarRedirectURL, + } + } + + token, err := oauth2.ReuseTokenSource(nil, &oauthutil.TokenSource{ + Config: oauthConfig, + AuthCode: func() string { + fmt.Fprintf(cmdmain.Stdout, "Get auth code from:\n\n") + fmt.Fprintf(cmdmain.Stdout, "%v\n\n", oauthConfig.AuthCodeURL("", oauth2.AccessTypeOffline, oauth2.ApprovalForce)) + return prompt("Enter auth code:") + }, + }).Token() + if err != nil { + return fmt.Errorf("could not acquire token: %v", err) + } + + fmt.Fprintf(cmdmain.Stdout, "\nYour Google auth object:\n\n") + enc := json.NewEncoder(cmdmain.Stdout) + authObj := map[string]string{ + "client_id": clientId, + "client_secret": clientSecret, + "refresh_token": token.RefreshToken, + } + enc.Encode(authObj) + fmt.Fprint(cmdmain.Stdout, "\n") + return nil +} + +// Prompt the user for an input line. Return the given input. +func prompt(promptText string) string { + fmt.Fprint(cmdmain.Stdout, promptText) + sc := bufio.NewScanner(cmdmain.Stdin) + sc.Scan() + return strings.TrimSpace(sc.Text()) +} + +// Prompt for client id / secret +func getClientInfo() (string, string) { + fmt.Fprintf(cmdmain.Stdout, "Please provide the client id and client secret \n") + fmt.Fprintf(cmdmain.Stdout, "(You can find these at http://code.google.com/apis/console > your project > API Access)\n") + var ( + clientId string + clientSecret string + ) + clientId = prompt("Client ID:") + clientSecret = prompt("Client Secret:") + return clientId, clientSecret +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/index.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/index.go new file mode 100644 index 00000000..256eaee1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/index.go @@ -0,0 +1,86 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + "os" + "strconv" + + "camlistore.org/pkg/client" + "camlistore.org/pkg/cmdmain" +) + +type indexCmd struct { + verbose bool + wipe bool + insecureTLS bool +} + +func init() { + cmdmain.RegisterCommand("index", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(indexCmd) + flags.BoolVar(&cmd.verbose, "verbose", false, "Be verbose.") + flags.BoolVar(&cmd.wipe, "wipe", false, "Erase and recreate all discovered indexes. NOOP for now.") + if debug, _ := strconv.ParseBool(os.Getenv("CAMLI_DEBUG")); debug { + flags.BoolVar(&cmd.insecureTLS, "insecure", false, "If set, when using TLS, the server's certificates verification is disabled, and they are not checked against the trustedCerts in the client configuration either.") + } + return cmd + }) +} + +func (c *indexCmd) Describe() string { + return "Synchronize blobs for all discovered blobs storage - indexer pairs." +} + +func (c *indexCmd) Usage() { + fmt.Fprintf(os.Stderr, "Usage: camtool [globalopts] index [indexopts] \n") +} + +func (c *indexCmd) RunCommand(args []string) error { + dc := c.discoClient() + syncHandlers, err := dc.SyncHandlers() + if err != nil { + return fmt.Errorf("sync handlers discovery failed: %v", err) + } + + for _, sh := range syncHandlers { + if sh.ToIndex { + if err := c.sync(sh.From, sh.To); err != nil { + return fmt.Errorf("Error while indexing from %v to %v: %v", sh.From, sh.To, err) + } + } + } + return nil +} + +func (c *indexCmd) sync(from, to string) error { + return (&syncCmd{ + src: from, + dest: to, + verbose: c.verbose, + wipe: c.wipe, + }).RunCommand(nil) +} + +// discoClient returns a client initialized with a server +// based from the configuration file. The returned client +// can then be used to discover the blobRoot and syncHandlers. +func (c *indexCmd) discoClient() *client.Client { + return newClient("") +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/list.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/list.go new file mode 100644 index 00000000..b159a1d9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/list.go @@ -0,0 +1,166 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "flag" + "fmt" + "log" + "os" + "strings" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/client" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/search" +) + +type listCmd struct { + *syncCmd + + describe bool // whether to describe each blob. + cl *client.Client // client used for the describe requests. +} + +func init() { + cmdmain.RegisterCommand("list", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := &listCmd{ + syncCmd: &syncCmd{ + dest: "stdout", + }, + describe: false, + } + flags.StringVar(&cmd.syncCmd.src, "src", "", "Source blobserver is either a URL prefix (with optional path), a host[:port], a path (starting with /, ./, or ../), or blank to use the Camlistore client config's default host.") + flags.BoolVar(&cmd.verbose, "verbose", false, "Be verbose.") + flags.BoolVar(&cmd.describe, "describe", false, "Use describe requests to get each blob's type. Requires a source server with a search endpoint. Mostly used for demos. Requires many extra round-trips to the server currently.") + return cmd + }) +} + +const describeBatchSize = 50 + +func (c *listCmd) Describe() string { + return "List blobs on a server." +} + +func (c *listCmd) Usage() { + fmt.Fprintf(os.Stderr, "Usage: camtool [globalopts] list [listopts] \n") +} + +func (c *listCmd) Examples() []string { + return nil +} + +func (c *listCmd) RunCommand(args []string) error { + if !c.describe { + return c.syncCmd.RunCommand(args) + } + + stdout := cmdmain.Stdout + defer func() { cmdmain.Stdout = stdout }() + pr, pw, err := os.Pipe() + if err != nil { + return fmt.Errorf("Could not create pipe to read from stdout: %v", err) + } + defer pr.Close() + cmdmain.Stdout = pw + + if err := c.setClient(); err != nil { + return err + } + + scanner := bufio.NewScanner(pr) + go func() { + err := c.syncCmd.RunCommand(args) + if err != nil { + log.Printf("Error when enumerating source with sync: %v", err) + } + pw.Close() + }() + + blobRefs := make([]blob.Ref, 0, describeBatchSize) + describe := func() error { + if len(blobRefs) == 0 { + return nil + } + // TODO(mpl): setting depth to 1, not 0, because otherwise r.depth() in pkg/search/handler.go defaults to 4. Can't remember why we disallowed 0 right now, and I do not want to change that in pkg/search/handler.go and risk breaking things. + described, err := c.cl.Describe(&search.DescribeRequest{ + BlobRefs: blobRefs, + Depth: 1, + }) + if err != nil { + return fmt.Errorf("Error when describing blobs %v: %v", blobRefs, err) + } + for _, v := range blobRefs { + blob, ok := described.Meta[v.String()] + if !ok { + // This can happen if the index is out of sync with the storage we enum from. + fmt.Fprintf(stdout, "%v \n", v) + continue + } + detailed := detail(blob) + if detailed != "" { + detailed = fmt.Sprintf("\t%v", detailed) + } + fmt.Fprintf(stdout, "%v %v%v\n", v, blob.Size, detailed) + } + blobRefs = make([]blob.Ref, 0, describeBatchSize) + return nil + } + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) != 2 { + return fmt.Errorf("Bogus output from sync: got %q, wanted \"blobref size\"", scanner.Text()) + } + blobRefs = append(blobRefs, blob.MustParse(fields[0])) + if len(blobRefs) == describeBatchSize { + if err := describe(); err != nil { + return err + } + } + } + if err := describe(); err != nil { + return err + } + if err := scanner.Err(); err != nil { + return fmt.Errorf("Error reading on pipe from stdout: %v", err) + } + return nil +} + +// setClient configures a client for c, for the describe requests. +func (c *listCmd) setClient() error { + ss, err := c.syncCmd.storageFromParam("src", c.syncCmd.src) + if err != nil { + fmt.Errorf("Could not set client for describe requests: %v", err) + } + var ok bool + c.cl, ok = ss.(*client.Client) + if !ok { + return fmt.Errorf("storageFromParam returned a %T, was expecting a *client.Client", ss) + } + return nil +} + +func detail(blob *search.DescribedBlob) string { + // TODO(mpl): attrType, value for claim. but I don't think they're accessible just with a describe req. + if blob.CamliType == "file" { + return fmt.Sprintf("%v (%v size=%v)", blob.CamliType, blob.File.FileName, blob.File.Size) + } + return blob.CamliType +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/makestatic.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/makestatic.go new file mode 100644 index 00000000..75d11ac0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/makestatic.go @@ -0,0 +1,131 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/search" +) + +type makeStaticCmd struct { + server string +} + +func init() { + cmdmain.RegisterCommand("makestatic", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(makeStaticCmd) + flags.StringVar(&cmd.server, "server", "", "Server to search. "+serverFlagHelp) + return cmd + }) +} + +func (c *makeStaticCmd) Describe() string { + return "Creates a static directory from a permanode set" +} + +func (c *makeStaticCmd) Usage() { + fmt.Fprintf(os.Stderr, "Usage: camtool [globalopts] makestatic [permanode]\n") +} + +func (c *makeStaticCmd) Examples() []string { + return []string{} +} + +func (c *makeStaticCmd) RunCommand(args []string) error { + if len(args) != 1 { + return cmdmain.UsageError("requires a permanode") + } + pn, ok := blob.Parse(args[0]) + if !ok { + return cmdmain.UsageError("invalid permanode argument") + } + + cl := newClient(c.server) + res, err := cl.Describe(&search.DescribeRequest{ + BlobRefs: []blob.Ref{pn}, + Rules: []*search.DescribeRule{ + { + IfResultRoot: true, + Attrs: []string{"camliMember"}, + Rules: []*search.DescribeRule{ + {Attrs: []string{"camliContent"}}, + }, + }, + }, + }) + if err != nil { + return err + } + + camliType := func(ref string) string { + m := res.Meta[ref] + if m == nil { + return "" + } + return m.CamliType + } + + var ss schema.StaticSet + pnDes, ok := res.Meta[pn.String()] + if !ok { + return fmt.Errorf("permanode %v not described", pn) + } + if pnDes.Permanode == nil { + return fmt.Errorf("blob %v is not a permanode", pn) + } + members := pnDes.Permanode.Attr["camliMember"] + if len(members) == 0 { + return fmt.Errorf("permanode %v has no camliMember attributes", pn) + } + for _, fileRefStr := range members { + if camliType(fileRefStr) != "permanode" { + continue + } + contentRef := res.Meta[fileRefStr].Permanode.Attr.Get("camliContent") + if contentRef == "" { + continue + } + if camliType(contentRef) == "file" { + ss.Add(blob.MustParse(contentRef)) + } + } + + b := ss.Blob() + _, err = cl.UploadBlob(b) + if err != nil { + return err + } + title := pnDes.Title() + title = strings.Replace(title, string(os.PathSeparator), "", -1) + if title == "" { + title = pn.String() + } + dir := schema.NewDirMap(title).PopulateDirectoryMap(b.BlobRef()) + dirBlob := dir.Blob() + _, err = cl.UploadBlob(dirBlob) + if err == nil { + fmt.Println(dirBlob.BlobRef().String()) + } + return err +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/mime.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/mime.go new file mode 100644 index 00000000..3e6ea2f0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/mime.go @@ -0,0 +1,34 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "log" + "os" + + "camlistore.org/pkg/magic" +) + +func showMIME(file string) { + f, err := os.Open(file) + if err != nil { + log.Fatal(err) + } + mime, _ := magic.MIMETypeFromReader(f) + fmt.Println(mime) +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/packblobs.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/packblobs.go new file mode 100644 index 00000000..eccc663f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/packblobs.go @@ -0,0 +1,107 @@ +/* +Copyright 2015 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "flag" + "fmt" + "io" + "log" + "os" + + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/search" +) + +type packBlobsCmd struct { + server string +} + +func init() { + cmdmain.RegisterCommand("packblobs", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(packBlobsCmd) + flags.StringVar(&cmd.server, "server", "", "Server to search. "+serverFlagHelp) + return cmd + }) +} + +func (c *packBlobsCmd) Describe() string { + return "Pack related blobs together (migration tool)" +} + +func (c *packBlobsCmd) Usage() { + fmt.Fprintf(os.Stderr, "Usage: camtool [globalopts] packblobs\n") +} + +func (c *packBlobsCmd) Examples() []string { + return []string{} +} + +func (c *packBlobsCmd) RunCommand(args []string) error { + if len(args) != 0 { + return cmdmain.UsageError("doesn't take arguments") + } + req := &search.SearchQuery{ + Limit: -1, + Sort: search.BlobRefAsc, + Constraint: &search.Constraint{ + File: &search.FileConstraint{ + FileSize: &search.IntConstraint{ + Min: 512 << 10, + }, + }, + }, + } + cl := newClient(c.server) + looseClient := cl.NewPathClient("/bs-loose/") + + res, err := cl.Query(req) + if err != nil { + return err + } + total := len(res.Blobs) + n := 0 + var buf bytes.Buffer + for _, sr := range res.Blobs { + n++ + fileRef := sr.Blob + rc, _, err := looseClient.Fetch(fileRef) + if err == os.ErrNotExist { + fmt.Printf("%d/%d: %v already done\n", n, total, fileRef) + continue + } + if err != nil { + log.Printf("error fetching %v: %v\n", fileRef, err) + continue + } + buf.Reset() + _, err = io.Copy(&buf, rc) + rc.Close() + if err != nil { + log.Printf("error reading %v: %v\n", fileRef, err) + continue + } + _, err = cl.ReceiveBlob(fileRef, &buf) + if err != nil { + log.Printf("error write %v: %v\n", fileRef, err) + continue + } + fmt.Printf("%d/%d: %v\n", n, total, fileRef) + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/search.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/search.go new file mode 100644 index 00000000..9e4596dc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/search.go @@ -0,0 +1,116 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + "strings" + + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/search" + "camlistore.org/pkg/strutil" +) + +type searchCmd struct { + server string + limit int + describe bool + rawQuery bool +} + +func init() { + cmdmain.RegisterCommand("search", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(searchCmd) + flags.StringVar(&cmd.server, "server", "", "Server to search. "+serverFlagHelp) + flags.IntVar(&cmd.limit, "limit", 0, "Limit number of results. 0 is default. Negative means no limit.") + flags.BoolVar(&cmd.describe, "describe", false, "Describe results as well.") + flags.BoolVar(&cmd.rawQuery, "rawquery", false, "If true, the provided JSON is a SearchQuery, and not a Constraint. In this case, the -limit flag if non-zero is applied after parsing the JSON.") + return cmd + }) +} + +func (c *searchCmd) Describe() string { + return "Execute a search query" +} + +func (c *searchCmd) Usage() { + fmt.Fprintf(os.Stderr, "Usage: camtool [globalopts] search \n") +} + +func (c *searchCmd) Examples() []string { + return []string{ + `"loc:paris is:portrait" # expression`, + `'{"blobrefPrefix":"sha1-f00d"}' # SearchConstraint JSON`, + `- # piped from stdin`, + } +} + +func (c *searchCmd) RunCommand(args []string) error { + if len(args) != 1 { + return cmdmain.UsageError("requires search expression or Constraint JSON") + } + q := args[0] + if q == "-" { + slurp, err := ioutil.ReadAll(cmdmain.Stdin) + if err != nil { + return err + } + q = string(slurp) + } + q = strings.TrimSpace(q) + + req := &search.SearchQuery{ + Limit: c.limit, + } + if c.rawQuery { + req.Limit = 0 // clear it if they provided it + if err := json.NewDecoder(strings.NewReader(q)).Decode(&req); err != nil { + return err + } + if c.limit != 0 { + req.Limit = c.limit + } + } else if strutil.IsPlausibleJSON(q) { + cs := new(search.Constraint) + if err := json.NewDecoder(strings.NewReader(q)).Decode(&cs); err != nil { + return err + } + req.Constraint = cs + } else { + req.Expression = q + } + if c.describe { + req.Describe = &search.DescribeRequest{} + } + + cl := newClient(c.server) + res, err := cl.Query(req) + if err != nil { + return err + } + resj, err := json.MarshalIndent(res, "", " ") + if err != nil { + return err + } + resj = append(resj, '\n') + _, err = os.Stdout.Write(resj) + return err +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/searchdoc.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/searchdoc.go new file mode 100644 index 00000000..acb22c45 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/searchdoc.go @@ -0,0 +1,83 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "text/tabwriter" + + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/search" +) + +type searchDocCmd struct{} + +func init() { + cmdmain.RegisterCommand("searchdoc", func(flags *flag.FlagSet) cmdmain.CommandRunner { + return new(searchDocCmd) + }) +} + +func (c *searchDocCmd) Describe() string { + return "Provide help on the predicates for search expressions" +} + +func (c *searchDocCmd) Usage() { + cmdmain.Errorf("camtool searchdoc") +} + +func (c *searchDocCmd) RunCommand(args []string) error { + if len(args) > 0 { + return cmdmain.UsageError("No arguments allowed") + } + + formattedSearchHelp() + return nil +} + +func formattedSearchHelp() { + s := search.SearchHelp() + type help struct{ Name, Description string } + h := []help{} + err := json.Unmarshal([]byte(s), &h) + if err != nil { + cmdmain.Errorf("%v", err) + os.Exit(1) + } + + w := new(tabwriter.Writer) + w.Init(cmdmain.Stdout, 0, 8, 0, '\t', 0) + fmt.Fprintln(w, "Predicates for search expressions") + fmt.Fprintln(w) + fmt.Fprintln(w, "Predicate\tDescription") + fmt.Fprintln(w) + for _, predicate := range h { + desc := strings.Split(predicate.Description, "\n") + for i, d := range desc { + if i == 0 { + fmt.Fprintf(w, "%s\t%s\n", predicate.Name, d) + } else { + fmt.Fprintf(w, "\t%s\n", d) + } + } + } + w.Flush() +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/splits.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/splits.go new file mode 100644 index 00000000..515bf6bc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/splits.go @@ -0,0 +1,96 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "strings" + + "camlistore.org/pkg/rollsum" +) + +type span struct { + from, to int64 + bits int + children []span +} + +func showSplits(file string) { + f, err := os.Open(file) + if err != nil { + panic(err.Error()) + } + bufr := bufio.NewReader(f) + + spans := []span{} + rs := rollsum.New() + n := int64(0) + last := n + + for { + c, err := bufr.ReadByte() + if err != nil { + if err == io.EOF { + if n != last { + spans = append(spans, span{from: last, to: n}) + } + break + } + panic(err.Error()) + } + n++ + rs.Roll(c) + if rs.OnSplit() { + bits := rs.Bits() + sliceFrom := len(spans) + for sliceFrom > 0 && spans[sliceFrom-1].bits < bits { + sliceFrom-- + } + nCopy := len(spans) - sliceFrom + var children []span + if nCopy > 0 { + children = make([]span, nCopy) + nCopied := copy(children, spans[sliceFrom:]) + if nCopied != nCopy { + panic("n wrong") + } + spans = spans[:sliceFrom] + } + spans = append(spans, span{from: last, to: n, bits: bits, children: children}) + + log.Printf("split at %d (after %d), bits=%d", n, n-last, bits) + last = n + } + } + + var dumpSpans func(s []span, indent int) + dumpSpans = func(s []span, indent int) { + in := strings.Repeat(" ", indent) + for _, sp := range s { + fmt.Printf("%sfrom=%d, to=%d (len %d) bits=%d\n", in, sp.from, sp.to, sp.to-sp.from, sp.bits) + if len(sp.children) > 0 { + dumpSpans(sp.children, indent+4) + } + } + } + dumpSpans(spans, 0) + fmt.Printf("\n\nNOTE NOTE NOTE: the camdebug tool hasn't been updated to use the splitting policy from pkg/schema/filewriter.go.") +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/sqlite_cond.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/sqlite_cond.go new file mode 100644 index 00000000..5339361a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/sqlite_cond.go @@ -0,0 +1,27 @@ +// +build with_sqlite + +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + _ "camlistore.org/third_party/github.com/mattn/go-sqlite3" +) + +func init() { + WithSQLite = true +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/sync.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/sync.go new file mode 100644 index 00000000..bcc0386d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/sync.go @@ -0,0 +1,456 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + "strconv" + "strings" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/blobserver/localdisk" + "camlistore.org/pkg/client" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/context" +) + +type syncCmd struct { + src string + dest string + third string + srcKeyID string // GPG public key ID of the source server, if supported. + destKeyID string // GPG public key ID of the destination server, if supported. + + loop bool + verbose bool + all bool + removeSrc bool + wipe bool + insecureTLS bool + oneIsDisk bool // Whether one of src or dest is a local disk. + + logger *log.Logger +} + +func init() { + cmdmain.RegisterCommand("sync", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(syncCmd) + flags.StringVar(&cmd.src, "src", "", "Source blobserver. "+serverFlagHelp) + flags.StringVar(&cmd.dest, "dest", "", "Destination blobserver (same format as src), or 'stdout' to just enumerate the --src blobs to stdout.") + flags.StringVar(&cmd.third, "thirdleg", "", "Copy blobs present in source but missing from destination to this 'third leg' blob store, instead of the destination. (same format as src)") + + flags.BoolVar(&cmd.loop, "loop", false, "Create an associate a new permanode for the uploaded file or directory.") + flags.BoolVar(&cmd.verbose, "verbose", false, "Be verbose.") + flags.BoolVar(&cmd.wipe, "wipe", false, "If dest is an index, drop it and repopulate it from scratch. NOOP for now.") + flags.BoolVar(&cmd.all, "all", false, "Discover all sync destinations configured on the source server and run them.") + flags.BoolVar(&cmd.removeSrc, "removesrc", false, "Remove each blob from the source after syncing to the destination; for queue processing.") + // TODO(mpl): maybe move this flag up to the client pkg as an AddFlag, as it can be used by all commands. + if debug, _ := strconv.ParseBool(os.Getenv("CAMLI_DEBUG")); debug { + flags.BoolVar(&cmd.insecureTLS, "insecure", false, "If set, when using TLS, the server's certificates verification is disabled, and they are not checked against the trustedCerts in the client configuration either.") + } + + return cmd + }) +} + +func (c *syncCmd) Describe() string { + return "Synchronize blobs from a source to a destination." +} + +func (c *syncCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, "Usage: camtool [globalopts] sync [syncopts] \n") +} + +func (c *syncCmd) Examples() []string { + return []string{ + "--all", + "--src http://localhost:3179/bs/ --dest http://localhost:3179/index-mem/", + } +} + +func (c *syncCmd) RunCommand(args []string) error { + if c.loop && !c.removeSrc { + return cmdmain.UsageError("Can't use --loop without --removesrc") + } + if c.verbose { + c.logger = log.New(cmdmain.Stderr, "", 0) // else nil + } + if c.all { + err := c.syncAll() + if err != nil { + return fmt.Errorf("sync all failed: %v", err) + } + return nil + } + + ss, err := c.storageFromParam("src", c.src) + if err != nil { + return err + } + ds, err := c.storageFromParam("dest", c.dest) + if err != nil { + return err + } + ts, err := c.storageFromParam("thirdleg", c.third) + if err != nil { + return err + } + + differentKeyIDs := fmt.Sprintf("WARNING: the source server GPG key ID (%v) and the destination's (%v) differ. All blobs will be synced, but because the indexer at the other side is indexing claims by a different user, you may not see what you expect in that server's web UI, etc.", c.srcKeyID, c.destKeyID) + + if c.dest != "stdout" && !c.oneIsDisk && c.srcKeyID != c.destKeyID { // both blank is ok. + // Warn at the top (and hope the user sees it and can abort if it was a mistake): + fmt.Fprintln(cmdmain.Stderr, differentKeyIDs) + // Warn also at the end (in case the user missed the first one) + defer fmt.Fprintln(cmdmain.Stderr, differentKeyIDs) + } + + passNum := 0 + for { + passNum++ + stats, err := c.doPass(ss, ds, ts) + if c.verbose { + log.Printf("sync stats - pass: %d, blobs: %d, bytes %d\n", passNum, stats.BlobsCopied, stats.BytesCopied) + } + if err != nil { + return fmt.Errorf("sync failed: %v", err) + } + if !c.loop { + break + } + } + return nil +} + +// A storageType is one of "src", "dest", or "thirdleg". These match the flag names. +type storageType string + +const ( + storageSource storageType = "src" + storageDest storageType = "dest" + storageThird storageType = "thirdleg" +) + +// which is one of "src", "dest", or "thirdleg" +func (c *syncCmd) storageFromParam(which storageType, val string) (blobserver.Storage, error) { + var httpClient *http.Client + + if val == "" { + switch which { + case storageThird: + return nil, nil + case storageSource: + discl := c.discoClient() + discl.SetLogger(c.logger) + src, err := discl.BlobRoot() + if err != nil { + return nil, fmt.Errorf("Failed to discover source server's blob path: %v", err) + } + val = src + httpClient = discl.HTTPClient() + } + if val == "" { + return nil, cmdmain.UsageError("No --" + string(which) + " flag value specified") + } + } + if which == storageDest && val == "stdout" { + return nil, nil + } + if looksLikePath(val) { + disk, err := localdisk.New(val) + if err != nil { + return nil, fmt.Errorf("Interpreted --%v=%q as a local disk path, but got error: %v", which, val, err) + } + c.oneIsDisk = true + return disk, nil + } + cl := client.New(val) + cl.InsecureTLS = c.insecureTLS + if httpClient == nil { + httpClient = &http.Client{ + Transport: cl.TransportForConfig(nil), + } + } + cl.SetHTTPClient(httpClient) + if err := cl.SetupAuth(); err != nil { + return nil, fmt.Errorf("could not setup auth for connecting to %v: %v", val, err) + } + cl.SetLogger(c.logger) + serverKeyID, err := cl.ServerKeyID() + if err != nil && err != client.ErrNoSigning { + fmt.Fprintf(cmdmain.Stderr, "Failed to discover keyId for server %v: %v", val, err) + } else { + if which == storageSource { + c.srcKeyID = serverKeyID + } else if which == storageDest { + c.destKeyID = serverKeyID + } + } + return cl, nil +} + +func looksLikePath(v string) bool { + prefix := func(s string) bool { return strings.HasPrefix(v, s) } + return prefix("./") || prefix("/") || prefix("../") +} + +type SyncStats struct { + BlobsCopied int + BytesCopied int64 + ErrorCount int +} + +func (c *syncCmd) syncAll() error { + if c.loop { + return cmdmain.UsageError("--all can't be used with --loop") + } + if c.third != "" { + return cmdmain.UsageError("--all can't be used with --thirdleg") + } + if c.dest != "" { + return cmdmain.UsageError("--all can't be used with --dest") + } + + dc := c.discoClient() + dc.SetLogger(c.logger) + syncHandlers, err := dc.SyncHandlers() + if err != nil { + return fmt.Errorf("sync handlers discovery failed: %v", err) + } + if c.verbose { + log.Printf("To be synced:\n") + for _, sh := range syncHandlers { + log.Printf("%v -> %v", sh.From, sh.To) + } + } + for _, sh := range syncHandlers { + from := client.New(sh.From) + from.SetLogger(c.logger) + from.InsecureTLS = c.insecureTLS + from.SetHTTPClient(&http.Client{ + Transport: from.TransportForConfig(nil), + }) + if err := from.SetupAuth(); err != nil { + return fmt.Errorf("could not setup auth for connecting to %v: %v", sh.From, err) + } + to := client.New(sh.To) + to.SetLogger(c.logger) + to.InsecureTLS = c.insecureTLS + to.SetHTTPClient(&http.Client{ + Transport: to.TransportForConfig(nil), + }) + if err := to.SetupAuth(); err != nil { + return fmt.Errorf("could not setup auth for connecting to %v: %v", sh.To, err) + } + if c.verbose { + log.Printf("Now syncing: %v -> %v", sh.From, sh.To) + } + stats, err := c.doPass(from, to, nil) + if c.verbose { + log.Printf("sync stats, blobs: %d, bytes %d\n", stats.BlobsCopied, stats.BytesCopied) + } + if err != nil { + return err + } + } + return nil +} + +// discoClient returns a client initialized with a server +// based from --src or from the configuration file if --src +// is blank. The returned client can then be used to discover +// the blobRoot and syncHandlers. +func (c *syncCmd) discoClient() *client.Client { + cl := newClient(c.src) + cl.SetLogger(c.logger) + cl.InsecureTLS = c.insecureTLS + return cl +} + +func enumerateAllBlobs(ctx *context.Context, s blobserver.Storage, destc chan<- blob.SizedRef) error { + // Use *client.Client's support for enumerating all blobs if + // possible, since it could probably do a better job knowing + // HTTP boundaries and such. + if c, ok := s.(*client.Client); ok { + return c.SimpleEnumerateBlobs(ctx, destc) + } + + defer close(destc) + return blobserver.EnumerateAll(ctx, s, func(sb blob.SizedRef) error { + select { + case destc <- sb: + case <-ctx.Done(): + return context.ErrCanceled + } + return nil + }) +} + +// src: non-nil source +// dest: non-nil destination +// thirdLeg: optional third-leg client. if not nil, anything on src +// but not on dest will instead be copied to thirdLeg, instead of +// directly to dest. (sneakernet mode, copying to a portable drive +// and transporting thirdLeg to dest) +func (c *syncCmd) doPass(src, dest, thirdLeg blobserver.Storage) (stats SyncStats, retErr error) { + srcBlobs := make(chan blob.SizedRef, 100) + destBlobs := make(chan blob.SizedRef, 100) + srcErr := make(chan error, 1) + destErr := make(chan error, 1) + + ctx := context.TODO() + enumCtx := ctx.New() // used for all (2 or 3) enumerates + defer enumCtx.Cancel() + enumerate := func(errc chan<- error, sto blobserver.Storage, blobc chan<- blob.SizedRef) { + err := enumerateAllBlobs(enumCtx, sto, blobc) + if err != nil { + enumCtx.Cancel() + } + errc <- err + } + + go enumerate(srcErr, src, srcBlobs) + checkSourceError := func() { + if err := <-srcErr; err != nil && err != context.ErrCanceled { + retErr = fmt.Errorf("Enumerate error from source: %v", err) + } + } + + if c.dest == "stdout" { + for sb := range srcBlobs { + fmt.Fprintf(cmdmain.Stdout, "%s %d\n", sb.Ref, sb.Size) + } + checkSourceError() + return + } + + if c.wipe { + // TODO(mpl): dest is a client. make it send a "wipe" request? + // upon reception its server then wipes itself if it is a wiper. + log.Print("Index wiping not yet supported.") + } + + go enumerate(destErr, dest, destBlobs) + checkDestError := func() { + if err := <-destErr; err != nil && err != context.ErrCanceled { + retErr = fmt.Errorf("Enumerate error from destination: %v", err) + } + } + + destNotHaveBlobs := make(chan blob.SizedRef) + + readSrcBlobs := srcBlobs + if c.verbose { + readSrcBlobs = loggingBlobRefChannel(srcBlobs) + } + + mismatches := []blob.Ref{} + onMismatch := func(br blob.Ref) { + // TODO(bradfitz): check both sides and repair, carefully. For now, fail. + log.Printf("WARNING: blobref %v has differing sizes on source and dest", br) + stats.ErrorCount++ + mismatches = append(mismatches, br) + } + + go blobserver.ListMissingDestinationBlobs(destNotHaveBlobs, onMismatch, readSrcBlobs, destBlobs) + + // Handle three-legged mode if tc is provided. + checkThirdError := func() {} // default nop + syncBlobs := destNotHaveBlobs + firstHopDest := dest + if thirdLeg != nil { + thirdBlobs := make(chan blob.SizedRef, 100) + thirdErr := make(chan error, 1) + go enumerate(thirdErr, thirdLeg, thirdBlobs) + checkThirdError = func() { + if err := <-thirdErr; err != nil && err != context.ErrCanceled { + retErr = fmt.Errorf("Enumerate error from third leg: %v", err) + } + } + thirdNeedBlobs := make(chan blob.SizedRef) + go blobserver.ListMissingDestinationBlobs(thirdNeedBlobs, onMismatch, destNotHaveBlobs, thirdBlobs) + syncBlobs = thirdNeedBlobs + firstHopDest = thirdLeg + } + + for sb := range syncBlobs { + fmt.Fprintf(cmdmain.Stdout, "Destination needs blob: %s\n", sb) + + blobReader, size, err := src.Fetch(sb.Ref) + if err != nil { + stats.ErrorCount++ + log.Printf("Error fetching %s: %v", sb.Ref, err) + continue + } + if size != sb.Size { + stats.ErrorCount++ + log.Printf("Source blobserver's enumerate size of %d for blob %s doesn't match its Get size of %d", + sb.Size, sb.Ref, size) + continue + } + + if _, err := blobserver.Receive(firstHopDest, sb.Ref, blobReader); err != nil { + stats.ErrorCount++ + log.Printf("Upload of %s to destination blobserver failed: %v", sb.Ref, err) + continue + } + stats.BlobsCopied++ + stats.BytesCopied += int64(size) + + if c.removeSrc { + if err = src.RemoveBlobs([]blob.Ref{sb.Ref}); err != nil { + stats.ErrorCount++ + log.Printf("Failed to delete %s from source: %v", sb.Ref, err) + } + } + } + + checkSourceError() + checkDestError() + checkThirdError() + if retErr == nil && stats.ErrorCount > 0 { + retErr = fmt.Errorf("%d errors during sync", stats.ErrorCount) + } + return stats, retErr +} + +func loggingBlobRefChannel(ch <-chan blob.SizedRef) chan blob.SizedRef { + ch2 := make(chan blob.SizedRef) + go func() { + defer close(ch2) + var last time.Time + var nblob, nbyte int64 + for v := range ch { + ch2 <- v + nblob++ + nbyte += int64(v.Size) + now := time.Now() + if last.IsZero() || now.After(last.Add(1*time.Second)) { + last = now + log.Printf("At source blob %v (%d blobs, %d bytes)", v.Ref, nblob, nbyte) + } + } + log.Printf("Total blobs: %d, %d bytes", nblob, nbyte) + }() + return ch2 +} diff --git a/vendor/github.com/camlistore/camlistore/cmd/camtool/sync_test.go b/vendor/github.com/camlistore/camlistore/cmd/camtool/sync_test.go new file mode 100644 index 00000000..d8e7148a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/cmd/camtool/sync_test.go @@ -0,0 +1,42 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" +) + +func TestLooksLikePath(t *testing.T) { + tests := []struct { + v string + want bool + }{ + {"foo.com", false}, + {"127.0.0.1:234", false}, + {"foo", false}, + + {"/foo", true}, + {"./foo", true}, + {"../foo", true}, + } + for _, tt := range tests { + got := looksLikePath(tt.v) + if got != tt.want { + t.Errorf("looksLikePath(%q) = %v; want %v", tt.v, got, tt.want) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/config/dev-blobserver-config.json b/vendor/github.com/camlistore/camlistore/config/dev-blobserver-config.json new file mode 100644 index 00000000..0b6de0a6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/config/dev-blobserver-config.json @@ -0,0 +1,18 @@ +{ "_for-emacs": "-*- mode: js2;-*-", + "handlerConfig": true, + "baseURL": ["_env", "http://localhost:${CAMLI_PORT}"], + "password": ["_env", "${CAMLI_PASSWORD}"], + + "TLSCertFile": ["_env", "${CAMLI_TLS_CRT_FILE}", ""], + "TLSKeyFile": ["_env", "${CAMLI_TLS_KEY_FILE}", ""], + + "prefixes": { + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT}"] + } + } + } +} + diff --git a/vendor/github.com/camlistore/camlistore/config/dev-client-dir-demo/client-config.json b/vendor/github.com/camlistore/camlistore/config/dev-client-dir-demo/client-config.json new file mode 100644 index 00000000..8ee1dd3c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/config/dev-client-dir-demo/client-config.json @@ -0,0 +1,12 @@ +{ + "servers": { + "devcam": { + "server": "http://localhost:3179/", + "auth": "devauth:camli3179", + "default": true + } + }, + "ignoredFiles": [".DS_Store"], + "identity": "26F5ABDA", + "identitySecretRing": ["_env", "${HOME}/src/camlistore.org/pkg/jsonsign/testdata/test-secring.gpg"] +} diff --git a/vendor/github.com/camlistore/camlistore/config/dev-client-dir/client-config.json b/vendor/github.com/camlistore/camlistore/config/dev-client-dir/client-config.json new file mode 100644 index 00000000..41ce982e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/config/dev-client-dir/client-config.json @@ -0,0 +1,12 @@ +{ + "servers": { + "devcam": { + "server": ["_env", "${CAMLI_SERVER}", "http://localhost:3179/"], + "auth": ["_env", "${CAMLI_AUTH}"], + "default": true + } + }, + "ignoredFiles": [".DS_Store"], + "identity": ["_env", "${CAMLI_KEYID}"], + "identitySecretRing": ["_env", "${CAMLI_SECRET_RING}"] +} diff --git a/vendor/github.com/camlistore/camlistore/config/dev-indexer-config.json b/vendor/github.com/camlistore/camlistore/config/dev-indexer-config.json new file mode 100644 index 00000000..df902766 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/config/dev-indexer-config.json @@ -0,0 +1,18 @@ +{ "_for-emacs": "-*- mode: js2;-*-", + "handlerConfig": true, + "baseURL": ["_env", "http://localhost:${CAMLI_PORT}"], + "password": ["_env", "${CAMLI_PASSWORD}"], + "prefixes": { + "/indexer/": { + "handler": "storage-mysqlindexer", + "handlerArgs": { + "database": "devcamlistore", + "user": "root", + "password": "root", + "host": "127.0.0.1" + } + } + } +} + + diff --git a/vendor/github.com/camlistore/camlistore/config/dev-server-config.json b/vendor/github.com/camlistore/camlistore/config/dev-server-config.json new file mode 100644 index 00000000..9f0ddd22 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/config/dev-server-config.json @@ -0,0 +1,362 @@ +{ "_for-emacs": "-*- mode: js2;-*-", + "handlerConfig": true, + "baseURL": ["_env", "${CAMLI_BASEURL}"], + "auth": ["_env", "${CAMLI_AUTH}"], + "https": ["_env", "${CAMLI_TLS}", false], + "httpsCert": "config/tls.crt", + "httpsKey": "config/tls.key", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "ownerName": ["_env", "${USER}-dev"], + "jsonSignRoot": "/sighelper/", + "blobRoot": "/bs-recv/", + "helpRoot": "/help/", + "statusRoot": "/status/", + "searchRoot": "/my-search/", + "stealth": false + } + }, + + "/hello/": { + "handler": "app", + "enabled": ["_env", "${CAMLI_HELLO_ENABLED}"], + "handlerArgs": { + "program": "hello", + "appConfig": { + "word": "world" + } + } + }, + + "/pics/": { + "handler": "app", + "enabled": ["_env", "${CAMLI_PUBLISH_ENABLED}"], + "handlerArgs": { + "program": "publisher", + "appConfig": { + "camliRoot": "dev-pics-root", + "sourceRoot": ["_env", "${CAMLI_DEV_CAMLI_ROOT}", ""], + "cacheRoot": ["_env", "${CAMLI_ROOT_CACHE}"], + "goTemplate": "gallery.html" + } + } + }, + + "/stub-test-disable/": { + "handler": "publish", + "enabled": false, + "handlerArgs": { + } + }, + + "/ui/": { + "handler": "ui", + "handlerArgs": { + "sourceRoot": ["_env", "${CAMLI_DEV_CAMLI_ROOT}", ""], + "cache": "/cache/", + "scaledImage": { + "type": "kv", + "file": ["_env", "${CAMLI_ROOT_CACHE}/thumbnails.kv", ""] + } + } + }, + + "/status/": { + "handler": "status" + }, + + "/help/": { + "handler": "help" + }, + + "/sync-index/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "to": ["_env", "${CAMLI_INDEXER_PATH}"], + "queue": { "type": "memory" }, + "fullSyncOnStart": ["_env", "${CAMLI_FULL_INDEX_SYNC_ON_START}"], + "blockingFullSyncOnStart": ["_env", "${CAMLI_FULL_INDEX_SYNC_ON_START}"] + } + }, + + "/sync-r1/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "to": "/r1/", + "queue": { "type": "memory" } + } + }, + + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "secretRing": ["_env", "${CAMLI_SECRET_RING}"], + "keyId": ["_env", "${CAMLI_KEYID}"], + "publicKeyDest": "/bs/" + } + }, + + "/bs-recv/": { + "handler": "storage-replica", + "handlerArgs": { + "minWritesForSuccess": 2, + "backends": ["/bs/", ["_env", "${CAMLI_INDEXER_PATH}"]], + "readBackends": ["/bs/"] + } + }, + + "/cond-unused/": { + "handler": "storage-cond", + "handlerArgs": { + "write": { + "if": "isSchema", + "then": "/bs-recv/", + "else": "/bs/" + }, + "read": "/bs/" + } + }, + + "/bs/": { + "handler": "storage-blobpacked", + "handlerArgs": { + "smallBlobs": "/bs-loose/", + "largeBlobs": "/bs-packed/", + "metaIndex": { + "type": "kv", + "file": ["_env", "${CAMLI_ROOT}/packindex.kv"] + } + } + }, + + "/bs-loose/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT}/loose"] + } + }, + + "/bs-packed/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT}/packed"] + } + }, + + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT_CACHE}"] + } + }, + + "/sharder/": { + "handler": "storage-shard", + "handlerArgs": { + "backends": ["/s1/", "/s2/"] + } + }, + + "/s1/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT_SHARD1}"] + } + }, + + "/s2/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT_SHARD2}"] + } + }, + + "/repl/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": ["/r1/", "/r2/", "/r3/"], + "minWritesForSuccess": 2 + } + }, + + "/r1/": { + "handler": "storage-diskpacked", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT_REPLICA1}"] + } + }, + + "/r2/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT_REPLICA2}"] + } + }, + + "/r3/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT_REPLICA3}"] + } + }, + + "/enc/": { + "handler": "storage-encrypt", + "handlerArgs": { + "I_AGREE": "that encryption support hasn't been peer-reviewed, isn't finished, and its format might change.", + "meta": "/encmeta/", + "blobs": "/encblob/", + "metaIndex": { "type": "memory" }, + "key": "000102030405060708090a0b0c0d0e0f" + } + }, + + "/encmeta/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT_ENCMETA}"] + } + }, + + "/encblob/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT_ENCBLOB}"] + } + }, + + "/index-memory/": { + "enabled": ["_env", "${CAMLI_MEMINDEX_ENABLED}"], + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "type": "memory" + } + } + }, + + "/index-leveldb/": { + "enabled": ["_env", "${CAMLI_LEVELDB_ENABLED}"], + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "type": "leveldb", + "file": ["_env", "${CAMLI_DBNAME}", ""] + } + } + }, + + "/index-kv/": { + "enabled": ["_env", "${CAMLI_KVINDEX_ENABLED}"], + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "type": "kv", + "file": ["_env", "${CAMLI_DBNAME}", ""] + } + } + }, + + "/index-mongo/": { + "enabled": ["_env", "${CAMLI_MONGO_ENABLED}", true], + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "type": "mongo", + "host": "localhost", + "database": ["_env", "${CAMLI_DBNAME}"] + } + } + }, + + "/index-mysql/": { + "enabled": ["_env", "${CAMLI_MYSQL_ENABLED}", true], + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "type": "mysql", + "database": ["_env", "${CAMLI_DBNAME}"], + "user": "root", + "password": "root", + "host": "127.0.0.1" + } + } + }, + + "/index-postgres/": { + "enabled": ["_env", "${CAMLI_POSTGRES_ENABLED}", true], + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "type": "postgres", + "database": ["_env", "${CAMLI_DBNAME}"], + "user": "postgres", + "password": "postgres", + "host": "127.0.0.1" + } + } + }, + + "/index-sqlite/": { + "enabled": ["_env", "${CAMLI_SQLITE_ENABLED}", true], + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "type": "sqlite", + "file": ["_env", "${CAMLI_DBNAME}"] + } + } + }, + + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": ["_env", "${CAMLI_INDEXER_PATH}"], + "owner": ["_env", "${CAMLI_PUBKEY_BLOBREF}"], + "slurpToMemory": true, + "devBlockStartupOn": "/sync-index/" + } + }, + + "/importer/": { + "handler": "importer", + "handlerArgs": { + "dummy": { + "clientID": "dummyID", + "clientSecret": "foobar" + }, + "flickr": { + "clientSecret": ["_env", "${CAMLI_FLICKR_API_KEY}", ""] + }, + "foursquare": { + "clientSecret": ["_env", "${CAMLI_FOURSQUARE_API_KEY}", ""] + }, + "picasa": { + "clientSecret": ["_env", "${CAMLI_PICASA_API_KEY}", ""] + }, + "twitter": { + "clientSecret": ["_env", "${CAMLI_TWITTER_API_KEY}", ""] + } + } + }, + + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + } + } + +} diff --git a/vendor/github.com/camlistore/camlistore/depcheck/depcheck.go b/vendor/github.com/camlistore/camlistore/depcheck/depcheck.go new file mode 100644 index 00000000..d7f9a77a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/depcheck/depcheck.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package depcheck does nothing except for fail to build when +// the system's version of Go is too old. +package depcheck diff --git a/vendor/github.com/camlistore/camlistore/depcheck/min_go_version.go b/vendor/github.com/camlistore/camlistore/depcheck/min_go_version.go new file mode 100644 index 00000000..5c6d4f69 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/depcheck/min_go_version.go @@ -0,0 +1,28 @@ +// +build !go1.3 + +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package depcheck + +import ` + +**************************************************************************** + + Camlistore requires Go 1.3 or later. + +**************************************************************************** +` diff --git a/vendor/github.com/camlistore/camlistore/dev/camfix.pl b/vendor/github.com/camlistore/camlistore/dev/camfix.pl new file mode 100755 index 00000000..f7fe3ff0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/camfix.pl @@ -0,0 +1,23 @@ +#!/usr/bin/perl + +my $file = shift; +die "$file doesn't exist" unless -e $file; + +open(my $fh, $file) or die "failed: $!\n"; +my $c = do { local $/; <$fh> }; +close($fh); + +my $changes = 0; + +$changes = 1 if $c =~ s!^(\s+)\"camli/(.+)\"!$1\"camlistore.org/pkg/$2\"!mg; +$changes = 1 if $c =~ s!^(\s+)\"camlistore/(.+)\"!$1\"camlistore.org/$2\"!mg; +$changes = 1 if $c =~ s!^(\s+_ )\"camlistore/(.+)\"!$1\"camlistore.org/$2\"!mg; +$changes = 1 if $c =~ s!/pkg/pkg/!/pkg/!g; +$changes = 1 if $c =~ s!camlistore.org/pkg/third_party/!camlistore.org/third_party/!g; + +exit 0 unless $changes; + +open(my $fh, ">$file") or die; +print $fh $c; +close($fh); +print STDERR "rewrote $file\n"; diff --git a/vendor/github.com/camlistore/camlistore/dev/config-dir-local/client-config.json b/vendor/github.com/camlistore/camlistore/dev/config-dir-local/client-config.json new file mode 100644 index 00000000..4e3a147e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/config-dir-local/client-config.json @@ -0,0 +1,12 @@ +{ + "servers": { + "dev": { + "server": "http://localhost:3179/", + "auth": "devauth:camli3179", + "default": true + } + }, + "ignoredFiles": [".DS_Store"], + "identity": "26F5ABDA", + "identitySecretRing": ["_env", "${CAMLI_CONFENV_SECRET_RING}"] +} diff --git a/vendor/github.com/camlistore/camlistore/dev/demo.sh b/vendor/github.com/camlistore/camlistore/dev/demo.sh new file mode 100644 index 00000000..37179f5d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/demo.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# Some hacks to make make demoing Camlistore less distracting but +# still permit using the dev-* scripts (which are normally slow and +# noisy) + +#go run make.go +#go install camlistore.org/dev/devcam +#export CAMLI_QUIET=1 +#export CAMLI_FAST_DEV=1 + +# Or just: +# (This way is buggy in that the server selection doesn't let you also +# pick an identity) +# export CAMLI_DEFAULT_SERVER=dev + +# Better: +export CAMLI_CONFIG_DIR=$HOME/src/camlistore.org/config/dev-client-dir-demo diff --git a/vendor/github.com/camlistore/camlistore/dev/dev-db b/vendor/github.com/camlistore/camlistore/dev/dev-db new file mode 100755 index 00000000..5598ffe7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/dev-db @@ -0,0 +1,3 @@ +#!/bin/sh + +exec mysql -uroot -proot devcamli$USER diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/appengine.go b/vendor/github.com/camlistore/camlistore/dev/devcam/appengine.go new file mode 100644 index 00000000..0d0b66d7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/appengine.go @@ -0,0 +1,120 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// This file adds the "appengine" subcommand to devcam, to run the +// development appengine camlistore with dev_appserver.py. + +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strconv" + + "camlistore.org/pkg/cmdmain" +) + +type gaeCmd struct { + // start of flag vars + all bool + port string + sdk string + wipe bool + // end of flag vars +} + +func init() { + cmdmain.RegisterCommand("appengine", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(gaeCmd) + flags.BoolVar(&cmd.all, "all", false, "Listen on all interfaces.") + flags.StringVar(&cmd.port, "port", "3179", "Port to listen on.") + flags.StringVar(&cmd.sdk, "sdk", "", "The path to the App Engine Go SDK (or a symlink to it).") + flags.BoolVar(&cmd.wipe, "wipe", false, "Wipe the blobs on disk and the indexer.") + return cmd + }) +} + +func (c *gaeCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, "Usage: devcam [globalopts] appengine [cmdopts] [other_dev_appserver_opts] \n") +} + +func (c *gaeCmd) Describe() string { + return "run the App Engine camlistored in dev mode." +} + +func (c *gaeCmd) RunCommand(args []string) error { + err := c.checkFlags(args) + if err != nil { + return cmdmain.UsageError(fmt.Sprint(err)) + } + applicationDir := filepath.Join("server", "appengine") + if _, err := os.Stat(applicationDir); err != nil { + return fmt.Errorf("Appengine application dir not found at %s", applicationDir) + } + if err = c.checkSDK(); err != nil { + return err + } + if err = c.mirrorSourceRoot(applicationDir); err != nil { + return err + } + + devAppServerBin := filepath.Join(c.sdk, "dev_appserver.py") + cmdArgs := []string{ + "--skip_sdk_update_check", + fmt.Sprintf("--port=%s", c.port), + } + if c.all { + cmdArgs = append(cmdArgs, "--host", "0.0.0.0") + } + if c.wipe { + cmdArgs = append(cmdArgs, "--clear_datastore") + } + cmdArgs = append(cmdArgs, args...) + cmdArgs = append(cmdArgs, applicationDir) + return runExec(devAppServerBin, cmdArgs, NewCopyEnv()) +} + +func (c *gaeCmd) checkFlags(args []string) error { + if _, err := strconv.ParseInt(c.port, 0, 0); err != nil { + return fmt.Errorf("Invalid -port value: %q", c.port) + } + return nil +} + +func (c *gaeCmd) checkSDK() error { + defaultSDK := "appengine-sdk" + if c.sdk == "" { + c.sdk = defaultSDK + } + if _, err := os.Stat(c.sdk); err != nil { + return fmt.Errorf("App Engine SDK not found. Please specify it with --sdk or:\n$ ln -s /path/to/appengine-go-sdk %s\n\n", defaultSDK) + } + return nil +} + +func (c *gaeCmd) mirrorSourceRoot(gaeAppDir string) error { + uiDirs := []string{"server/camlistored/ui", "third_party/closure/lib/closure", "pkg/server"} + for _, dir := range uiDirs { + oriPath := filepath.Join(camliSrcRoot, filepath.FromSlash(dir)) + dstPath := filepath.Join(gaeAppDir, "source_root", filepath.FromSlash(dir)) + if err := cpDir(oriPath, dstPath, []string{".go"}); err != nil { + return fmt.Errorf("Error while mirroring %s to %s: %v", oriPath, dstPath, err) + } + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/camget.go b/vendor/github.com/camlistore/camlistore/dev/devcam/camget.go new file mode 100644 index 00000000..f1cc33a3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/camget.go @@ -0,0 +1,118 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// This file adds the "get" subcommand to devcam, to run camget against the dev server. + +package main + +import ( + "flag" + "fmt" + "path/filepath" + "regexp" + "strconv" + "strings" + + "camlistore.org/pkg/cmdmain" +) + +type getCmd struct { + // start of flag vars + altkey bool + path string + port string + tls bool + // end of flag vars + + env *Env +} + +func init() { + cmdmain.RegisterCommand("get", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := &getCmd{ + env: NewCopyEnv(), + } + flags.BoolVar(&cmd.altkey, "altkey", false, "Use different gpg key and password from the server's.") + flags.StringVar(&cmd.path, "path", "/bs", "Optional URL prefix path.") + flags.StringVar(&cmd.port, "port", "3179", "Port camlistore is listening on.") + flags.BoolVar(&cmd.tls, "tls", false, "Use TLS.") + return cmd + }) +} + +func (c *getCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, "Usage: devcam get [get_opts] -- camget_args\n") +} + +func (c *getCmd) Examples() []string { + return []string{ + "", + "-- --shared http://localhost:3169/share/", + } +} + +func (c *getCmd) Describe() string { + return "run camget in dev mode." +} + +func (c *getCmd) RunCommand(args []string) error { + err := c.checkFlags(args) + if err != nil { + return cmdmain.UsageError(fmt.Sprint(err)) + } + if !*noBuild { + if err := build(filepath.Join("cmd", "camget")); err != nil { + return fmt.Errorf("Could not build camget: %v", err) + } + } + c.env.SetCamdevVars(c.altkey) + // wipeCacheDir needs to be called after SetCamdevVars, because that is + // where CAMLI_CACHE_DIR is defined. + if *wipeCache { + c.env.wipeCacheDir() + } + + cmdBin := filepath.Join("bin", "camget") + cmdArgs := []string{ + "-verbose=" + strconv.FormatBool(*cmdmain.FlagVerbose || !quiet), + } + if !isSharedMode(args) { + blobserver := "http://localhost:" + c.port + c.path + if c.tls { + blobserver = strings.Replace(blobserver, "http://", "https://", 1) + } + cmdArgs = append(cmdArgs, "-server="+blobserver) + } + cmdArgs = append(cmdArgs, args...) + return runExec(cmdBin, cmdArgs, c.env) +} + +func (c *getCmd) checkFlags(args []string) error { + if _, err := strconv.ParseInt(c.port, 0, 0); err != nil { + return fmt.Errorf("Invalid -port value: %q", c.port) + } + return nil +} + +func isSharedMode(args []string) bool { + sharedRgx := regexp.MustCompile("--?shared") + for _, v := range args { + if sharedRgx.MatchString(v) { + return true + } + } + return false +} diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/cammount.go b/vendor/github.com/camlistore/camlistore/dev/devcam/cammount.go new file mode 100644 index 00000000..4e049e8e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/cammount.go @@ -0,0 +1,127 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// This file adds the "mount" subcommand to devcam, to run cammount against the dev server. + +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + + "camlistore.org/pkg/cmdmain" +) + +type mountCmd struct { + // start of flag vars + altkey bool + path string + port string + tls bool + debug bool + // end of flag vars + + env *Env +} + +const mountpoint = "/tmp/cammount-dir" + +func init() { + cmdmain.RegisterCommand("mount", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := &mountCmd{ + env: NewCopyEnv(), + } + flags.BoolVar(&cmd.altkey, "altkey", false, "Use different gpg key and password from the server's.") + flags.BoolVar(&cmd.tls, "tls", false, "Use TLS.") + flags.StringVar(&cmd.path, "path", "/", "Optional URL prefix path.") + flags.StringVar(&cmd.port, "port", "3179", "Port camlistore is listening on.") + flags.BoolVar(&cmd.debug, "debug", false, "print debugging messages.") + return cmd + }) +} + +func (c *mountCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, "Usage: devcam mount [mount_opts] [|]\n") +} + +func (c *mountCmd) Examples() []string { + return []string{ + "", + "http://localhost:3169/share/", + } +} + +func (c *mountCmd) Describe() string { + return "run cammount in dev mode." +} + +func tryUnmount(dir string) error { + if runtime.GOOS == "darwin" { + return exec.Command("diskutil", "umount", "force", dir).Run() + } + return exec.Command("fusermount", "-u", dir).Run() +} + +func (c *mountCmd) RunCommand(args []string) error { + err := c.checkFlags(args) + if err != nil { + return cmdmain.UsageError(fmt.Sprint(err)) + } + if !*noBuild { + if err := build(filepath.Join("cmd", "cammount")); err != nil { + return fmt.Errorf("Could not build cammount: %v", err) + } + } + c.env.SetCamdevVars(c.altkey) + // wipeCacheDir needs to be called after SetCamdevVars, because that is + // where CAMLI_CACHE_DIR is defined. + if *wipeCache { + c.env.wipeCacheDir() + } + + tryUnmount(mountpoint) + if err := os.Mkdir(mountpoint, 0700); err != nil && !os.IsExist(err) { + return fmt.Errorf("Could not make mount point: %v", err) + } + + blobserver := "http://localhost:" + c.port + c.path + if c.tls { + blobserver = strings.Replace(blobserver, "http://", "https://", 1) + } + + cmdBin := filepath.Join("bin", "cammount") + cmdArgs := []string{ + "-debug=" + strconv.FormatBool(c.debug), + "-server=" + blobserver, + } + cmdArgs = append(cmdArgs, args...) + cmdArgs = append(cmdArgs, mountpoint) + fmt.Printf("Cammount running with mountpoint %v. Press 'q' or ctrl-c to shut down.\n", mountpoint) + return runExec(cmdBin, cmdArgs, c.env) +} + +func (c *mountCmd) checkFlags(args []string) error { + if _, err := strconv.ParseInt(c.port, 0, 0); err != nil { + return fmt.Errorf("Invalid -port value: %q", c.port) + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/camput.go b/vendor/github.com/camlistore/camlistore/dev/devcam/camput.go new file mode 100644 index 00000000..dda1dfdc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/camput.go @@ -0,0 +1,105 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// This file adds the "put" subcommand to devcam, to run camput against the dev server. + +package main + +import ( + "flag" + "fmt" + "path/filepath" + "strconv" + "strings" + + "camlistore.org/pkg/cmdmain" +) + +type putCmd struct { + // start of flag vars + altkey bool + path string + port string + tls bool + // end of flag vars + + env *Env +} + +func init() { + cmdmain.RegisterCommand("put", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := &putCmd{ + env: NewCopyEnv(), + } + flags.BoolVar(&cmd.altkey, "altkey", false, "Use different gpg key and password from the server's.") + flags.BoolVar(&cmd.tls, "tls", false, "Use TLS.") + flags.StringVar(&cmd.path, "path", "/", "Optional URL prefix path.") + flags.StringVar(&cmd.port, "port", "3179", "Port camlistore is listening on.") + return cmd + }) +} + +func (c *putCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, "Usage: devcam put [put_opts] camput_args\n") +} + +func (c *putCmd) Examples() []string { + return []string{ + "file --filenodes /mnt/camera/DCIM", + } +} + +func (c *putCmd) Describe() string { + return "run camput in dev mode." +} + +func (c *putCmd) RunCommand(args []string) error { + err := c.checkFlags(args) + if err != nil { + return cmdmain.UsageError(fmt.Sprint(err)) + } + if !*noBuild { + if err := build(filepath.Join("cmd", "camput")); err != nil { + return fmt.Errorf("Could not build camput: %v", err) + } + } + c.env.SetCamdevVars(c.altkey) + // wipeCacheDir needs to be called after SetCamdevVars, because that is + // where CAMLI_CACHE_DIR is defined. + if *wipeCache { + c.env.wipeCacheDir() + } + + blobserver := "http://localhost:" + c.port + c.path + if c.tls { + blobserver = strings.Replace(blobserver, "http://", "https://", 1) + } + + cmdBin := filepath.Join("bin", "camput") + cmdArgs := []string{ + "-verbose=" + strconv.FormatBool(*cmdmain.FlagVerbose || !quiet), + "-server=" + blobserver, + } + cmdArgs = append(cmdArgs, args...) + return runExec(cmdBin, cmdArgs, c.env) +} + +func (c *putCmd) checkFlags(args []string) error { + if _, err := strconv.ParseInt(c.port, 0, 0); err != nil { + return fmt.Errorf("Invalid -port value: %q", c.port) + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/camtool.go b/vendor/github.com/camlistore/camlistore/dev/devcam/camtool.go new file mode 100644 index 00000000..4eeb5ed9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/camtool.go @@ -0,0 +1,77 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// This file adds the "tool" subcommand to devcam, to run camtool against +// the dev server. + +package main + +import ( + "flag" + "fmt" + "path/filepath" + + "camlistore.org/pkg/cmdmain" +) + +type toolCmd struct { + // start of flag vars + altkey bool + // end of flag vars + + env *Env +} + +func init() { + cmdmain.RegisterCommand("tool", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := &toolCmd{ + env: NewCopyEnv(), + } + flags.BoolVar(&cmd.altkey, "altkey", false, "Use different gpg key and password from the server's.") + return cmd + }) +} + +func (c *toolCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, "Usage: devcam tool [globalopts] [commandopts] [commandargs]\n") +} + +func (c *toolCmd) Examples() []string { + return []string{ + "sync --all", + } +} + +func (c *toolCmd) Describe() string { + return "run camtool in dev mode." +} + +func (c *toolCmd) RunCommand(args []string) error { + if !*noBuild { + if err := build(filepath.Join("cmd", "camtool")); err != nil { + return fmt.Errorf("Could not build camtool: %v", err) + } + } + c.env.SetCamdevVars(c.altkey) + // wipeCacheDir needs to be called after SetCamdevVars, because that is + // where CAMLI_CACHE_DIR is defined. + if *wipeCache { + c.env.wipeCacheDir() + } + + cmdBin := filepath.Join("bin", "camtool") + return runExec(cmdBin, args, c.env) +} diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/devcam.go b/vendor/github.com/camlistore/camlistore/dev/devcam/devcam.go new file mode 100644 index 00000000..7cbd2a99 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/devcam.go @@ -0,0 +1,284 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "fmt" + "io" + "log" + "os" + "os/exec" + "os/signal" + pathpkg "path" + "path/filepath" + "runtime" + "strconv" + "strings" + "syscall" + "time" + + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/osutil" +) + +var ( + noBuild = flag.Bool("nobuild", false, "do not rebuild anything") + race = flag.Bool("race", false, "build with race detector") + quiet, _ = strconv.ParseBool(os.Getenv("CAMLI_QUIET")) + wipeCache = flag.Bool("wipecache", false, "wipe the cache directory. Server cache with devcam server, client cache otherwise.") + // Whether to build the subcommand with sqlite support. This only + // concerns the server subcommand, which sets it to serverCmd.sqlite. + withSqlite bool +) + +// The path to the Camlistore source tree. Any devcam command +// should be run from there. +var camliSrcRoot string + +// sysExec is set to syscall.Exec on platforms that support it. +var sysExec func(argv0 string, argv []string, envv []string) (err error) + +// runExec execs bin. If the platform doesn't support exec, it runs it and waits +// for it to finish. +func runExec(bin string, args []string, env *Env) error { + if sysExec != nil { + sysExec(bin, append([]string{filepath.Base(bin)}, args...), env.Flat()) + } + + cmd := exec.Command(bin, args...) + cmd.Env = env.Flat() + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + return fmt.Errorf("Could not run %v: %v", bin, err) + } + go handleSignals(cmd.Process) + return cmd.Wait() +} + +// cpDir copies the contents of src dir into dst dir. +// filter is a list of file suffixes to skip. ex: ".go" +func cpDir(src, dst string, filter []string) error { + return filepath.Walk(src, func(fullpath string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + for _, suffix := range filter { + if strings.HasSuffix(fi.Name(), suffix) { + return nil + } + } + suffix, err := filepath.Rel(src, fullpath) + if err != nil { + return fmt.Errorf("Failed to find Rel(%q, %q): %v", src, fullpath, err) + } + if fi.IsDir() { + return nil + } + return cpFile(fullpath, filepath.Join(dst, suffix)) + }) +} + +func cpFile(src, dst string) error { + sfi, err := os.Stat(src) + if err != nil { + return err + } + if !sfi.Mode().IsRegular() { + return fmt.Errorf("cpFile can't deal with non-regular file %s", src) + } + + dstDir := filepath.Dir(dst) + if err := os.MkdirAll(dstDir, 0755); err != nil { + return err + } + + df, err := os.Create(dst) + if err != nil { + return err + } + sf, err := os.Open(src) + if err != nil { + return err + } + defer sf.Close() + + n, err := io.Copy(df, sf) + if err == nil && n != sfi.Size() { + err = fmt.Errorf("copied wrong size for %s -> %s: copied %d; want %d", src, dst, n, sfi.Size()) + } + cerr := df.Close() + if err == nil { + err = cerr + } + return err +} + +func handleSignals(camliProc *os.Process) { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT) + for { + sig := <-c + sysSig, ok := sig.(syscall.Signal) + if !ok { + log.Fatal("Not a unix signal") + } + switch sysSig { + case syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT: + log.Printf("Received %v signal, terminating.", sig) + err := camliProc.Kill() + if err != nil { + log.Fatalf("Failed to kill child: %v ", err) + } + default: + log.Fatal("Received another signal, should not happen.") + } + } +} + +func checkCamliSrcRoot() { + args := flag.Args() + // TODO(mpl): we should probably get rid of that limitation someday. + if len(args) > 0 && (args[0] == "review" || + args[0] == "hook" || + args[0] == "fixv") { + // exception for devcam review, which does its own check. + return + } + if _, err := os.Stat("make.go"); err != nil { + if !os.IsNotExist(err) { + log.Fatalf("Could not stat make.go: %v", err) + } + log.Fatal("./make.go not found; devcam needs to be run from the Camlistore source tree root.") + } + cwd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + camliSrcRoot = cwd +} + +func repoRoot() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("could not get current directory: %v", err) + } + rootlen := 1 + if runtime.GOOS == "windows" { + rootlen += len(filepath.VolumeName(dir)) + } + for { + if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil { + return dir, nil + } + if len(dir) == rootlen && dir[rootlen-1] == filepath.Separator { + return "", fmt.Errorf(".git not found. Rerun from within the Camlistore source tree.") + } + dir = filepath.Dir(dir) + } +} + +func selfModTime() (time.Time, error) { + var modTime time.Time + devcamBin, err := osutil.SelfPath() + if err != nil { + return modTime, err + } + fi, err := os.Stat(devcamBin) + if err != nil { + return modTime, err + } + return fi.ModTime(), nil +} + +func checkModtime() error { + binModtime, err := selfModTime() + if err != nil { + return fmt.Errorf("could not get ModTime of current devcam executable: %v", err) + } + + devcamDir := filepath.Join(camliSrcRoot, "dev", "devcam") + d, err := os.Open(devcamDir) + if err != nil { + return fmt.Errorf("could not read devcam source dir %v: %v", devcamDir, err) + } + defer d.Close() + fis, err := d.Readdir(-1) + if err != nil { + return fmt.Errorf("could not read devcam source dir %v: %v", devcamDir, err) + } + for _, fi := range fis { + if fi.ModTime().After(binModtime) { + log.Printf("**************************************************************") + log.Printf("WARNING: your devcam binary is outdated, you should rebuild it") + log.Printf("**************************************************************") + return nil + } + } + return nil +} + +// Build builds the camlistore command at the given path from the source tree root. +func build(path string) error { + if v, _ := strconv.ParseBool(os.Getenv("CAMLI_FAST_DEV")); v { + // Demo mode. See dev/demo.sh. + return nil + } + _, cmdName := filepath.Split(path) + target := pathpkg.Join("camlistore.org", filepath.ToSlash(path)) + binPath := filepath.Join("bin", cmdName) + var modtime int64 + fi, err := os.Stat(binPath) + if err != nil { + if !os.IsNotExist(err) { + return fmt.Errorf("Could not stat %v: %v", binPath, err) + } + } else { + modtime = fi.ModTime().Unix() + } + args := []string{ + "run", "make.go", + "--quiet", + "--race=" + strconv.FormatBool(*race), + "--embed_static=false", + "--sqlite=" + strconv.FormatBool(withSqlite), + fmt.Sprintf("--if_mods_since=%d", modtime), + "--targets=" + target, + } + cmd := exec.Command("go", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("Error building %v: %v", target, err) + } + return nil +} + +func main() { + cmdmain.CheckCwd = checkCamliSrcRoot + cmdmain.CheckModtime = func() error { + if err := checkModtime(); err != nil { + log.Printf("Skipping freshness check: %v", err) + } + return nil + } + + // TODO(mpl): usage error is not really correct for devcam. + // See if I can reimplement it while still using cmdmain.Main(). + cmdmain.Main() +} diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/doc.go b/vendor/github.com/camlistore/camlistore/dev/devcam/doc.go new file mode 100644 index 00000000..5c9bcc7e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/doc.go @@ -0,0 +1,47 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +The devcam tool is a collection of wrappers around the camlistore programs +(camistored, camput, camtool...) which take care of setup and configuration, +so they can be used by developers to ease hacking on camlistore. + +Usage: + + devcam [modeopts] -- [commandargs] + +Modes: + + appengine: run the App Engine camlistored in dev mode. + get: run camget in dev mode. + put: run camput in dev mode. + server: run the stand-alone camlistored in dev mode. + +Examples: + + devcam get + devcam get -- --shared http://localhost:3169/share/ + + devcam put file --filenodes /mnt/camera/DCIM + + devcam server -wipe -mysql -fullclosure + +For mode-specific help: + + devcam -help + +*/ +package main diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/env.go b/vendor/github.com/camlistore/camlistore/dev/devcam/env.go new file mode 100644 index 00000000..6d2bfdac --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/env.go @@ -0,0 +1,168 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "errors" + "flag" + "log" + "os" + "path/filepath" + "strings" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/jsonsign" + "camlistore.org/pkg/osutil" +) + +const ( + // default secret ring used in tests and in devcam commands + defaultSecring = "pkg/jsonsign/testdata/test-secring.gpg" + // public ID of the GPG key in defaultSecring + defaultIdentity = "26F5ABDA" +) + +var ( + flagSecretRing = flag.String("secretring", "", "the secret ring file to run with") + flagIdentity = flag.String("identity", "", "the key id of the identity to run with") +) + +type Env struct { + m map[string]string + order []string +} + +func (e *Env) Set(k, v string) { + _, dup := e.m[k] + e.m[k] = v + if !dup { + e.order = append(e.order, k) + } +} + +func (e *Env) Del(k string) { + delete(e.m, k) +} + +// NoGo removes GOPATH and GOBIN. +func (e *Env) NoGo() { + e.Del("GOPATH") + e.Del("GOBIN") +} + +func (e *Env) Flat() []string { + vv := make([]string, 0, len(e.order)) + for _, k := range e.order { + if v, ok := e.m[k]; ok { + vv = append(vv, k+"="+v) + } + } + return vv +} + +func NewEnv() *Env { + return &Env{make(map[string]string), nil} +} + +func NewCopyEnv() *Env { + env := NewEnv() + for _, kv := range os.Environ() { + eq := strings.Index(kv, "=") + if eq > 0 { + env.Set(kv[:eq], kv[eq+1:]) + } + } + return env +} + +func (e *Env) SetCamdevVars(altkey bool) { + setCamdevVarsFor(e, altkey) +} + +func setCamdevVars() { + setCamdevVarsFor(nil, false) +} + +func rootInTmpDir() (string, error) { + user := osutil.Username() + if user == "" { + return "", errors.New("Could not get username from environment") + } + return filepath.Join(os.TempDir(), "camliroot-"+user), nil +} + +func setCamdevVarsFor(e *Env, altkey bool) { + var setenv func(string, string) error + if e != nil { + setenv = func(k, v string) error { e.Set(k, v); return nil } + } else { + setenv = os.Setenv + } + + setenv("CAMLI_AUTH", "userpass:camlistore:pass3179") + // env values for clients. server will overwrite them anyway in its setEnvVars. + root, err := rootInTmpDir() + if err != nil { + log.Fatal(err) + } + setenv("CAMLI_CACHE_DIR", filepath.Join(root, "client", "cache")) + setenv("CAMLI_CONFIG_DIR", filepath.Join("config", "dev-client-dir")) + + secring := defaultSecring + identity := defaultIdentity + + if altkey { + secring = filepath.FromSlash("pkg/jsonsign/testdata/password-foo-secring.gpg") + identity = "C7C3E176" + println("**\n** Note: password is \"foo\"\n**\n") + } else { + if *flagSecretRing != "" { + secring = *flagSecretRing + } + if *flagIdentity != "" { + identity = *flagIdentity + } + } + + entity, err := jsonsign.EntityFromSecring(identity, secring) + if err != nil { + panic(err) + } + armoredPublicKey, err := jsonsign.ArmoredPublicKey(entity) + if err != nil { + panic(err) + } + pubKeyRef := blob.SHA1FromString(armoredPublicKey) + + setenv("CAMLI_SECRET_RING", secring) + setenv("CAMLI_KEYID", identity) + setenv("CAMLI_PUBKEY_BLOBREF", pubKeyRef.String()) + setenv("CAMLI_KV_VERIFY", "true") +} + +func (e *Env) wipeCacheDir() { + cacheDir, _ := e.m["CAMLI_CACHE_DIR"] + if cacheDir == "" { + log.Fatal("Could not wipe cache dir, CAMLI_CACHE_DIR not defined") + } + if err := os.RemoveAll(cacheDir); err != nil { + log.Fatalf("Could not remove cache dir %v: %v", cacheDir, err) + } + if err := os.MkdirAll(cacheDir, 0700); err != nil { + log.Fatalf("Could not recreate cache dir %v: %v", cacheDir, err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/exec.go b/vendor/github.com/camlistore/camlistore/dev/devcam/exec.go new file mode 100644 index 00000000..f62ee153 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/exec.go @@ -0,0 +1,27 @@ +// +build !windows + +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "syscall" +) + +func init() { + sysExec = syscall.Exec +} diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/hook.go b/vendor/github.com/camlistore/camlistore/dev/devcam/hook.go new file mode 100644 index 00000000..4da9a7a7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/hook.go @@ -0,0 +1,305 @@ +/* +Copyright 2015 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// This file adds the "hook" subcommand to devcam, to install and run git hooks. +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + + "camlistore.org/pkg/cmdmain" +) + +var hookPath = ".git/hooks/" +var hookFiles = []string{ + "pre-commit", +} + +func (c *hookCmd) installHook() error { + root, err := repoRoot() + if err != nil { + return err + } + for _, hookFile := range hookFiles { + filename := filepath.Join(root, hookPath+hookFile) + hookContent := fmt.Sprintf(hookScript, hookFile) + // If hook file exists, assume it is okay. + _, err := os.Stat(filename) + if err == nil { + if c.verbose { + data, err := ioutil.ReadFile(filename) + if err != nil { + c.verbosef("reading hook: %v", err) + } else if string(data) != hookContent { + c.verbosef("unexpected hook content in %s", filename) + } + } + continue + } + + if !os.IsNotExist(err) { + return fmt.Errorf("checking hook: %v", err) + } + c.verbosef("installing %s hook", hookFile) + if err := ioutil.WriteFile(filename, []byte(hookContent), 0700); err != nil { + return fmt.Errorf("writing hook: %v", err) + } + } + return nil +} + +var hookScript = `#!/bin/sh +exec devcam hook %s "$@" +` + +type hookCmd struct { + verbose bool +} + +func init() { + cmdmain.RegisterCommand("hook", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := &hookCmd{} + flags.BoolVar(&cmd.verbose, "verbose", false, "Be verbose.") + // TODO(mpl): "-w" flag to run gofmt -w and devcam fixv -w. for now just print instruction. + return cmd + }) +} + +func (c *hookCmd) Usage() { + printf("Usage: devcam [globalopts] hook [[hook-name] [args...]]\n") +} + +func (c *hookCmd) Examples() []string { + return []string{ + "# install the hooks (if needed)", + "pre-commit # install the hooks (if needed), then run the pre-commit hook", + } +} + +func (c *hookCmd) Describe() string { + return "Install git hooks for Camlistore, and if given, run the hook given as argument. Currently available hooks are: " + strings.TrimSuffix(strings.Join(hookFiles, ", "), ",") + "." +} + +func (c *hookCmd) RunCommand(args []string) error { + if err := c.installHook(); err != nil { + return err + } + if len(args) == 0 { + return nil + } + switch args[0] { + case "pre-commit": + if err := c.hookPreCommit(args[1:]); err != nil { + if !(len(args) > 1 && args[1] == "test") { + printf("You can override these checks with 'git commit --no-verify'\n") + } + cmdmain.ExitWithFailure = true + return err + } + } + return nil +} + +// hookPreCommit does the following checks, in order: +// gofmt, and trailing space. +// If appropriate, any one of these checks prints the action +// required from the user, and the following checks are not +// performed. +func (c *hookCmd) hookPreCommit(args []string) (err error) { + if err = c.hookGofmt(); err != nil { + return err + } + return c.hookTrailingSpace() +} + +// hookGofmt runs a gofmt check on the local files matching the files in the +// git staging area. +// An error is returned if something went wrong or if some of the files need +// gofmting. In the latter case, the instruction is printed. +func (c *hookCmd) hookGofmt() error { + if os.Getenv("GIT_GOFMT_HOOK") == "off" { + printf("gofmt disabled by $GIT_GOFMT_HOOK=off\n") + return nil + } + + files, err := c.runGofmt() + if err != nil { + printf("gofmt hook reported errors:\n\t%v\n", strings.Replace(strings.TrimSpace(err.Error()), "\n", "\n\t", -1)) + return errors.New("gofmt errors") + } + if len(files) == 0 { + return nil + } + printf("You need to format with gofmt:\n\tgofmt -w %s\n", + strings.Join(files, " ")) + return errors.New("gofmt required") +} + +func (c *hookCmd) hookTrailingSpace() error { + out, _ := cmdOutputDirErr(".", "git", "diff-index", "--check", "--diff-filter=ACM", "--cached", "HEAD", "--") + if out != "" { + printf("\n%s", out) + printf("Trailing whitespace detected, you need to clean it up manually.\n") + return errors.New("trailing whitespace.") + } + return nil +} + +// runGofmt runs the external gofmt command over the local version of staged files. +// It returns the files that need gofmting. +func (c *hookCmd) runGofmt() (files []string, err error) { + repo, err := repoRoot() + if err != nil { + return nil, err + } + if !strings.HasSuffix(repo, string(filepath.Separator)) { + repo += string(filepath.Separator) + } + + out, err := cmdOutputDirErr(".", "git", "diff-index", "--name-only", "--diff-filter=ACM", "--cached", "HEAD", "--") + if err != nil { + return nil, err + } + indexFiles := addRoot(repo, filter(gofmtRequired, nonBlankLines(out))) + if len(indexFiles) == 0 { + return + } + + args := []string{"-l"} + // TODO(mpl): it would be nice to TrimPrefix the pwd from each file to get a shorter output. + // However, since git sets the pwd to GIT_DIR before running the pre-commit hook, we lost + // the actual pwd from when we ran `git commit`, so no dice so far. + for _, file := range indexFiles { + args = append(args, file) + } + + if c.verbose { + fmt.Fprintln(cmdmain.Stderr, commandString("gofmt", args)) + } + cmd := exec.Command("gofmt", args...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err = cmd.Run() + + if err != nil { + // Error but no stderr: usually can't find gofmt. + if stderr.Len() == 0 { + return nil, fmt.Errorf("invoking gofmt: %v", err) + } + return nil, fmt.Errorf("%s: %v", stderr.String(), err) + } + + // Build file list. + files = lines(stdout.String()) + sort.Strings(files) + return files, nil +} + +func printf(format string, args ...interface{}) { + cmdmain.Errorf(format, args...) +} + +func addRoot(root string, list []string) []string { + var out []string + for _, x := range list { + out = append(out, filepath.Join(root, x)) + } + return out +} + +// nonBlankLines returns the non-blank lines in text. +func nonBlankLines(text string) []string { + var out []string + for _, s := range lines(text) { + if strings.TrimSpace(s) != "" { + out = append(out, s) + } + } + return out +} + +// filter returns the elements in list satisfying f. +func filter(f func(string) bool, list []string) []string { + var out []string + for _, x := range list { + if f(x) { + out = append(out, x) + } + } + return out +} + +// gofmtRequired reports whether the specified file should be checked +// for gofmt'dness by the pre-commit hook. +// The file name is relative to the repo root. +func gofmtRequired(file string) bool { + if !strings.HasSuffix(file, ".go") { + return false + } + if !strings.HasPrefix(file, "test/") { + return true + } + return strings.HasPrefix(file, "test/bench/") || file == "test/run.go" +} + +func commandString(command string, args []string) string { + return strings.Join(append([]string{command}, args...), " ") +} + +func lines(text string) []string { + out := strings.Split(text, "\n") + // Split will include a "" after the last line. Remove it. + if n := len(out) - 1; n >= 0 && out[n] == "" { + out = out[:n] + } + return out +} + +func (c *hookCmd) verbosef(format string, args ...interface{}) { + if c.verbose { + fmt.Fprintf(cmdmain.Stdout, format, args...) + } +} + +// cmdOutputDirErr runs the command line in dir, returning its output +// and any error results. +// +// NOTE: cmdOutputDirErr must be used only to run commands that read state, +// not for commands that make changes. Commands that make changes +// should be run using runDirErr so that the -v and -n flags apply to them. +func cmdOutputDirErr(dir, command string, args ...string) (string, error) { + // NOTE: We only show these non-state-modifying commands with -v -v. + // Otherwise things like 'git sync -v' show all our internal "find out about + // the git repo" commands, which is confusing if you are just trying to find + // out what git sync means. + + cmd := exec.Command(command, args...) + if dir != "." { + cmd.Dir = dir + } + b, err := cmd.CombinedOutput() + return string(b), err +} diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/review.go b/vendor/github.com/camlistore/camlistore/dev/devcam/review.go new file mode 100644 index 00000000..5fd0b4fe --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/review.go @@ -0,0 +1,126 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// This file adds the "review" subcommand to devcam, to send changes for peer review. + +package main + +import ( + "bufio" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + + "camlistore.org/pkg/cmdmain" +) + +var ( + defaultHook = filepath.FromSlash("misc/commit-msg.githook") + hookFile = filepath.FromSlash(".git/hooks/commit-msg") +) + +type reviewCmd struct{} + +func init() { + cmdmain.RegisterCommand("review", func(flags *flag.FlagSet) cmdmain.CommandRunner { + return new(reviewCmd) + }) +} + +func (c *reviewCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, "Usage: devcam review\n") +} + +func (c *reviewCmd) Describe() string { + return "Submit your git commits for review." +} + +func (c *reviewCmd) RunCommand(args []string) error { + if len(args) > 0 { + return cmdmain.UsageError("too many arguments.") + } + goToCamliRoot() + c.checkHook() + gitPush() + return nil +} + +func goToCamliRoot() { + prevDir, err := os.Getwd() + if err != nil { + log.Fatalf("could not get current directory: %v", err) + } + for { + if _, err := os.Stat(defaultHook); err == nil { + return + } + if err := os.Chdir(".."); err != nil { + log.Fatalf("Could not chdir: %v", err) + } + currentDir, err := os.Getwd() + if err != nil { + log.Fatalf("Could not get current directory: %v", err) + } + if currentDir == prevDir { + log.Fatal("Camlistore tree root not found. Run from within the Camlistore tree please.") + } + prevDir = currentDir + } +} + +func (c *reviewCmd) checkHook() { + _, err := os.Stat(hookFile) + if err == nil { + return + } + if !os.IsNotExist(err) { + log.Fatal(err) + } + fmt.Fprintf(cmdmain.Stdout, "Presubmit hook to add Change-Id to commit messages is missing.\nNow automatically creating it at %v from %v\n\n", hookFile, defaultHook) + data, err := ioutil.ReadFile(defaultHook) + if err != nil { + log.Fatal(err) + } + if err := ioutil.WriteFile(hookFile, data, 0700); err != nil { + log.Fatal(err) + } + fmt.Fprintf(cmdmain.Stdout, "Amending last commit to add Change-Id.\nPlease re-save description without making changes.\n\n") + fmt.Fprintf(cmdmain.Stdout, "Press Enter to continue.\n") + if _, _, err := bufio.NewReader(cmdmain.Stdin).ReadLine(); err != nil { + log.Fatal(err) + } + + cmd := exec.Command("git", []string{"commit", "--amend"}...) + cmd.Stdout = cmdmain.Stdout + cmd.Stderr = cmdmain.Stderr + if err := cmd.Run(); err != nil { + log.Fatal(err) + } +} + +func gitPush() { + cmd := exec.Command("git", + []string{"push", "https://camlistore.googlesource.com/camlistore", "HEAD:refs/for/master"}...) + cmd.Stdout = cmdmain.Stdout + cmd.Stderr = cmdmain.Stderr + if err := cmd.Run(); err != nil { + log.Fatalf("Could not git push: %v", err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/server.go b/vendor/github.com/camlistore/camlistore/dev/devcam/server.go new file mode 100644 index 00000000..d72462c8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/server.go @@ -0,0 +1,553 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// This file adds the "server" subcommand to devcam, to run camlistored. + +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "camlistore.org/pkg/client" + "camlistore.org/pkg/cmdmain" + "camlistore.org/pkg/importer" + _ "camlistore.org/pkg/importer/allimporters" + "camlistore.org/pkg/netutil" + "camlistore.org/pkg/osutil" +) + +type serverCmd struct { + // start of flag vars + all bool + hostname string + port string + tls bool + wipe bool + things bool + debug bool + + mongo bool + mysql bool + postgres bool + sqlite bool + kvfile bool + memory bool + + slow bool + throttle int + latency int + + fullIndexSync bool + + fullClosure bool + mini bool + publish bool // whether to build and start the publisher app(s) + hello bool // whether to build and start the hello demo app + + openBrowser bool + flickrAPIKey string + foursquareAPIKey string + picasaAPIKey string + twitterAPIKey string + extraArgs string // passed to camlistored + // end of flag vars + + listen string // address + port to listen on + root string // the temp dir where blobs are stored + env *Env +} + +func init() { + cmdmain.RegisterCommand("server", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := &serverCmd{ + env: NewCopyEnv(), + } + flags.BoolVar(&cmd.all, "all", false, "Listen on all interfaces.") + flags.StringVar(&cmd.hostname, "hostname", "", "Hostname to advertise, defaults to the hostname reported by the kernel.") + flags.StringVar(&cmd.port, "port", "3179", "Port to listen on.") + flags.BoolVar(&cmd.tls, "tls", false, "Use TLS.") + flags.BoolVar(&cmd.wipe, "wipe", false, "Wipe the blobs on disk and the indexer.") + flags.BoolVar(&cmd.things, "makethings", false, "Create various test data on startup (twitter imports for now). Requires wipe. Conflicts with mini.") + flags.BoolVar(&cmd.debug, "debug", false, "Enable http debugging.") + flags.BoolVar(&cmd.publish, "publish", true, "Enable publisher app(s)") + flags.BoolVar(&cmd.hello, "hello", false, "Enable hello (demo) app") + flags.BoolVar(&cmd.mini, "mini", false, "Enable minimal mode, where all optional features are disabled. (Currently just publishing)") + + flags.BoolVar(&cmd.mongo, "mongo", false, "Use mongodb as the index storage. Excludes -mysql, -postgres, -sqlite, -memory, -kvfile.") + flags.BoolVar(&cmd.mysql, "mysql", false, "Use mysql as the index storage. Excludes -mongo, -postgres, -sqlite, -memory, -kvfile.") + flags.BoolVar(&cmd.postgres, "postgres", false, "Use postgres as the index storage. Excludes -mongo, -mysql, -sqlite, -memory, -kvfile.") + flags.BoolVar(&cmd.sqlite, "sqlite", false, "Use sqlite as the index storage. Excludes -mongo, -mysql, -postgres, -memory, -kvfile.") + flags.BoolVar(&cmd.kvfile, "kvfile", false, "Use cznic/kv as the index storage. Excludes -mongo, -mysql, -postgres, -memory, -sqlite.") + flags.BoolVar(&cmd.memory, "memory", false, "Use a memory-only index storage. Excludes -mongo, -mysql, -postgres, -sqlite, -kvfile.") + + flags.BoolVar(&cmd.slow, "slow", false, "Add artificial latency.") + flags.IntVar(&cmd.throttle, "throttle", 150, "If -slow, this is the rate in kBps, to which we should throttle.") + flags.IntVar(&cmd.latency, "latency", 90, "If -slow, this is the added latency, in ms.") + + flags.BoolVar(&cmd.fullIndexSync, "fullindexsync", false, "Perform full sync to indexer on startup.") + + flags.BoolVar(&cmd.fullClosure, "fullclosure", false, "Use the ondisk closure library.") + + flags.BoolVar(&cmd.openBrowser, "openbrowser", false, "Open the start page on startup.") + flags.StringVar(&cmd.flickrAPIKey, "flickrapikey", "", "The key and secret to use with the Flickr importer. Formatted as ':'.") + flags.StringVar(&cmd.foursquareAPIKey, "foursquareapikey", "", "The key and secret to use with the Foursquare importer. Formatted as ':'.") + flags.StringVar(&cmd.picasaAPIKey, "picasakey", "", "The username and password to use with the Picasa importer. Formatted as ':'.") + flags.StringVar(&cmd.twitterAPIKey, "twitterapikey", "", "The key and secret to use with the Twitter importer. Formatted as ':'.") + flags.StringVar(&cmd.root, "root", "", "A directory to store data in. Defaults to a location in the OS temp directory.") + flags.StringVar(&cmd.extraArgs, "extraargs", "", + "List of comma separated options that will be passed to camlistored") + return cmd + }) +} + +func (c *serverCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, "Usage: devcam [globalopts] server [serveropts]\n") +} + +func (c *serverCmd) Examples() []string { + return []string{ + "-wipe -mysql -fullclosure", + } +} + +func (c *serverCmd) Describe() string { + return "run the stand-alone camlistored in dev mode." +} + +func (c *serverCmd) checkFlags(args []string) error { + if len(args) != 0 { + c.Usage() + } + if c.mini { + if c.things { + return cmdmain.UsageError("--mini and --makethings are mutually exclusive.") + } + c.publish = false + c.hello = false + } + if c.things && !c.wipe { + return cmdmain.UsageError("--makethings requires --wipe.") + } + nindex := 0 + for _, v := range []bool{c.mongo, c.mysql, c.postgres, c.sqlite, c.memory, c.kvfile} { + if v { + nindex++ + } + } + if nindex > 1 { + return fmt.Errorf("Only one index option allowed") + } + + if _, err := strconv.ParseInt(c.port, 0, 0); err != nil { + return fmt.Errorf("Invalid -port value: %q", c.port) + } + return nil +} + +func (c *serverCmd) setRoot() error { + if c.root == "" { + if root, err := rootInTmpDir(); err != nil { + return err + } else { + c.root = filepath.Join(root, "port"+c.port) + } + } + log.Printf("Temp dir root is %v", c.root) + if c.wipe { + log.Printf("Wiping %v", c.root) + if err := os.RemoveAll(c.root); err != nil { + return fmt.Errorf("Could not wipe %v: %v", c.root, err) + } + } + return nil +} + +func (c *serverCmd) makeSuffixdir(fullpath string) { + if err := os.MkdirAll(fullpath, 0755); err != nil { + log.Fatalf("Could not create %v: %v", fullpath, err) + } +} + +func (c *serverCmd) setEnvVars() error { + c.env.SetCamdevVars(false) + setenv := func(k, v string) { + c.env.Set(k, v) + } + if c.slow { + setenv("DEV_THROTTLE_KBPS", fmt.Sprintf("%d", c.throttle)) + setenv("DEV_THROTTLE_LATENCY_MS", fmt.Sprintf("%d", c.latency)) + } + if c.debug { + setenv("CAMLI_HTTP_DEBUG", "1") + } + user := osutil.Username() + if user == "" { + return errors.New("Could not get username from environment") + } + setenv("CAMLI_FULL_INDEX_SYNC_ON_START", "false") + if c.fullIndexSync { + setenv("CAMLI_FULL_INDEX_SYNC_ON_START", "true") + } + setenv("CAMLI_DBNAME", "devcamli"+user) + setenv("CAMLI_MYSQL_ENABLED", "false") + setenv("CAMLI_MONGO_ENABLED", "false") + setenv("CAMLI_POSTGRES_ENABLED", "false") + setenv("CAMLI_SQLITE_ENABLED", "false") + setenv("CAMLI_KVINDEX_ENABLED", "false") + setenv("CAMLI_MEMINDEX_ENABLED", "false") + setenv("CAMLI_LEVELDB_ENABLED", "false") + + setenv("CAMLI_PUBLISH_ENABLED", strconv.FormatBool(c.publish)) + setenv("CAMLI_HELLO_ENABLED", strconv.FormatBool(c.hello)) + switch { + case c.memory: + setenv("CAMLI_MEMINDEX_ENABLED", "true") + setenv("CAMLI_INDEXER_PATH", "/index-memory/") + case c.mongo: + setenv("CAMLI_MONGO_ENABLED", "true") + setenv("CAMLI_INDEXER_PATH", "/index-mongo/") + case c.postgres: + setenv("CAMLI_POSTGRES_ENABLED", "true") + setenv("CAMLI_INDEXER_PATH", "/index-postgres/") + case c.mysql: + setenv("CAMLI_MYSQL_ENABLED", "true") + setenv("CAMLI_INDEXER_PATH", "/index-mysql/") + case c.kvfile: + setenv("CAMLI_KVINDEX_ENABLED", "true") + setenv("CAMLI_INDEXER_PATH", "/index-kv/") + if c.root == "" { + panic("no root set") + } + setenv("CAMLI_DBNAME", filepath.Join(c.root, "kvindex.db")) + case c.sqlite: + setenv("CAMLI_SQLITE_ENABLED", "true") + setenv("CAMLI_INDEXER_PATH", "/index-sqlite/") + if c.root == "" { + panic("no root set") + } + setenv("CAMLI_DBNAME", filepath.Join(c.root, "sqliteindex.db")) + default: + setenv("CAMLI_LEVELDB_ENABLED", "true") + setenv("CAMLI_INDEXER_PATH", "/index-leveldb/") + if c.root == "" { + panic("no root set") + } + setenv("CAMLI_DBNAME", filepath.Join(c.root, "leveldbindex.db")) + } + + base := "http://localhost:" + c.port + c.listen = "127.0.0.1:" + c.port + if c.all { + c.listen = "0.0.0.0:" + c.port + if c.hostname == "" { + hostname, err := os.Hostname() + if err != nil { + return fmt.Errorf("Could not get system hostname: %v", err) + } + base = "http://" + hostname + ":" + c.port + } else { + base = "http://" + c.hostname + ":" + c.port + } + } + setenv("CAMLI_TLS", "false") + if c.tls { + base = strings.Replace(base, "http://", "https://", 1) + setenv("CAMLI_TLS", "true") + } + setenv("CAMLI_BASEURL", base) + + setenv("CAMLI_DEV_CAMLI_ROOT", camliSrcRoot) + setenv("CAMLI_AUTH", "devauth:pass3179") + fullSuffix := func(name string) string { + return filepath.Join(c.root, name) + } + suffixes := map[string]string{ + "CAMLI_ROOT": fullSuffix("bs"), + "CAMLI_ROOT_SHARD1": fullSuffix("s1"), + "CAMLI_ROOT_SHARD2": fullSuffix("s2"), + "CAMLI_ROOT_REPLICA1": fullSuffix("r1"), + "CAMLI_ROOT_REPLICA2": fullSuffix("r2"), + "CAMLI_ROOT_REPLICA3": fullSuffix("r3"), + "CAMLI_ROOT_CACHE": fullSuffix("cache"), + "CAMLI_ROOT_ENCMETA": fullSuffix("encmeta"), + "CAMLI_ROOT_ENCBLOB": fullSuffix("encblob"), + } + for k, v := range suffixes { + c.makeSuffixdir(v) + setenv(k, v) + } + c.makeSuffixdir(filepath.Join(fullSuffix("bs"), "packed")) + c.makeSuffixdir(filepath.Join(fullSuffix("bs"), "loose")) + setenv("CAMLI_PORT", c.port) + if c.flickrAPIKey != "" { + setenv("CAMLI_FLICKR_ENABLED", "true") + setenv("CAMLI_FLICKR_API_KEY", c.flickrAPIKey) + } + if c.foursquareAPIKey != "" { + setenv("CAMLI_FOURSQUARE_ENABLED", "true") + setenv("CAMLI_FOURSQUARE_API_KEY", c.foursquareAPIKey) + } + if c.picasaAPIKey != "" { + setenv("CAMLI_PICASA_ENABLED", "true") + setenv("CAMLI_PICASA_API_KEY", c.picasaAPIKey) + } + if c.twitterAPIKey != "" { + setenv("CAMLI_TWITTER_ENABLED", "true") + setenv("CAMLI_TWITTER_API_KEY", c.twitterAPIKey) + } + setenv("CAMLI_CONFIG_DIR", "config") + setenv("CAMLI_CACHE_DIR", filepath.Join(c.root, "cache")) + setenv("CAMLI_APP_BINDIR", "bin") + return nil +} + +func (c *serverCmd) setupIndexer() error { + args := []string{"dbinit"} + switch { + case c.postgres: + args = append(args, + "-dbtype=postgres", + "-user=postgres", + "-password=postgres", + "-host=localhost", + "-dbname="+c.env.m["CAMLI_DBNAME"]) + case c.mysql: + args = append(args, + "-user=root", + "-password=root", + "-host=localhost", + "-dbname="+c.env.m["CAMLI_DBNAME"]) + case c.sqlite: + args = append(args, + "-dbtype=sqlite", + "-dbname="+c.env.m["CAMLI_DBNAME"]) + case c.mongo: + args = append(args, + "-dbtype=mongo", + "-host=localhost", + "-dbname="+c.env.m["CAMLI_DBNAME"]) + default: + return nil + } + if c.wipe { + args = append(args, "-wipe") + } else { + args = append(args, "-ignoreexists") + } + binPath := filepath.Join("bin", "camtool") + cmd := exec.Command(binPath, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("Could not run camtool dbinit: %v", err) + } + return nil +} + +func (c *serverCmd) syncTemplateBlobs() error { + if c.wipe { + templateDir := "dev-server-template" + if _, err := os.Stat(templateDir); err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + blobsDir := filepath.Join(c.root, "sha1") + if err := cpDir(templateDir, blobsDir, nil); err != nil { + return fmt.Errorf("Could not cp template blobs: %v", err) + } + } + return nil +} + +func (c *serverCmd) setFullClosure() error { + if c.fullClosure { + oldsvn := filepath.Join(c.root, filepath.FromSlash("tmp/closure-lib/.svn")) + if err := os.RemoveAll(oldsvn); err != nil { + return fmt.Errorf("Could not remove svn checkout of closure-lib %v: %v", + oldsvn, err) + } + log.Println("Updating closure library...") + args := []string{"run", "third_party/closure/updatelibrary.go", "-verbose"} + cmd := exec.Command("go", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("Could not run updatelibrary.go: %v", err) + } + c.env.Set("CAMLI_DEV_CLOSURE_DIR", "third_party/closure/lib/closure") + } + return nil +} + +func (c *serverCmd) makeThings() error { + const importerPrefix = "/importer/" + // check that "/importer/" prefix is in config, just in case it ever changes. + configFile := filepath.Join(camliSrcRoot, "config", "dev-server-config.json") + config, err := ioutil.ReadFile(configFile) + if err != nil { + return fmt.Errorf("could not read config file %v: %v", configFile, err) + } + if !bytes.Contains(config, []byte(importerPrefix)) { + return fmt.Errorf("%s prefix not found in dev config. Did it change?", importerPrefix) + } + + if err := netutil.AwaitReachable("localhost:"+c.port, time.Minute); err != nil { + return err + } + + osutil.AddSecretRingFlag() + setCamdevVars() + + baseURL := c.env.m["CAMLI_BASEURL"] + if baseURL == "" { + return errors.New("CAMLI_BASEURL is not set") + } + + cl := client.New(baseURL) + signer, err := cl.Signer() + if err != nil { + return err + } + ClientId := make(map[string]string) + ClientSecret := make(map[string]string) + for name := range importer.All() { + ClientId[name] = "fakeStaticClientId" + ClientSecret[name] = "fakeStaticClientSecret" + } + hc := importer.HostConfig{ + BaseURL: baseURL, + Prefix: importerPrefix, + Target: cl, + BlobSource: cl, + Signer: signer, + Search: cl, + ClientId: ClientId, + ClientSecret: ClientSecret, + } + + for name, imp := range importer.All() { + mk, ok := imp.(importer.TestDataMaker) + if !ok { + continue + } + + tr := mk.MakeTestData() + + hc.HTTPClient = &http.Client{Transport: tr} + host, err := importer.NewHost(hc) + if err != nil { + return fmt.Errorf("could not obtain Host: %v", err) + } + + rc, err := importer.CreateAccount(host, name) + if err != nil { + return err + } + + if err := mk.SetTestAccount(rc.AccountNode()); err != nil { + return fmt.Errorf("could not set fake account node for importer %v: %v", name, err) + } + + if err := imp.Run(rc); err != nil { + return err + } + } + return nil +} + +func (c *serverCmd) RunCommand(args []string) error { + err := c.checkFlags(args) + if err != nil { + return cmdmain.UsageError(fmt.Sprint(err)) + } + if !*noBuild { + withSqlite = c.sqlite + targets := []string{ + filepath.Join("server", "camlistored"), + filepath.Join("cmd", "camtool"), + } + if c.hello { + targets = append(targets, filepath.Join("app", "hello")) + } + if c.publish { + targets = append(targets, filepath.Join("app", "publisher")) + } + for _, name := range targets { + err := build(name) + if err != nil { + return fmt.Errorf("Could not build %v: %v", name, err) + } + } + } + if err := c.setRoot(); err != nil { + return fmt.Errorf("Could not setup the camli root: %v", err) + } + if err := c.setEnvVars(); err != nil { + return fmt.Errorf("Could not setup the env vars: %v", err) + } + // wipeCacheDir needs to be called after setEnvVars, because that is where + // CAMLI_CACHE_DIR is defined. + if *wipeCache { + c.env.wipeCacheDir() + } + if err := c.setupIndexer(); err != nil { + return fmt.Errorf("Could not setup the indexer: %v", err) + } + if err := c.syncTemplateBlobs(); err != nil { + return fmt.Errorf("Could not copy the template blobs: %v", err) + } + if err := c.setFullClosure(); err != nil { + return fmt.Errorf("Could not setup the closure lib: %v", err) + } + + log.Printf("Starting dev server on %v/ui/ with password \"pass3179\"\n", + c.env.m["CAMLI_BASEURL"]) + + camliBin := filepath.Join("bin", "camlistored") + cmdArgs := []string{ + "-configfile=" + filepath.Join(camliSrcRoot, "config", "dev-server-config.json"), + "-listen=" + c.listen, + "-openbrowser=" + strconv.FormatBool(c.openBrowser), + } + if c.extraArgs != "" { + cmdArgs = append(cmdArgs, strings.Split(c.extraArgs, ",")...) + } + if c.things { + // force camlistored to be run as a child process instead of with + // syscall.Exec, so c.makeThings() is able to run. + sysExec = nil + go func() { + if err := c.makeThings(); err != nil { + log.Fatalf("%v", err) + } + }() + } + return runExec(camliBin, cmdArgs, c.env) +} diff --git a/vendor/github.com/camlistore/camlistore/dev/devcam/test.go b/vendor/github.com/camlistore/camlistore/dev/devcam/test.go new file mode 100644 index 00000000..013b487c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/devcam/test.go @@ -0,0 +1,169 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// This file adds the "test" subcommand to devcam, to run the full test suite. + +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "camlistore.org/pkg/cmdmain" +) + +type testCmd struct { + // start of flag vars + verbose bool + precommit bool + short bool + run string + // end of flag vars + + // buildGoPath becomes our child "go" processes' GOPATH environment variable + buildGoPath string +} + +func init() { + cmdmain.RegisterCommand("test", func(flags *flag.FlagSet) cmdmain.CommandRunner { + cmd := new(testCmd) + flags.BoolVar(&cmd.short, "short", false, "Use '-short' with go test.") + flags.BoolVar(&cmd.precommit, "precommit", true, "Run the pre-commit githook as part of tests.") + flags.BoolVar(&cmd.verbose, "v", false, "Use '-v' (for verbose) with go test.") + flags.StringVar(&cmd.run, "run", "", "Use '-run' with go test.") + return cmd + }) +} + +func (c *testCmd) Usage() { + fmt.Fprintf(cmdmain.Stderr, "Usage: devcam test [test_opts] [targets]\n") +} + +func (c *testCmd) Describe() string { + return "run the full test suite, or the tests in the specified target packages." +} + +func (c *testCmd) RunCommand(args []string) error { + if c.precommit { + if err := c.runPrecommitHook(); err != nil { + return err + } + } + if err := c.syncSrc(); err != nil { + return err + } + buildSrcDir := filepath.Join(c.buildGoPath, "src", "camlistore.org") + if err := os.Chdir(buildSrcDir); err != nil { + return err + } + if err := c.buildSelf(); err != nil { + return err + } + if err := c.runTests(args); err != nil { + return err + } + println("PASS") + return nil +} + +func (c *testCmd) env() *Env { + if c.buildGoPath == "" { + panic("called too early") + } + env := NewCopyEnv() + env.NoGo() + env.Set("GOPATH", c.buildGoPath) + env.Set("CAMLI_MAKE_USEGOPATH", "true") + env.Set("GO15VENDOREXPERIMENT", "1") + return env +} + +func (c *testCmd) syncSrc() error { + args := []string{"run", "make.go", "--onlysync"} + cmd := exec.Command("go", args...) + cmd.Stderr = os.Stderr + out, err := cmd.Output() + if err != nil { + return fmt.Errorf("Error populating tmp src tree: %v", err) + } + c.buildGoPath = strings.TrimSpace(string(out)) + return nil +} + +func (c *testCmd) buildSelf() error { + args := []string{ + "install", + filepath.FromSlash("./dev/devcam"), + } + cmd := exec.Command("go", args...) + binDir, err := filepath.Abs("bin") + if err != nil { + return fmt.Errorf("Error setting GOBIN: %v", err) + } + env := c.env() + env.Set("GOBIN", binDir) + cmd.Env = env.Flat() + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("Error building devcam: %v", err) + } + return nil +} + +func (c *testCmd) runTests(args []string) error { + targs := []string{"test"} + if !strings.HasSuffix(c.buildGoPath, "-nosqlite") { + targs = append(targs, "--tags=with_sqlite fake_android") + } else { + targs = append(targs, "--tags=fake_android") + } + if c.short { + targs = append(targs, "-short") + } + if c.verbose { + targs = append(targs, "-v") + } + if c.run != "" { + targs = append(targs, "-run="+c.run) + } + if len(args) > 0 { + targs = append(targs, args...) + } else { + targs = append(targs, []string{ + "./pkg/...", + "./server/camlistored", + "./server/appengine", + "./cmd/...", + }...) + } + env := c.env() + env.Set("SKIP_DEP_TESTS", "1") + return runExec("go", targs, env) +} + +func (c *testCmd) runPrecommitHook() error { + out, err := exec.Command(filepath.FromSlash("./bin/devcam"), "hook", "pre-commit", "test").CombinedOutput() + if err != nil { + fmt.Println(string(out)) + } + return err + +} diff --git a/vendor/github.com/camlistore/camlistore/dev/envvardoc/envvardoc.go b/vendor/github.com/camlistore/camlistore/dev/envvardoc/envvardoc.go new file mode 100644 index 00000000..1ccbaecc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/envvardoc/envvardoc.go @@ -0,0 +1,189 @@ +// Program envvardoc will verify all referenced environment variables in go +// source are properly documented. +package main + +import ( + "bufio" + "flag" + "fmt" + "io" + "log" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "text/tabwriter" +) + +var ( + srcDirs = flag.String("srcDirs", "cmd,dev,pkg,server", + "comma separated source directories") + doc = flag.String("doc", "doc/environment-vars.txt", + "file containing environment variable documentation") + all = flag.Bool("all", false, "show all environment vars found") + prefixes = flag.String("prefixes", "CAM,DEV,AWS", + "comma-separated list of env var prefixes we care about. Empty implies all") + + docVar = regexp.MustCompile(`^(\w+) \(.+?\):$`) + literalEnvVar = regexp.MustCompile(`os.Getenv\("(\w+)"\)`) + variableEnvVar = regexp.MustCompile(`os.Getenv\((\w+)\)`) +) + +type pos struct { + line int + path string +} + +func (p pos) String() string { + return fmt.Sprintf("%s:%d", p.path, p.line) +} + +type varMap map[string][]pos + +func sortedKeys(m varMap) []string { + keys := make([]string, 0, len(m)) + for k, _ := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +type envCollector struct { + literals varMap + variables varMap + documented map[string]struct{} +} + +func newEncCollector() *envCollector { + return &envCollector{ + literals: varMap{}, + variables: varMap{}, + documented: map[string]struct{}{}, + } +} + +func (ec *envCollector) findEnvVars(path string, r io.Reader) error { + scanner := bufio.NewScanner(r) + line := 1 + for scanner.Scan() { + l := scanner.Text() + m := literalEnvVar.FindStringSubmatch(l) + if len(m) == 2 { + p := pos{line: line, path: path} + ec.literals[m[1]] = append(ec.literals[m[1]], p) + } + + m = variableEnvVar.FindStringSubmatch(l) + if len(m) == 2 { + p := pos{line: line, path: path} + ec.variables[m[1]] = append(ec.variables[m[1]], p) + } + line++ + } + return scanner.Err() +} + +func (ec *envCollector) findDocVars(r io.Reader) error { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + l := scanner.Text() + m := docVar.FindStringSubmatch(l) + if len(m) == 2 { + ec.documented[m[1]] = struct{}{} + } + } + return scanner.Err() +} + +func (ec *envCollector) walk(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() || !strings.HasSuffix(path, ".go") { + return nil + } + + r, err := os.Open(path) + if err != nil { + return err + } + defer r.Close() + return ec.findEnvVars(path, r) +} + +func printMap(header string, m varMap) { + w := new(tabwriter.Writer) + w.Init(os.Stdout, 0, 8, 1, ' ', 0) + fmt.Fprintln(w, header) + for _, k := range sortedKeys(m) { + for _, pos := range m[k] { + fmt.Fprintf(w, "%s\t%s\n", k, pos) + } + } + w.Flush() +} + +func (ec *envCollector) printAll() { + fmt.Println("All environment variables") + printMap("Literal\tLocation", ec.literals) + fmt.Println() + printMap("Variable\tLocation", ec.variables) +} + +func (ec *envCollector) printUndocumented(prefixes []string) bool { + missing := varMap{} + for k, v := range ec.literals { + if _, ok := ec.documented[k]; !ok { + keep := false + for _, p := range prefixes { + if strings.HasPrefix(k, p) { + keep = true + break + } + } + if keep || len(prefixes) == 0 { + missing[k] = v + } + } + } + + if len(missing) != 0 { + printMap("Undocumented\tLocation", missing) + } else { + fmt.Println("All environment variables are documented") + } + return len(missing) != 0 +} + +func main() { + flag.Parse() + ec := newEncCollector() + + r, err := os.Open(*doc) + if err != nil { + log.Fatal(err) + } + defer r.Close() + err = ec.findDocVars(r) + if err != nil { + log.Fatal(err) + } + + for _, dn := range strings.Split(*srcDirs, ",") { + err := filepath.Walk(dn, ec.walk) + if err != nil { + log.Fatal(err) + } + } + + if *all { + ec.printAll() + } else { + if ec.printUndocumented(strings.Split(*prefixes, ",")) { + os.Exit(1) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/dev/local.sh b/vendor/github.com/camlistore/camlistore/dev/local.sh new file mode 100644 index 00000000..c302d9c6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/local.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +export CAMLI_CONFENV_SECRET_RING=$(camtool env camsrcroot)/pkg/jsonsign/testdata/test-secring.gpg +export CAMLI_CONFIG_DIR=$(camtool env camsrcroot)/dev/config-dir-local + +# Redundant, but: +export CAMLI_DEFAULT_SERVER=dev diff --git a/vendor/github.com/camlistore/camlistore/dev/make-release b/vendor/github.com/camlistore/camlistore/dev/make-release new file mode 100755 index 00000000..4aa3cc34 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/make-release @@ -0,0 +1,45 @@ +#!/usr/bin/perl + +use strict; +use Getopt::Long; +my $opt_force; +GetOptions("force" => \$opt_force) or die "Usage: make-release [-f] "; + +my $version = shift or die "Usage: make-release "; + +die "Not being run from root of Camlistore" unless -e ".git" && -e "pkg/blob/ref.go"; + +my $cur_branch = `git rev-parse --abbrev-ref HEAD`; +chomp $cur_branch; +die "Not on master" unless $cur_branch eq "master"; + +my $new_branch = "releases/$version"; + +if ($opt_force) { + system("git", "tag", "-d", $version); + system("git", "branch", "-D", $new_branch); +} + +system("git", "checkout", "-b", $new_branch) and die "Failed to create branch $new_branch from master. Does it already exist?"; + +open(my $fh, ">VERSION") or die; +print $fh "$version\n"; +close($fh); + +system("git", "add", "VERSION") and die; +system("git", "commit", "-m", "Add VERSION file on the $new_branch branch.") and die "Failed to commit"; +system("git", "tag", $version) and die "Failed to tag"; + +my $commit = do { open(my $f, ".git/refs/tags/$version") or die; local $/; <$f> }; +chomp $commit; + +system("git", "checkout", "master") and die; +open(my $fh, ">>misc/release-history-tags"); +print $fh "$commit\t$version\n"; +close($fh); + +print "Created branch $new_branch from master, cleaned it and wrote VERSION file, & tagged $version.\n"; +print "\n"; +print "Push with:\n"; +print "\$ git push github refs/tags/$version:refs/tags/$version\n"; +print "\$ git push github $new_branch:$new_branch\n"; diff --git a/vendor/github.com/camlistore/camlistore/dev/push b/vendor/github.com/camlistore/camlistore/dev/push new file mode 100755 index 00000000..45f00a32 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/push @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +git push origin master +git push github master +curl http://camlistore.org/mailnow diff --git a/vendor/github.com/camlistore/camlistore/dev/update_closure_compiler.go b/vendor/github.com/camlistore/camlistore/dev/update_closure_compiler.go new file mode 100644 index 00000000..847a3230 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/dev/update_closure_compiler.go @@ -0,0 +1,139 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// update_closure_compiler downloads a new version +// of the closure compiler if the one in tmp/closure-compiler +// doesn't exist or is older than the requested version. +package main + +import ( + "archive/zip" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + + "camlistore.org/pkg/osutil" +) + +const ( + compilerDirURL = "http://closure-compiler.googlecode.com/files/" + compilerVersion = "20121212" +) + +var rgxVersion = regexp.MustCompile(`.*Version: (.*) \(revision.*`) + +func main() { + + // check JRE presence + _, err := exec.LookPath("java") + if err != nil { + log.Fatal("Didn't find 'java' in $PATH. The Java Runtime Environment is needed to run the closure compiler.\n") + } + + camliRootPath, err := osutil.GoPackagePath("camlistore.org") + if err != nil { + log.Fatal("Package camlistore.org not found in $GOPATH (or $GOPATH not defined).") + } + destDir := filepath.Join(camliRootPath, "tmp", "closure-compiler") + // check if compiler already exists + jarFile := filepath.Join(destDir, "compiler.jar") + _, err = os.Stat(jarFile) + if err == nil { + // if compiler exists, check version + cmd := exec.Command("java", "-jar", jarFile, "--version", "--help", "2>&1") + output, _ := cmd.CombinedOutput() + m := rgxVersion.FindStringSubmatch(string(output)) + if m == nil { + log.Fatalf("Could not find compiler version in %q", output) + } + if m[1] == compilerVersion { + log.Printf("compiler already at version %v , nothing to do.", compilerVersion) + os.Exit(0) + } + if err := os.Remove(jarFile); err != nil { + log.Fatalf("Could not remove %v: %v", jarFile, err) + } + } else { + if !os.IsNotExist(err) { + log.Fatalf("Could not stat %v: %v", jarFile, err) + } + } + + // otherwise, download compiler + log.Printf("Getting closure compiler version %s.\n", compilerVersion) + if err := os.MkdirAll(destDir, 0755); err != nil { + log.Fatal(err) + } + if err := os.Chdir(destDir); err != nil { + log.Fatal(err) + } + zipFilename := "compiler-" + compilerVersion + ".zip" + compilerURL := compilerDirURL + zipFilename + resp, err := http.Get(compilerURL) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + f, err := os.Create(zipFilename) + if err != nil { + log.Fatal(err) + } + if _, err := io.Copy(f, resp.Body); err != nil { + log.Fatal(err) + } + if err := f.Close(); err != nil { + log.Fatal(err) + } + + r, err := zip.OpenReader(zipFilename) + if err != nil { + log.Fatal(err) + } + for x, f := range r.File { + if f.FileHeader.Name != "compiler.jar" { + if x == len(r.File)-1 { + log.Fatal("compiler.jar was not found in the zip archive") + } + continue + } + rc, err := f.Open() + if err != nil { + log.Fatal(err) + } + g, err := os.Create(jarFile) + if err != nil { + log.Fatal(err) + } + defer g.Close() + if _, err = io.Copy(g, rc); err != nil { + log.Fatal(err) + } + rc.Close() + break + } + + if err := r.Close(); err != nil { + log.Fatal(err) + } + if err := os.Remove(zipFilename); err != nil { + log.Fatal(err) + } + log.Printf("Success. Installed at %v", jarFile) +} diff --git a/vendor/github.com/camlistore/camlistore/doc/app-environment.txt b/vendor/github.com/camlistore/camlistore/doc/app-environment.txt new file mode 100644 index 00000000..e5d5b97f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/app-environment.txt @@ -0,0 +1,31 @@ +Camlistore applications run with the following environment variables set: + +CAMLI_API_HOST (string): + URL prefix of the Camlistore server which the app should use to make API calls. + It always ends in a trailing slash. Examples: + https://foo.org:3178/pub/ + https://foo.org/pub/ + http://192.168.0.1/ + http://192.168.0.1:1234/ + +CAMLI_APP_BACKEND_URL (string): + URL of the application's process, always ending in a trailing slash. That path + represents the top-most path that requests will hit. The path usually matches + the path as visible in the outside world when camlistored is proxying an app, + but that is not guaranteed. Examples: + https://foo.org:3178/pub/ + https://foo.org/pub/ + http://192.168.0.1/ + http://192.168.0.1:1234/ + +CAMLI_APP_CONFIG_URL (string): + URL containing JSON configuration for the app. The app should once, upon + startup, fetch this URL (using CAMLI_AUTH) to retrieve its configuration data. + The response JSON is the contents of the app's "appConfig" part of the config + file. + +CAMLI_AUTH (string): + Username and password (username:password) that the app should use to + authenticate over HTTP basic auth with the Camlistore server. Basic auth is + unencrypted, hence it should only be used with HTTPS or in a secure (local + loopback) environment. diff --git a/vendor/github.com/camlistore/camlistore/doc/blog-notes.txt b/vendor/github.com/camlistore/camlistore/doc/blog-notes.txt new file mode 100644 index 00000000..b8c397e0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/blog-notes.txt @@ -0,0 +1,40 @@ +Thoughts on storing a blog in Camlistore and serving it from the +publish handler. + +* a blog is a permanode + +* a blog post is a permanode + +* the post's permanode is a member of the blog's permanode + +* views of the blog we'd like: + + 1) reverse chronological (typical blog view) + + - needs efficient reverse time index on membership. + + - membership is currently "add-attribute" claims on parent + permanode, implying that a large/old blog with thousands + of posts will involve resolving the attributes of + the blog's permanode all the time. we need to either make + that efficient (caching it as a function of last mutation + claim to that permanode?) or find a different model + for memberships. I'm inclined to say keep the model + and make it fast. + + 2) forward chronological by date posted. (year, month, day view) + + - denormalization question. the date of the blog post should + be an attribute of the post's permanode (defaulting to the + date of the first/last claim mutation on it), but for efficient + indexing we'll need to either mirror this into the blog + permanode's attributes, or have another attribute on the + blog post that we can prefix scan that includes as the prefix + the blog's permanode. the latter is probably ideal so + blog posts can be cross-posted to multiple blogs, and keeps + the number of attributes on the blog permanode lower. + + e.g. blog post can have (add-)attributes: + + "inparent" => "| + diff --git a/vendor/github.com/camlistore/camlistore/doc/environment-vars.txt b/vendor/github.com/camlistore/camlistore/doc/environment-vars.txt new file mode 100644 index 00000000..29db5255 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/environment-vars.txt @@ -0,0 +1,215 @@ +The standard library's strconv.ParseBool() is used to parse boolean environment +variables. It accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, +False. Any other value is an implicit false. + +For integer values, strconv.Atoi() is used which means only base 10 numbers are +valid. + +AWS_ACCESS_KEY_ID (string): +AWS_ACCESS_KEY_SECRET (string): + See http://docs.aws.amazon.com/fws/1.1/GettingStartedGuide/index.html?AWSCredentials.html + Used in s3 tests. If not set some tests are skip. If set, queries will be + sent to Amazon's S3 service. + +CAMLI_APP_BINDIR (string): + Path to the directory where Camlistore first looks for the server applications + executables, when starting them. It looks in PATH otherwise. + +CAMLI_AUTH (string): + See http://camlistore.org/docs/server-config + Used as a fallback in pkg/client.Client (except on android) when + configuration files lack and 'auth' entry. If a client is using the -server + commandline to specify the camlistore instance to talk to, this env var + takes precedence over that specified in the configuration files. + +CAMLI_BASEURL (string): + URL set in devcam to act as a baseURL in the devcam launched camlistored. + +CAMLI_CACHE_DIR (string): + Path used by pkg/osutil to override operating system specific cache + directory. + +CAMLI_CONFIG_DIR (string): + Path used by pkg/osutil to override operating system specific configuration + directory. + +CAMLI_DBNAME (string): + Backend specific data source name (DSN). + Set in devcam to pass database configuration for the indexer to the devcam + launched camlistored. + +CAMLI_DEBUG (bool): + Used by camlistored and camput to enable additional commandline options. + Used in pkg/schema to enable additional logging. + +CAMLI_DEBUG_CONFIG (bool): + Causes pkg/serverconfig to dump low-level configuration derived from + high-level configuation on load. + +CAMLI_DEBUG_X (string): + String containing magic substring(s) to enable debuggging in code. + +CAMLI_DEBUG_UPLOADS (bool): + Used by pkg/client to enable additional logging. + +CAMLI_DEFAULT_SERVER (string): + The server alias to use by default. The string is the server's alias key + in the client-config.json "servers" object. If set, the CAMLI_DEFAULT_SERVER + takes precedence over the "default" bool in client-config.json. + +CAMLI_DEV_CAMLI_ROOT (string): + If set, the base directory of Camlistore when in dev mode. + Used by pkg/server for finding static assests (js, css, html). + Used as a signal by pkg/index/* and pkg/server to output more helpful error + message when run under devcam. + +CAMLI_DEV_CLOSURE_DIR (string): + Path override for pkg/server. If specified, this path will be used to serve + the closure handler. + +CAMLI_DISABLE_IMPORTERS (bool): + If true, importers are disabled (at least automatic background + importing, e.g. at start-up). Mostly for debugging. + +CAMLI_FAST_DEV (bool): + Used by dev/demo.sh for giving presentations with devcam server/put/etc + for faster pre-built builds, without calling make.go. + +CAMLI_FORCE_OSARCH (bool): + Used by make.go to force building an unrecommended OS/ARCH pair. + +CAMLI_GCE_*: + Variables prefixed with CAMLI_GCE_ concern the Google Compute Engine deploy handler in + pkg/deploy/gce, which is only used by camweb to launch Camlistore on Google Compute + Engine. They do not affect Camlistore's behaviour. + +CAMLI_GCE_CLIENTID (string): + See CAMLI_GCE_* first. This string is used by gce.DeployHandler as the application's + OAuth Client ID. If blank, camweb does not enable the Google Compute Engine launcher. + +CAMLI_GCE_CLIENTSECRET (string): + See CAMLI_GCE_* first. Used by gce.DeployHandler as the application's OAuth Client + Secret. If blank, gce.NewDeployHandler returns an error, and camweb fails to start if + the Google Compute Engine launcher was enabled. + +CAMLI_GCE_DATA (string): + See CAMLI_GCE_* first. Path to the directory where gce.DeployHandler stores the + instances configuration and state. If blank, the "camli-gce-data" default is used + instead. + +CAMLI_GCE_PROJECT (string): + See CAMLI_GCE_* first. ID of the Google Project that provides the above client ID and + secret. It is used when we query for the list of all the existing zones, since such a + query requires a project ID. If blank, a hard-coded list of zones is used instead. + +CAMLI_GCE_SERVICE_ACCOUNT (string): + See CAMLI_GCE_* first. Path to a Google service account JSON file. This account should + have at least compute.readonly permissions on the Google Project wih ID CAMLI_GCE_PROJECT. + It is used to authenticate when querying for the list of all the existing zones. If blank, + a hard-coded list of zones is used instead. + +CAMLI_GCE_XSRFKEY (string): + See CAMLI_GCE_* first. Used by gce.DeployHandler as the XSRF protection key. If blank, + gce.NewDeployHandler generates a new random key instead. + +CAMLI_HTTP_DEBUG (bool): + Enable per-request logging in pkg/webserver. + +CAMLI_HTTP_EXPVAR (bool): + Enable json export of expvars at /debug/vars + +CAMLI_HTTP_PPROF (bool): + Enable standard library's pprof handler at /debug/pprof/ + +CAMLI_IGNORED_FILES (string): + Override client configuration option 'ignoredFiles'. + Comma-seperated list of files to be ignored by pkg/client when uploading. + +CAMLI_INCLUDE_PATH (string): + Path to search for files. + Referenced in pkg/osutil and used indirectly by pkg/jsonconfig.ConfigParser + to search for files mentioned in configurations. This is used as a last + resort after first checking the current directory and the camlistore config + directory. It should be in the OS path form, i.e. unix-like systems would be + /path/1:/path/two:/some/other/path, and Windows would be C:\path\one;D:\path\2 + +CAMLI_KEYID (string): + Optional GPG identity to use, taking precedence over config files. + Used by devcam commands, in config/dev-server-config.json, and + config/dev-client-dir/client-config.json as the public ID of the GPG + key to use for signing. + +CAMLI_KV_VERIFY (bool): + Enable all the VerifyDb* options in cznic/kv, to e.g. track down + corruptions. + +CAMLI_KVINDEX_ENABLED (bool): + Use cznic/kv as the indexer. Variable used only by devcam server. + +CAMLI_LEVELDB_ENABLED (bool): + Use syndtr/goleveldb as the indexer. Variable used only by devcam server. + +CAMLI_MEMINDEX_ENABLED (bool): + Use a memory-only indexer. Supported only by devcam server. + +CAMLI_MONGO_WIPE (bool): + Wipe out mongo based index on startup. + +CAMLI_MAKE_USEGOPATH (bool): + When running make.go, overrides the -use_gopath flag. + +CAMLI_NO_FILE_DUP_SEARCH (bool): + This will cause the search-for-exists-before-upload step to be skipped when + camput is uploading files. + +CAMLI_PPROF_START (string): + Filename base to write a ".cpu" and ".mem" profile out + to during server start-up. Used to profile index corpus scanning, + mostly. + +CAMLI_QUIET (bool): + Used by devcam to enable -verbose flag for camput/camget. + +CAMLI_SECRET_RING (string): + Path to the GPG secret keyring, which is otherwise set by identitySecretRing + in the server config, and secretRing in the client config. + +CAMLI_DISABLE_CLIENT_CONFIG_FILE (bool): + If set, the pkg/client code will never use the on-disk config file. + +CAMLI_TRACK_FS_STATS (bool): + Enable operation counts for fuse filesystem. + +CAMLI_TRUSTED_CERT (string): + Override client configuration option 'trustedCerts'. + Comma-seperated list of paths to trusted certificate fingerprints. + +CAMPUT_ANDROID_OUTPUT (bool): + Enable pkg/client status messages to print to stdout. Used in android client. + +CAMLI_DEBUG_IMAGES (bool): + Enable extra debugging in pkg/images when decoding images. Used by indexers. + +CAMLI_DISABLE_DJPEG (bool): + Disable use of djpeg(1) to down-sample JPEG images by a factor of 2, 4 or 8. + Only has an effect when djpeg is found in the PATH. + +CAMLI_DISABLE_THUMB_CACHE (bool): + If true, no thumbnail caching is done, and URLs even have cache + buster components, to force browsers to reload a lot. + +CAMLI_VAR_DIR (string): + Path used by pkg/osutil to override operating system specific application + storage directory. Generally unused. + +CAMLI_S3_FAIL_PERCENT (int): + Number from 0-100 of what percentage of the time to fail receiving blobs + for the S3 handler. + +DEV_THROTTLE_KBPS (integer): +DEV_THROTTLE_LATENCY_MS (integer): + Rate limit and/or inject latency in pkg/webserver responses. A value of 0 + disables traffic-shaping. + +RUN_BROKEN_TESTS (bool): + Run known-broken tests. diff --git a/vendor/github.com/camlistore/camlistore/doc/example-blobs/README.txt b/vendor/github.com/camlistore/camlistore/doc/example-blobs/README.txt new file mode 100644 index 00000000..2d8d1e18 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/example-blobs/README.txt @@ -0,0 +1,2 @@ +A random collection of collection of blobs for reference in mailing +lists and other docs. diff --git a/vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-648357ea2ee9dd7031d0ff786840e6deac8b7a6a.dat b/vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-648357ea2ee9dd7031d0ff786840e6deac8b7a6a.dat new file mode 100644 index 00000000..6526ba82 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-648357ea2ee9dd7031d0ff786840e6deac8b7a6a.dat @@ -0,0 +1,5 @@ +{"camliVersion": 1, + "camliSigner": "sha1-c4da9d771661563a27704b91b67989e7ea1e50b8", + "camliType": "permanode", + "random": "}6{T2o+u<[!aE.&babHX" +,"camliSig":"iQEcBAABAgAGBQJNScvDAAoJEGjzeDN/6vt8h+sIAILavU5sLhdtb5zsdLQJE5uVzWVNyImtUS+xlH8qi+LFefGVqlMpuij0mzuNBybRKKYkJeYXD/bQ4hZb/6XgzQWHhcXpDqLJrLRVK6jjflaOv23bDZm8J/1Q0pMe291qfON+iX2KD7F1f6yCY60uUMTwaF/0MfJxITH9sPdD0AhNxJUNPSkviUnPa9YLr8S3NmARsReV1EorvYQmZI2y2cJdfWgQ4LAghJhWIE1LwexHcOuiDLg2QLZiGx6sqhBCKpIfAzigSXs/ehIBNC9uSjkiWa4aRIWk6MGxkg1z4yzERIjJ38+1vJh1866DMeo5fxwH1K2o53gvBm+xq1UJXcg==iqHS"} diff --git a/vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-648357ea2ee9dd7031d0ff786840e6deac8b7a6a.txt b/vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-648357ea2ee9dd7031d0ff786840e6deac8b7a6a.txt new file mode 100644 index 00000000..163ff545 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-648357ea2ee9dd7031d0ff786840e6deac8b7a6a.txt @@ -0,0 +1 @@ +A permanode, signed by sha1-c4da9d771661563a27704b91b67989e7ea1e50b8. diff --git a/vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-c4da9d771661563a27704b91b67989e7ea1e50b8.dat b/vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-c4da9d771661563a27704b91b67989e7ea1e50b8.dat new file mode 100644 index 00000000..de8bc48d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-c4da9d771661563a27704b91b67989e7ea1e50b8.dat @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +mQENBE0zz0ABCACxjNdfvUMA1iHFKIMaHQadNrpQVlH+/+3bgJO7dPeKv3eg/TC1 +OdmN3PdZg0u0KxfAhs+Q7SFPD53YWFvd333WJwiGpDdJ1QA538BQnxHGGGxYGCIQ +Rr1rgB4wm/tWh326/uXWpA7RAty2Mm64UkgqULuZVrMxNjXm3e+q9QNNnBp1dWpo +k1/j8y9Z+a1zoxFK5zw3i15hV8zmpMsoLLRBBkvT2qYasThGUM65tDkkoCCW+Qom +NoRiAx4bDEI1y5pP+CUmJTr9Zo+N20IXC5tDTRxRLOYprxxDiw3Mg0ML6PSMitcM +u5LMztm+hJ2FCg/n0ZkKXXFTEGr6lflaIHUxABEBAAG0IUJyYWQgRml0enBhdHJp +Y2sgPGJyYWRAZGFuZ2EuY29tPokBOAQTAQIAIgUCTTPPQAIbAwYLCQgHAwIGFQgC +CQoLBBYCAwECHgECF4AACgkQaPN4M3/q+3w6hgf/bHBFllZgaicklI6iaGedDh44 +rvwSzpOQ3v2VyfqG6CrdgGa6JpqMTntvLygxaFQBkLwQYDX8jgxqkm7fOGYXqoK1 +UPAkORNAt8gyqYA/Uo3jYI8VvJ5NK/BvYgiGCFwceuXLus24+PuKnp2PMpEn7fe1 +Yz57vOO3khsvA+nPOoWYhK++Xv/TG5qVStV8eON0WXDbyoUcVFyUEFCjE0ipCUBe +rEFxotgtiJ+OscY4XtbXnOa5ztqW0xEAnSSlomauJWmLZn5uaJceaObB1tf+r8af +WkjSU+YLjqZ/HdUmg/6Kv/Xhk3fn5v2tDLjkbmwBhhT46g/zrvWVk1rnMPZGaLkB +DQRNM89AAQgA1U4HDH2s/Fy2f7WSB6pqQVHcGqVLjj3KtnDV2zBxa5ps6NiMaZav +LdPcj1DcaytvmGMN+VeWy4nR2hYMNej645ZIVuuPKnLFe5ir9gKEYcM6nE+BSGUp +bkIhLsitXmcc2fXJfP7aZOl61EsQJyihrNb7yWT8ViX+txC8/59ftdBB+eB8G+Dh +I0KVRfZlMgHA1Et8o9FsBULrw2zFuqdRYBQsidnaOnG61UN1/R1iij7vAQEGxJaH +jghL6OYthi6dFoYlKuBc8439UonKsiV80wmH6fZdJxM//WfpNY5N3c2fvRmR/PNR +UXxOZ8ySArBphimH5jMlUJ5bJAQYLj4KcQARAQABiQEfBBgBAgAJBQJNM89AAhsM +AAoJEGjzeDN/6vt8hX8H/1Veh1wFJW2qo4hkTOD9JTLirHiDGK11L+Xy/PR8mu3p +dgdFv1kSUe4i3Qqrf606kF8TkjFVbWd8zvmANgLfwAPJD5xWJMNWT2dvpKxzSvjj +DTyeRM0wcUMCvgiWrQdbyoGbh0UZu7QAt7OihHlUbFH9EU3IpimayzRTDYLhLyWI +0bgFaH6BdbmpL4flAs31AI6WHfBZmAO34hAEdMxBDEPIQpSqRZn3GmgAknIw5bUk +emjIVDVO1402eZTLRGDi/1Aw9ey/GJUPeSxm8JqHRRLhpuTrMEtGe7xv2oDskhaG +u+wLedM+EXyuni2oGRcAQPBBNFyOoABzBUhW+bK13oo= +=361b +-----END PGP PUBLIC KEY BLOCK----- diff --git a/vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-c4da9d771661563a27704b91b67989e7ea1e50b8.txt b/vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-c4da9d771661563a27704b91b67989e7ea1e50b8.txt new file mode 100644 index 00000000..185b70c9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/example-blobs/sha1-c4da9d771661563a27704b91b67989e7ea1e50b8.txt @@ -0,0 +1,2 @@ +Brad's public key, used to sign the permanode in +sha1-648357ea2ee9dd7031d0ff786840e6deac8b7a6a.dat diff --git a/vendor/github.com/camlistore/camlistore/doc/json-signing/example/public-key.txt b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/public-key.txt new file mode 100644 index 00000000..813ebf8f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/public-key.txt @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.11 (GNU/Linux) + +mQENBEzgoVsBCAC/56aEJ9BNIGV9FVP+WzenTAkg12k86YqlwJVAB/VwdMlyXxvi +bCT1RVRfnYxscs14LLfcMWF3zMucw16mLlJCBSLvbZ0jn4h+/8vK5WuAdjw2YzLs +WtBcjWn3lV6tb4RJz5gtD/o1w8VWxwAnAVIWZntKAWmkcChCRgdUeWso76+plxE5 +aRYBJqdT1mctGqNEISd/WYPMgwnWXQsVi3x4z1dYu2tD9uO1dkAff12z1kyZQIBQ +rexKYRRRh9IKAayD4kgS0wdlULjBU98aeEaMz1ckuB46DX3lAYqmmTEL/Rl9cOI0 +Enpn/oOOfYFa5h0AFndZd1blMvruXfdAobjVABEBAAG0JUNhbWxpIFRlc3RlciA8 +Y2FtbGktdGVzdEBleGFtcGxlLmNvbT6JATgEEwECACIFAkzgoVsCGwMGCwkIBwMC +BhUIAgkKCwQWAgMBAh4BAheAAAoJECkxpnwm9avaHE0IAJ/pMZgiURl3kefrFMAV +7ei0XDfTekZOwDRcZWTVQ/A97phpzO8t78qLYbFeHuq3myNhrlVO9Gyp+2V904rN +dudoHLhpegf5TNeHGmAGHBxcooMPMp0JyIDnUBxtCNGxgWfbKpEDRsQAjkCc7sR0 +H+OegzlEf6JZGzEhV5ohOioTsC1DmJNoQsRz5Kes7sLoAzpQCbCv4yv+1o+mnzgW +9qPJXKxcScc0t2YTvcvpJ7LV8no1OP6vpYqB1A9Pzze6XFBlcXOUKbRKk0fEIV/u +pU3ph1fF7wlyRgA4A3iPwDC4BgVmHYkz9nYPn+7IcT/dDig5SWU+n7WZgGeyv75y +0Ue5AQ0ETOChWwEIALuHxKI+oSH+eeMSXhxcSUXnhp4cUeyvOV7oNPYcmsDclF0Y +7y8NrSPiEZod9vSTEDMq7hd3BG+feCBqjgR4qtmoXguJhWcnJqDBk5iAMuuAph9O +CC8QLACMJPhoxQ0UtDPKlpG4X8kLK1woHd716ulPl2KLjTgd6K4kCGj+CV5Ekn6u +IJj+3IPbYDOwk1l06ksimwQAY4dA1CXOTviH1bVqR6CzuzVPg4hcryWDva1rEO5c +LcOR8Wk/thANFLSNjqX8UgtGXhFZRWxKetFDQiX5f2BKoqTVYvD3pqt+zzyLNFAz +xhMc3cyFfqM8yQdzdEey/DIWtMoDqZCSVMJ63N8AEQEAAYkBHwQYAQIACQUCTOCh +WwIbDAAKCRApMaZ8JvWr2mHACACkco+fAfRK+gmprF2m8E0Bp1frwFH0g4RJVHXQ +BUDbg7OZbWumzD4Br28si6XDVMP6fLOeyD0EHYb6LhAHDkBLqx6e3kKG1mQ8fMIV +O4YMQfskYH2FJqlCtgMnM8N3oslPBTpZedNPSUq7HJh2pKr9GIDi1V+Hgc/qEigE +dj9f2zSSaKZdC4eL73GvlQOh+4XqgaMnMiKfI+/2WlRaJs1KOgKmIp5yHt0qY0ef +y+40BY/z9pMjyUvr/Wwp8KXArw0NAwzp8NUl5fNxRg9XWQWLn6hW8ydR20X3t2ym +iNSWzNQiTT6k7fumOABCoSZsow/AJxQSxqKOJBjgpKjIKCgY +=ru0J +-----END PGP PUBLIC KEY BLOCK----- diff --git a/vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-after.camli b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-after.camli new file mode 100644 index 00000000..49244c28 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-after.camli @@ -0,0 +1,4 @@ +{"camliVersion": 1, + "camliSigner": "sha1-8616ebc5143efe038528c2ab8fa6582353805a7a", + "foo": "bar" +,"camliSig":"iQEcBAABAgAGBQJO3/DNAAoJECkxpnwm9avaf6EH/3HVJC+6ybOJDTJIInQBum9YFzC1I8b6xNLN0yFdDtypZUotvW9pvU2pVpbfNSmcW/OL02eR2kgL55dHxbUjbN9CvXlvSb2QAy8IQMdA3721pMR41rNNn08w5bbAWgW/suiyN5z0pIKn3vPEHbguGeNQBStgOSq1WkgCozNBxPA7V5mcUx2rUOsWHYSmEY8foPdeDYcrw2pvxPN8kXk6zBrZilrtaY+Yx5zPLkq8trhHPgCdf4chL+Y2kmxXMKYjU+bkmJaNycUURdncZakTEv9YfbBp04kbHIaN6DttEoXuU96nTyuCFhIftmV+GPbvGpl3e2yhmae5hUUt1g0o8FE==aSCK"} diff --git a/vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-before-J.camli b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-before-J.camli new file mode 100644 index 00000000..0da13522 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-before-J.camli @@ -0,0 +1,4 @@ +{"camliVersion": 1, + "camliSigner": "sha1-8616ebc5143efe038528c2ab8fa6582353805a7a", + "foo": "bar" +} diff --git a/vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-before.camli b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-before.camli new file mode 100644 index 00000000..426b2e94 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-before.camli @@ -0,0 +1,3 @@ +{"camliVersion": 1, + "camliSigner": "sha1-8616ebc5143efe038528c2ab8fa6582353805a7a", + "foo": "bar" diff --git a/vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-before.camli.detachsig b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-before.camli.detachsig new file mode 100644 index 00000000..125626e7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/signing-before.camli.detachsig @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.11 (GNU/Linux) + +iQEcBAABAgAGBQJO3/DNAAoJECkxpnwm9avaf6EH/3HVJC+6ybOJDTJIInQBum9Y +FzC1I8b6xNLN0yFdDtypZUotvW9pvU2pVpbfNSmcW/OL02eR2kgL55dHxbUjbN9C +vXlvSb2QAy8IQMdA3721pMR41rNNn08w5bbAWgW/suiyN5z0pIKn3vPEHbguGeNQ +BStgOSq1WkgCozNBxPA7V5mcUx2rUOsWHYSmEY8foPdeDYcrw2pvxPN8kXk6zBrZ +ilrtaY+Yx5zPLkq8trhHPgCdf4chL+Y2kmxXMKYjU+bkmJaNycUURdncZakTEv9Y +fbBp04kbHIaN6DttEoXuU96nTyuCFhIftmV+GPbvGpl3e2yhmae5hUUt1g0o8FE= +=aSCK +-----END PGP SIGNATURE----- diff --git a/vendor/github.com/camlistore/camlistore/doc/json-signing/example/some-notes.txt b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/some-notes.txt new file mode 100644 index 00000000..098acb4b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/some-notes.txt @@ -0,0 +1,8 @@ +This is the example notes file. + +TODO: +[X] find good unique name for this project +[ ] write docs about it +[ ] implement + + diff --git a/vendor/github.com/camlistore/camlistore/doc/json-signing/example/some-notes.txt.camli b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/some-notes.txt.camli new file mode 100644 index 00000000..a275c8da --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/some-notes.txt.camli @@ -0,0 +1,9 @@ +{"camliVersion":"1", + "camliType": "file:1", + "name": "some-notes.txt", + "contents": "sha1-8ba9e53cbc83c1be3835b94a3690c3b03de0b522", + "size": 122, + "modtime": "2010-06-10T18:02Z", + "ctime": "2008-04-12T04:12:17.194Z", + "permissions": "0644" +} diff --git a/vendor/github.com/camlistore/camlistore/doc/json-signing/example/test-keyring.gpg b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/test-keyring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..3d20ba6837e26057e38b773a00e79af92f52acb6 GIT binary patch literal 1196 zcmV;d1XKH&0SyF9;GtUq2mrt5ri3TZO(11`6;u9OH>XSqAlGR;>58Sml|TpeaCFIX zUmN0VB=tp9U!9C>a?N-wx7;ydcg)M2!(OH?QbGkH?`@qUpNM|{%gW_zfOb4KV>0Yo z&|Hma_my6)Z-hzDm@N76B%w zQ`ToK8lyxZCx2Ok%!3KmT?-Y9e0a}SSi5UO_T#m7Kp%fyv(`+RK!8xK>`Gx2QHRnB z0jz`KND|WrWl*@mQ{NhRMvTu_B)A?r4SnSSil&({3;h{=aN;x)dT0KFj(vez<{bbQ zcUgB<`0stZf0!-kcTLK#c1`7!Y2Ll2I6$k#M1P`L8!;hQnjtzW6R<5qn3HHi#B=1QtnR|- z13FL%u&?7Q{??DCpEwrwqsd&XTuH|?w`LQ)%jqYw)$)2ZIR3Atihpcb`PKK$Z#^ znSf`qzrJ$ON3a3_1GxbW1We$eTLB0FyNAT0KA|E0dE*jZ99&67=Z2mfQS7fdUg$LT z9Gbw~lwBC_FAc3D;t`r1_VkkwGb-*EcLZ;rcpz$y1bC|1s9p<+g=Z%wpuv-vfHLcV zrXNlSFAyvMj3oGI#SIj+Gs>2cxL?T&D_kfY-u3F~PnTkgjW`|Xt|SO({s~@0l76lr znEu>@+h8-WlUa1?OCp;D0Aq(h)FsYN_=nZCYDb{6yERXPh+MBFgT1Y55bj(p!;$f6 zKeiAJ6ts?EN3#4f7PQI(sgROX!g}1_01*KI0f_-01Q-DV00{*GOyHqg0vikf3JDM?F{XSb z^{d)pzz6`Oa*v+@^h){(sjOY5@J#`ySL?u0^n-*+RCUk=K-+_}nQd#P%sv6HZ!C+Y z!&Jlie6ya&Jp>(w`YsR$4nRw*9-iJphSp>}e8Lqwh73XbBw&4oCaFTU11B@XccRHp z1v*)I(@#lCyBwHyq^kWGfa29(hk?)P5-0?AKVRE4l4zz~3x|vEaj%sFq5FmEfukof zBA+Ag_F7b0Ce2DZ0;VFKavt3(V@IFM?lc9D^Y)V?$xG|~Y$@=iz^@Gr0}Sc#)g|Tg zaYheUSp|!qs8;hQQQJlLw``_})RxTDB27M|?fa%U079WAY@-jrClnIKqK+gO;H0R? KC@2`P0ssRNTnzyL literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/doc/json-signing/example/test-secring.gpg b/vendor/github.com/camlistore/camlistore/doc/json-signing/example/test-secring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..bca3ad0392b59249597bc1ec8a93a5d9f6141758 GIT binary patch literal 2498 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 MsK_WN7_b5W05^l1$p8QV literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/doc/json-signing/json-signing.txt b/vendor/github.com/camlistore/camlistore/doc/json-signing/json-signing.txt new file mode 100644 index 00000000..88b1bb4f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/json-signing/json-signing.txt @@ -0,0 +1,166 @@ +JSON claim objects need to be signed. If I want to distribute a Camli +blob object publicly, declaring that I "favorite" or "star" a named +entity, it should be verifiable. + +The properties we want in the JSON file, ideally, include: + +GOAL #1) it's still a valid JSON file in its entirety. + +This means no non-JSON compliant header or footer. + +This implies that the data structure to be signed and the signature +metadata be separate, in an outer JSON wrapper. + +This has been discussed and implemented in various ways. For example, +in jchris's canonical-json project, + + http://github.com/jchris/canonical-json + +... the "signed-content" and the "signature" are parallel objects under the +same outer JSON object. + +The problem then becomes that the verifier, after parsing the JSON +blob, needs to re-serialize the JSON "signed-content" object, +byte-for-byte as in the original, in order to verify the signature. + +In jchris' strategy, the canonicalization is implemented by +referencing JavaScript code that serializes it. This has the +advantage that the serialization could change over time, but the +disadvantage that you have to embed a Rhino, V8, SpiderMonkey, or +similar into your parser, which is somewhat heavy. Considering that +canonical JSON serialization is something that should be relatively +static and could be defined once, I'm not sure that the flexibility is +worth the cost. + +Overall, though, the jchris approach's structure of the JSON file is +good. + +Notably, it satisfies on of my other goals: + +GOAL #2) The document still be human-readable. + +For instance, the laptop.org project is proposing this Canonical JSON +format: + + http://wiki.laptop.org/go/Canonical_JSON + +.. unfortunately, all whitespace is stripped. It's not a deal +breaker, but lacks human readableness. + +You might say, "Bring your own serialization! Wrap the signed-content +in a string!" + +But then you're back to the readable problem, because JSON strings +can't have embedded newline literals. + +Further, the laptop.org proposal requires the use of a new JSON +serialization library and parser for each language which wants to +produce camli documents. This isn't a huge deal, but considering that +JSON libraries already exist and people are oddly passionate about +their favorites and inertia's not to be ignored, I state the next +goal: + +GOAL #3) Don't require a new JSON library for parsing/serialization. + +With the above goals in mind, Camli uses the following scheme to sign +and verify JSON documents: + +SIGNING +======= + +-- Start with a JSON object (not an array) to be encoded and signed. + We'll call this data structure 'O'. While this signing technique + could be used for applications other than Camlistore, this document + is specifically about Camlistore, which requires that the JSON + object 'O' contain the following two key/value pairs: + "camliVersion": "1" + "camliSigner": "hashalg-xxxxxxxxxxx" (blobref of ASCII-armored public key) + +-- To find your camliSigner value, you could use GPG like: + + $ gpg --no-default-keyring --keyring=example/test-keyring.gpg --secret-keyring=example/test-secring.gpg \ + --export --armor 26F5ABDA > example/public-key.txt + + $ sha1sum example/public-key.txt + 8616ebc5143efe038528c2ab8fa6582353805a7a + + ... so the blobref value for camliSigner is "sha1-8616ebc5143efe038528c2ab8fa6582353805a7a". + Clients will use this value in the future to find the public key to verify + signtures. + +-- Serialize in-memory JSON object 'O' with whatever JSON + serialization library you have available. internal or trailing + whitespace doesn't matter. We'll call the JSON serialization of + 'O' (defined in earlier step) 'J' + (e.g. doc/example/signing-before-J.camli) + +-- Now remove any trailing whitespace and exactly and only one '}' + character from the end of string 'J'. We'll call this truncated, + trimmed string 'T'. + (e.g. doc/example/signing-before.camli) + +-- Create an ASCII-armored detached signature of this document, + e.g.: + + gpg --detach-sign --local-user=54F8A914 --armor \ + -o signing-before.camli.detachsig signing-before.camli + + (The output file is in doc/example/signing-before.camli.detachsig) + +-- Take just the base64 part of that ASCII detached signature + into a single line, and call that 'S'. + +-- Append the following to 'T' above: + + ,"camliSig":""}\n + + ... where is the single-line ASCII base64 detached signature. + Note that there are exactly 13 bytes before and exactly + 3 bytes after . Those must match exactly. + +-- The resulting string is 'C', the camli-signed JSON document. + + (The output file is in doc/example/signing-after.camli) + +In review: + +O == the object to be signed +J == any valid JSON serialization of O +T == J, with 0+ trailing whitespace removed, and then 1 '}' character + removed +S == ascii-armored detached signature of T +C == CONCAT(T, ',"camliSig":"', S, '"}', '\n') + +(strictly, the trailing newline and the exact JSON serialziation of +the camlisig element doesn't matter, but it'd be advised to follow +this recommendation for compatibility with other verification code) + +VERIFYING +========= + +-- start with a byte array representing the JSON to be verified. + call this 'BA' ("bytes all") + +-- given the byte array, find the last index in 'BA' of the 13 byte + substring: + ,"camliSig":" + + Let's call the bytes before that 'BP' ("bytes payload") and the bytes + starting at that substring 'BS' ("bytes signature") + +-- define 'BPJ' ("bytes payload JSON") as 'BP' + the single byte '}'. + +-- parse 'BPJ', verifying that it's valid JSON object (dictionary). + verify that the object has a 'camliSigner' key with a string key + that's a valid blobref (e.g. "sha1-xxxxxxx") note the camliSigner. + +-- replace the first byte of 'BS' (the ',') with an open brace ('{') + and parse it as JSON. verify that it's a valid JSON object with + exactly one key: "camliSig" + +-- using 'camliSigner', a camli blobref, find the blob (cached, via + camli/web lookup, etc) that represents a GPG public key. + +-- use GnuPG or equivalent libraries to verify that the ASCII-armored + GPG signature in "camliSig" signs the bytes in 'BP' using the + GPG public key found via the 'camliSigner' blobref diff --git a/vendor/github.com/camlistore/camlistore/doc/overview.txt b/vendor/github.com/camlistore/camlistore/doc/overview.txt new file mode 100644 index 00000000..0b6915a1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/overview.txt @@ -0,0 +1,161 @@ +============================================================================ +Camlistore: Content-Addressable Multi-Layer, Indexed Store +============================================================================ + +This file contains old design notes. They're correct in spirit, but shouldn't +be considered authorative. + +See http://camlistore.org/docs/ + + +-=-=-=-=-=-=-=-=-=-=-=-=-=- +Design goals: +-=-=-=-=-=-=-=-=-=-=-=-=-=- + +* Content storage & indexing & backup system +* No master node +* Anything can sync any which way, in any directed graph (cycles or not) + (phone -> personal server <-> home machine <-> amazon <-> google, etc) +* No sync state or races on arguments of latest versions +* Future-proof +* Very obvious/intuitive schema (easy to recover in the future, even + if all docs/notes about Camlistore are lost, or the recoverer in + five decades after I die doesn't even know that Camlistore was being + used....) should be easy for future digital archaeologists to grok. + +-=-=-=-=-=-=-=-=-=-=-=-=-=- +Design assumptions: +-=-=-=-=-=-=-=-=-=-=-=-=-=- + +* disk is cheap and getting cheaper +* bandwidth is high and getting faster +* plentiful CPU & compression will fix size & redundancy of metadata + +-=-=-=-=-=-=-=-=-=-=-=-=-=- +Layer 1: +-=-=-=-=-=-=-=-=-=-=-=-=-=- + +* content-addressable blobs only + - no notion of "files", filenames, dates, streams, encryption, + permissions, metadata. +* immutable +* only operations: + - store(digest, bytes) + - check(digest) => bool (have it or not) + - get(digest) => bytes + - list([start_digest]) => [(digest[, size]), ...]+ +* amenable to implementation on ordinary filesystems (e.g. ext3, vfat, + ntfs) or on Amazon S3, BigTable, AppEngine Datastore, Azure, Hadoop + HDFS, etc. + +-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=-=-=-=-=-=-=-=- +Schema of files/objects in Layer 1: +-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=-=-=-=-=-=-=-=- + +* Let's start by describing the storage of files that aren't self-describing, + e.g. "some-notes.txt" (as opposed to a jpg file from a camera that might + likely contain EXIF data, addressed later...). This file, for reference, + is in doc/json-signing/example/some-notes.txt + +* The bytes of file "some-notes.txt" are stored as-is in one blob, + addressed as "sha1-8ba9e53cbc83c1be3835b94a3690c3b03de0b522". + (note the explicit naming of the hash function as part of the name, + for upgradability later, and so all parties involved know how to + verify it...) + +* The filename, stat(2) metadata (modtime, ctime, permissions, etc) now + also need to be stored. The key design point here is that file + metdata is ALSO just a blob, content-addressed. The blob is a JSON + file (for human readability, compactness). XML and Protocol Buffers + were both also considered, but the former is too redundant, bloaty, + tree-ish (overkill) and out of vogue, while Protocol Buffers don't + stand up to the human readable future digital archaeologist test, + and they're also not self-describing with the proto schema declared + in-line. + + This file would thus be represented by a JSON file, as seen in + docs/json-signing/example/some-notes.txt.camli, and addressed as + "sha1-7e7960756b39cd7da614e7edbcf1fa7d696eb660", its sha1sum. This identifier + can be used in directory listings, etc. Note that camli files do not have any + magical filename, as they're not typically stored with their filename. (they + are in the doc/json-signing/examples/ directory just to separate them out, but + that's a rare case.) Instead, a camli JSON object is known as such if the + bytes of the file begin exactly with the bytes: + + {"camliVersion" + + ... which lets upper layers know what it is, and how to index it. + + See the doc/schema/ directory for details on Camli JSON objects and their + schema. + +* Note that camli files can represent: + + -- files + -- directories + -- trees/snapshots (git-style) + -- tags on other objects + -- stars/ratings on other objects + -- deletion claims/requests (since everything is immutable, you can + only request a deletion, and wait/hope for GC later...) + -- signed statements/claims on other objects + (think decentralized commenting/starring on the web, + verifying claims with webfinger lookups to find + public keys to verify signatures) + -- references to encrypted/split files + -- etc... (extensible over time) + +-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=-=-=-=-=-=-=-=- +Syncing +-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=-=-=-=-=-=-=-=- + +-- nodes can push/pull between storage layers without thought. No + chance of overwriting stuff. + +-- the assumption is that users control and trust and secure all their + storage nodes: e.g. your phone, your home server, your internet + server, your Amazon S3 node, your App Engine appid / datastore + instance, etc. + +-- users configure which nodes push/pull to which other nodes, forming + their own sync topology. For instance, your phone may not need a + full copy of all content you've ever saved/produced... its primary + goal in life is probably to quickly push out any unique content it + produces (e.g. photos) to another machine for backup. And maybe + cache other recently-accessed content locally, but not worry about + it being destroyed when you drop and break your phone. + +-- no encryption is assumed at the Camli storage layer, though you may + run a Camli storage node on an encrypted filesystem or blockdevice. + +-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=-=-=-=-=-=-=-=- +Indexing Layer +-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=-=-=-=-=-=-=-=- + +* scans/mapreduces over all blobs, provides higher-level APIs to list + objects, list directories, see snapshots of trees at points in time, + traverse graphs of objects (reverse indexing e.g. tags/stars/claims + object<->object) + +* ... TODO: document + +-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=-=-=-=-=-=-=-=- +Mid layer +-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=-=-=-=-=-=-=-=- + +* It'll often be the case that a client (e.g. your phone) knows about + a file (e.g. a photo) and has its metadata, but doesn't have its raw + JPEG blob bytes, which might be several MB, and slow to transfer + over a wireless connection. Camli storage nodes may also declare + their support for helper APIs for when the client knows/assumes the + type of a given blob. + + In addition to the operations in layer 1 above, you could also assume + most Camli storage nodes would support any API such as: + + getThumbnail(blobName, [ ... sizeParams .. ]) -> JPEG thumbnail + + .. which would make mobile content browsers lives easier. + + +TODO: finish documenting diff --git a/vendor/github.com/camlistore/camlistore/doc/protocol/blob-enumerate-protocol.txt b/vendor/github.com/camlistore/camlistore/doc/protocol/blob-enumerate-protocol.txt new file mode 100644 index 00000000..3ed84723 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/protocol/blob-enumerate-protocol.txt @@ -0,0 +1,74 @@ +The /camli/enumerate-blobs endpoint enumerates all blobs that the +server knows about. + +They're returned in sorted order, sorted by (digest_type, +digest_value). That is, md5-acbd18db4cc2f85cedef654fccc4a4d8 sorts +before sha1-0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33 because "m" sorts +before "s", even though "0" sorts before "a". + +GET /camli/enumerate-blobs?after=&limit= HTTP/1.1 +Host: example.com + +URL GET parameters: + + after optional If provided, only blobs GREATER THAN this + value are returned. + + Can't be used in combination with 'maxwaitsec' + + limit optional Limit the number of returned blobrefs. The + server may have its own lower limit, however, + so be sure to pay attention to the presence + of a "continueAfter" key in the JSON response. + + maxwaitsec optional The client may send this, an integer max + number of seconds the client is willing to + wait for the arrival of blobs. If the + server supports long-polling (an optional + feature), then the server will return + immediately if any blobs or available, else + it will wait for this number of seconds. + It is an error to send this option with a non- + zero value along with the 'after' option. + The server's reply must include + "canLongPoll" set to true if the server + supports this feature. Even if the server + supports long polling, the server may cap + 'maxwaitsec' and wait for less time than + requested by the client. + + Can't be used in combination with 'after'. + + +Response: + +HTTP/1.1 200 OK +Content-Type: text/javascript + +{ + "blobs": [ + {"blobRef": "md5-acbd18db4cc2f85cedef654fccc4a4d8", + "size": 3}, + {"blobRef": "sha1-0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33", + "size": 3}, + ], + "continueAfter": "sha1-0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33", + "canLongPoll": true, +} + +Response keys: + + blobs required Array of {"blobRef": BLOBREF, "size": INT_bytes} + will be an empty list if no blobs are present. + + continueAfter optional If present, the result is truncated and there are + are (likely) more blobs after the provided blobref, + which should be passed to the next request's + "after" request parameter. It's possible but rare + that the final page of actual results has + continueAfter set, but the subsequent page is + empty. (if numBlobs % limit == 0) + + canLongPoll optional Set to true (type boolean) if the server supports + long polling. If not true, the server ignores + the client's "maxwaitsec" parameter. diff --git a/vendor/github.com/camlistore/camlistore/doc/protocol/blob-get-protocol.txt b/vendor/github.com/camlistore/camlistore/doc/protocol/blob-get-protocol.txt new file mode 100644 index 00000000..c75b01f9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/protocol/blob-get-protocol.txt @@ -0,0 +1,46 @@ +The /camli/ endpoint returns a blob the server knows about. + +A request with the GET verb will return 200 and the blob contents if +present, 404 if not. A request with the HEAD verb will return 200 and +the blob meta data (i.e., content-length), or 404 if the blob is not +present. + +The response must include an explicit Content-Length, even with HTTP/1.1. +(The one piece of metadata a blobserver keeps on a blob is its length, + which is used in both enumerate-blobs bodies and responses to blob GETs.) + +Get the blob: + +GET /camli/sha1-126249fd8c18cbb5312a5705746a2af87fba9538 HTTP/1.1 +Host: example.com + +Response: + +HTTP/1.1 200 OK +Content-Type: application/octet-stream +Content-Length: + + + + +Existence check: + +HEAD /camli/sha1-126249fd8c18cbb5312a5705746a2af87fba9538 HTTP/1.1 +Host: example.com + +Response: + +HTTP/1.1 200 OK +Content-Type: application/octet-stream +Content-Length: + + +Does not exist: + +GET /camli/sha1-126249fd8c18cbb5312a5705746a2af87fba9538 HTTP/1.1 +Host: example.com + +Response: + +HTTP/1.1 404 Not Found + diff --git a/vendor/github.com/camlistore/camlistore/doc/protocol/blob-stat-protocol.txt b/vendor/github.com/camlistore/camlistore/doc/protocol/blob-stat-protocol.txt new file mode 100644 index 00000000..ad3dc76b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/protocol/blob-stat-protocol.txt @@ -0,0 +1,83 @@ +This document describes the "batch stat" API end-point, for checking +the size/existence of multiple blobs when the client and/or server do +not support SPDY or HTTP/2.0. See blob-upload-protocol.txt for more +background. + +Notably: the HTTP method may be GET or POST. GET is more correct but +be aware that HTTP servers and proxies start to suck around the 2k and +4k URL lengths. If you're stat'ing more than ~37 blobs, using POST +would be safest. + +The HTTP request path is $blobRoot/camli/stat. See +blob-upload-protocol.txt and discovery.txt for background. + +In either case, the request form values: (either in the URL for GET or +application/x-www-form-urlencoded body for POST) + + camliversion required Version of camlistore and/or stat protocol; + reserved for future use. Must be "1" for now. + + blob optional/ Must start at 1 and go up, no gaps allowed, not + repeated zero-padded, etc. Value is a blobref, e.g + "sha1-9b03f7aca1ac60d40b5e570c34f79a3e07c918e8" + There's no defined limit on how many you include here, + but servers may return a 400 Bad Request if you ask + for too many. All servers should support <= 1000 + though. + + maxwaitsec optional The client may send this, an integer max number + of seconds the client is willing to wait + for the arrival of blobs. If the server + supports long-polling (an optional + feature), then the server will return + immediately if all the requested blobs + or available, or wait up until this + amount of time for the blobs to become + available. The server's reply must + include "canLongPoll" set to true if the + server supports this feature. Even if + the server supports long polling, the + server may cap 'maxwaitsec' and wait for + less time than requested by the client. + +Examples: + +GET /some-blob-root/camli/stat?camliversion=1&blob1=sha1-9b03f7aca1ac60d40b5e570c34f79a3e07c918e8 HTTP/1.1 +Host: example.com + + -or- + +POST /some-blob-root/camli/stat HTTP/1.1 +Content-Type: application/x-www-form-urlencoded +Host: example.com + +camliversion=1& +blob1=sha1-9b03f7aca1ac60d40b5e570c34f79a3e07c918e8& +blob2=sha1-abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd& +blob3=sha1-deadbeefdeadbeefdeadbeefdeadbeefdeadbeef + +-------------------------------------------------- +Response: +-------------------------------------------------- + +HTTP/1.1 200 OK +Content-Length: ... +Content-Type: text/javascript + +{ + "stat": [ + {"blobRef": "sha1-abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd", + "size": 12312} + ], + "canLongPoll": true +} + +Response keys: + + stat required Array of {"blobRef": BLOBREF, "size": INT_bytes} + for blobs that the system already has. Empty + list if no blobs are already present. + + canLongPoll optional Set to true (type boolean) if the server supports + long polling. If not true, the server ignores + the client's "maxwaitsec" parameter. diff --git a/vendor/github.com/camlistore/camlistore/doc/protocol/blob-upload-protocol.txt b/vendor/github.com/camlistore/camlistore/doc/protocol/blob-upload-protocol.txt new file mode 100644 index 00000000..8af1b65b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/protocol/blob-upload-protocol.txt @@ -0,0 +1,122 @@ +Uploading a single blob is done in two parts: + +1) Optional: see if the server already has it with a HEAD request to + its blob URL, or do a multi-stat request with a single blob. If the + server has it, you're done. Blobs are content-addressable and have + no metadata or version, so if the server has it, it has the right + version. + +2) PUT that blob to its blob URL (or do a multipart batch put of a + single blob) + + +When uploading multiple blobs (the common case), the fastest option +depends on whether or not you're using a modern HTTP transport +(e.g. SPDY). If your client and server don't support SPDY, you want +to use the batch stat and batch upload endpoints, which hopefully can +die when the future finishes arriving. + +If you have SPDY, uploading 100 blobs is just like uploading 100 +single blobs, but all at once. Send all your 100 HEAD requests at +once, wait 1 RTT for all 100 replies, and then send then the <= 100 +PUT requests with the blobs that the server didn't have. + +If you DON'T have SPDY on both sides, you want to use the batch stat +and batch upload endpoints, described below. + +============================================================================ +Preupload request: +============================================================================ + +(see blob-stat-protocol.txt) + +============================================================================ +Batch upload request: +============================================================================ + +Things to note about the request: + + * You do a POST to $BLOB_ROOT/camli/upload where $BLOB_ROOT is the + blobserver's root path. You can get the path from + performing "discovery" on the server and getting the + "blobRoot" value. See discovery.txt. + + * You MUST provide a "name" parameter in each multipart part's + Content-Disposition value. The part's name matters and is the + blobref ("digest-hexhexhexhex") of your blob. The bytes MUST + match the blobref and the server MUST reject it if they don't + match. + + * You (currently) MUST provide a Content-Type for each multipart + part. It doesn't matter what it is (it's thrown away), but it's + necessary to satisfy various HTTP libraries. Easiest is to just + set it to "application/octet-stream" Server implementions SHOULD + fail if you clients forget it, to encourage clients to remember + it for compatibility with all blob servers. + + * You (currently) MUST provide a "filename" parameter in each + multipart's Content-Disposition value, unique per blob, but it + will also be thrown away and exists purely to satisfy various + HTTP libraries (mostly App Engine). It's recommended to either + set this to an increasing number (e.g. "blob1", "blob2") or just + repeat the blobref value here. + + * The total size of a batch upload HTTP request, including headers + and body (including MIME bits) should not exceed 32 MB. (A + single blob can be at most 16 MB, but will in practice be much + smaller: claims will be at most ~1 KB, and file chunks are + typically at most 64 KB or 256 KB) + +Some of these requirements may be relaxed in the future. + +Example: + +POST /$BLOB_SERVER_PATH_ROOT/camli/upload HTTP/1.1 +Host: upload-server.example.com +Content-Type: multipart/form-data; boundary=randomboundaryXYZ + +--randomboundaryXYZ +Content-Disposition: form-data; name="sha1-9b03f7aca1ac60d40b5e570c34f79a3e07c918e8"; filename="blob1" +Content-Type: application/octet-stream + +(binary or text blob data) +--randomboundaryXYZ +Content-Disposition: form-data; name="sha1-deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; filename="blob2" +Content-Type: application/octet-stream + +(binary or text blob data) +--randomboundaryXYZ-- + +----------------------------------------------------- +Response (status may be a 200 or a 303 to this data) +----------------------------------------------------- + +HTTP/1.1 200 OK +Content-Type: text/plain + +{ + "received": [ + {"blobRef": "sha1-9b03f7aca1ac60d40b5e570c34f79a3e07c918e8", + "size": 12312}, + {"blobRef": "sha1-deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "size": 29384933} + ] +} + +Response keys: + + received required Array of {"blobRef": BLOBREF, "size": INT_bytes} + for blobs that were successfully saved. Empty + list in the case nothing was received. + errorText optional String error message for protocol errors + not relating to a particular blob. + Mostly for debugging clients. + +If connection drops during a POST to an upload URL, you should re-do a +stat request to verify which objects were received by the server +and which were not. Also, the URL you received from stat before +might no longer work, so stat is required to a get a valid upload +URL. + +For information on resuming truncated uploads, read blob-upload-resume.txt + diff --git a/vendor/github.com/camlistore/camlistore/doc/protocol/blob-upload-resume.txt b/vendor/github.com/camlistore/camlistore/doc/protocol/blob-upload-resume.txt new file mode 100644 index 00000000..034fcf03 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/protocol/blob-upload-resume.txt @@ -0,0 +1,36 @@ +Optional upload protocol extension: + +Blobs can be large, devices (e.g. mobile phones) can have slow +uploads, or both. Thus, it's nice to have an upload resume mechanism. + +In a stat response, a server can return a JSON key +"alreadyHavePartially" with similar format to the spec-required "stat" +array. Instead of just "blobRef" and "size", though, there's a +continuation key and blobref of the part that server already has: + +... + "alreadyHavePartially": [ + {"blobRef": "sha1-abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd", + "size": 12312, + "partBlobRef": "sha1-beefbeefbeefbeefbeefbeefbeefbeefbeefbeef" + "resumeKey": "resume-sha1-abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd-12312-server-chosen", + } + ], +... + +If the client also supports this optional extension and parses the +"alreadyHavePartially" section, the client may resume their upload by: + + 1) verifying that digest of the client blob from byte 0 (incl) to + "size" (exclusive) matches the server's provided "partBlobRef". + (the server must use the same digest function). if it doesn't, + skip, and/or proceed to any other "alreadyHavePartially" + blobref with the same "blobRef" value. (the server may have + multiple partial uploads in different states, and perhaps one + is corrupt for various HTTP client failure reasons...) + + 2) do an upload like normal, but the name of the + multipart/form-data body part should be whatever the server + provided in the mandatory "resumeKey" value. skip the first + "size" bytes in your upload. + diff --git a/vendor/github.com/camlistore/camlistore/doc/protocol/discovery.txt b/vendor/github.com/camlistore/camlistore/doc/protocol/discovery.txt new file mode 100644 index 00000000..446d0d81 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/protocol/discovery.txt @@ -0,0 +1,62 @@ +Discovery is the process of asking the server for its configuration. + +You send the discovery HTTP request to the URL the user has +configured. If the user hasn't specified a path, use "/". + +Then make a GET request to that URL with either Accept header set to +"text/x-camli-configuration" or the the URL query parameter +"camli.mode" set to "config": + + GET /some/user/?camli.mode=config HTTP/1.1 + Host: camlihost.example.com + +Or: + + GET / HTTP1.1 + Host: 127.0.0.1 + Accept: text/x-camli-configuration + +The response is a JSON document: + +{ + "blobHashFuncs": [ + "sha1" + ], + "blobRoot": "/bs-and-maybe-also-index/", + "directoryHelper": "/ui/tree/", + "downloadHelper": "/ui/download/", + "helpRoot": "/help/", + "importerRoot": "/importer/", + "jsonSignRoot": "/sighelper/", + "ownerName": "The User Name", + "publishRoots": {}, + "searchRoot": "/my-search/", + "signing": { + "publicKey": "/sighelper/camli/sha1-f72d9090b61b70ee6501cceacc9d81a0801d32f6", + "publicKeyBlobRef": "sha1-f72d9090b61b70ee6501cceacc9d81a0801d32f6", + "publicKeyId": "94DE83C46401800C", + "signHandler": "/sighelper/camli/sig/sign", + "verifyHandler": "/sighelper/camli/sig/verify" + }, + "statusRoot": "/status/", + "storageGeneration": "231ceff7a04a77cdf881b0422ea733334eee3b8f", + "storageInitTime": "2012-11-30T03:34:47Z", + "syncHandlers": [ + { + "from": "/bs/", + "to": "/index-mysql/", + "toIndex": true + }, + { + "from": "/bs/", + "to": "/sto-s3/", + "toIndex": false + } + ], + "thumbVersion": "2", + "uiRoot": "/ui/", + "uploadHelper": "/ui/?camli.mode=uploadhelper", + "wsAuthToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +} + +TODO: document these more diff --git a/vendor/github.com/camlistore/camlistore/doc/publishing/README b/vendor/github.com/camlistore/camlistore/doc/publishing/README new file mode 100644 index 00000000..444e3ae2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/publishing/README @@ -0,0 +1,16 @@ +Camlistore delegates publishing to the publisher server application, which uses Go html templates (http://golang.org/pkg/text/template/) to publish pages. + +Resources for publishing, such as go templates, javascript and css files should be placed in the application source directory - app/publisher/ - so they can be served directly when using the dev server or automatically embedded in production. + +You should then specify the Go template to be used through the configuration file. The CSS files are automatically all available to the app. For example, there already is a go template (gallery.html), and css file (pics.css) that work together to provide publishing for image galleries. The dev server config (config/dev-server-config.json) already uses them. Here is how one would configure publishing for an image gallery in the server config ($HOME/.config/camlistore/server-config.json): + +"publish": { + "/pics/": { + "camliRoot": "mypics", + "cacheRoot": "/home/joe/var/camlistore/blobs/cache", + "goTemplate": "gallery.html" + } +} + +If you want to provide your own (Go) template, see http://camlistore.org/pkg/publish for the data structures and functions available to the template. + diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/blob-magic.txt b/vendor/github.com/camlistore/camlistore/doc/schema/blob-magic.txt new file mode 100644 index 00000000..10571c80 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/blob-magic.txt @@ -0,0 +1,26 @@ +-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +Camli Blob Magic +-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +[Note: not totally happy with this yet...] + +Ideal Camli JSON blobs should begin with the following 15 bytes: + + {"camliVersion" + +However, it's acknowledged that some JSON serialization libraries will +format things differently, so additional whitespace should be +tolerated. + +An ideal camli serializer will strive for the above header, though, by +doing something like: + + -- removing the "camliVersion" from the object, noting its value + (and requiring it to be present) + + -- serializing the JSON with an existing JSON serialization library, + + -- removing the serialized JSON's leading "{" character and prepending + the 15 byte header above, as well as the colon and saved version + and comma (which can have whitespace as desired) + diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/bytes.txt b/vendor/github.com/camlistore/camlistore/doc/schema/bytes.txt new file mode 100644 index 00000000..41afbc9c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/bytes.txt @@ -0,0 +1,38 @@ +Description of a series of bytes. + +A "bytes" is a metadata (JSON) blob to describe blobs. It's a recursive +definition that's able to describe a hash tree, describing very large +blobs (or "files"). + +A "bytes" blob can be used on its own, but is also used by things like +a "file" schema blob. + + +{"camliVersion": 1, + "camliType": "bytes", + + // Required. Array of contiguous regions of bytes. Zero or more elements. + // + // Each element must have: + // "size": the number of bytes that this element contributes to array of bytes. + // Required, and must be greater than zero. + // + // At most one of: + // "blobRef": where to get the raw bytes from. if this and "bytesRef" + // are missing, the bytes are all zero (e.g. a sparse file hole) + // "bytesRef": alternative to blobRef, where to get the range's bytes + // from, but pointing recursively at a "bytes" schema blob + // describing the range, recursively. large files are made of + // these in a hash tree. it is an error if both "bytesRef" + // and "blobRef" are specified. + // + // Optional: + // "offset": the number of bytes into blobRef or bytesRef to skip to + // get the necessary bytes for the range. usually zero (unspecified) + "parts": [ + {"blobRef": "digalg-blobref", "size": 1024}, + {"bytesRef": "digalg-blobref", "size": 5000000, "offset": 492 }, + {"size": 1000000}, + {"blobRef": "digalg-blobref", "size": 10}, + ] +} diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/claims/TODO b/vendor/github.com/camlistore/camlistore/doc/schema/claims/TODO new file mode 100644 index 00000000..85ce4932 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/claims/TODO @@ -0,0 +1,90 @@ +TODO: +----- +Clean this up and/or break into separate files. + +{"camliVersion": 1, + "camliType": "claim", + "camliSigner": "....", + "claimDate": "2010-07-10T17:20:03.9212Z", // redundant with data in ascii armored "camliSig", + // but required. more legible. takes precedence over + // any date inferred from camliSig + "permaNode": "dig-xxxxxxx", // what is being modified + "claimType": "set-attribute", + "attribute": "camliContent", + "value": "dig-yyyyyyy", + "camliSig": .........} + +claimTypes: +----------- +"add-attribute" (adds a value to a multi-valued attribute (e.g. "tag")) +"set-attribute" (set a single-valued attribute. equivalent to "del-attribute" of "attribute" and then add-attribute) +"del-attribute" (deletes all values of "attribute", if no "value" given, or just the provided "value" if multi-valued) + +"multi".. atomically do multiple add/set/del from above on potentially different permanodes. looks like: + + {"camliVersion": 1, + "camliType": "claim", + "claimType": "multi", + "claimDate": "2013-02-24T17:20:03.9212Z", + "claims": [ + {"claimType": "set-attribute", + "permanode": "dig-xxxxxx", + "attribute": "foo", + "value": "fooValue"}, + {"claimType": "add-attribute", + "permanode": "dig-yyyyy", + "attribute": "tag", + "value": "funny"} + ], + "camliSig": .........} + +Attribute names: +---------------- +camliContent: a permanode "becoming" something. value is pointer to what it is now. + + +Old notes from July 2010 doc: +----------------------------- +Claim types: +permanode-become: + -- implies either: + 1) switching from typeless/lifeless virgin pnode into something (dynamic set, filesystem tree, etc) + 2) changing versions of that base metadata (new filesystem snapshot) + -- ‘permaNode’ is the thing that is changing + -- ‘contents’ is the current node that represents what permaNode changes to +set-membership: add a blobref to a dynamic set + -- "permaNode" is blobref of the dynamic set +delete-claim: delete another claim (target is claim to delete) + -- "contents" is the claim blobref you’re deleting +{set,add}-attribute: + -- attach a piece of metadata to something. + -- use set-attribute for single-valued attributes only: highest dated claim wins (of trusted person) e.g. "title", "description" + -- use add-attribute for multi-valued things. e.g. "tag" + +Tagging something: +{"claimType": "add-attribute", // + "attribute": "tag", // utf-8, should have list of valid attributes names, preferrably not made up by us (open social spec?) + "value": "funny", // value that doesn’t have lasting value + "valueRef": "sha1-blobref", // hefty reference to a lasting value + + "claimer?": "sha1-of-the-dude-who’s-signing", + "claimDate": "2010-07-10T17:20:03.9212Z", + "claimType", "permanode-become", + "permaNode": "sha1-pnode", +} + +filesystem root claim: +{ + "camliVersion": 1, + "camliType": "claim", + + // Stuff for camliType "claim": + "claimDate": "2010-07-10T17:20:03.9212Z", // redundant with data in ascii armored "camliSig". TODO: resolve + "claimType", "permanode-become", + + // Stuff for "permanode-become": + "permaNode": "sha1-pnode", + "contents": "sha1-fs-node" + +,"camliSigner": "digalg-blobref-to-ascii-armor-public-key-of-signer", +"camliSig": "......"} diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/claims/attributes.txt b/vendor/github.com/camlistore/camlistore/doc/schema/claims/attributes.txt new file mode 100644 index 00000000..89d5ea4b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/claims/attributes.txt @@ -0,0 +1,80 @@ +Permanode Attributes + +While a permanode can have any arbitrary attributes and values (and +each value can be single-valued or multi-valued), the following are +the conventional attributes and values used by the various tools, +search, FUSE, and web UI. + +"tag": (multi-valued) + + a set of zero or more keywords (or phrases) indexed completely, for + searching by tag. No HTML. + +"title": (single-valued) + + A name given to the permanode. No HTML. + +"description": (single-valued) + + An account of the permanode. It may include but is not limited to: + an abstract, a table of contents, or a free-text account of the + resource. No HTML. As of 2013-12-28, not very defined yet. + +"camliContent": (single-valued) + + when a permanode is a file, the camliContent is set to the fileref + (the blobref of the "file" schema blob) + +"camliContentImage": (single-valued) + + when a permanode has camliContent set, but the camliContent is of + a non-image, the camliContentImage points to a "file" schema's + blobref of an image representation of the content. For instance, + this might be the cover art of an MP3 file or a thumbnail of a + spreadsheet. + +"camliMember": (multi-valued) + + when the permanode represents a set (unordered, unkeyed), the + parent permanode (the container set) has a camliMember set to the + permanode of each child element. + +"camliPath:$dirent_name" (single-valued) + + when the permanode represents an associative container, each keyed + child permanode blobref is pointed to by the "camliPath:$key" + attribute on the parent. This is used by the FUSE client, and + respected in the UI (browser, publishing code), etc. + +"camliNodeType" (single-valued) + + when the application needs to note the type of a permanode before + any other attributes (like those above) are added which would otherwise + imply its type, the camliNodeType lets applications be specific. + This should only be used if another attribute can't imply it. + Currently only used by FUSE to indicate the difference between a new + file and a new directory permanode. Known values include: + + * "directory". this permanode will have "camliPath:$key" + attributes later. + * "file". this permanode will have a "camliContent" later. for + now, it should be treated as if it's an empty, 0-byte file. + +"camliDefVis" (single-valued) + + Can be "hide". Experimental. Affects default visibility in web UI. + +"xattr:$attr_name" (single-valued) + + when a permanode represents a file or directory visible to FUSE, + "xattr:$x" is used to store the value for extended attribute "x". + Extended attribute data may contain any arbitrary bytes, so values + are base64 encoded. + +"camliRoot" (single-valued) + + TODO: doc + +"camliImportRoot" (single-valued) + + TODO: doc diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/claims/delete.txt b/vendor/github.com/camlistore/camlistore/doc/schema/claims/delete.txt new file mode 100644 index 00000000..6d44e99f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/claims/delete.txt @@ -0,0 +1,13 @@ +A claim can delete a permanode or another claim. +(Un)Deletions are not considered as modifications, so the claimDate of a delete claim +is never considered as a modtime in the context of time constrained searches. +----- + +{"camliVersion": 1, + "camliType": "claim", + "camliSigner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "claimDate": "2010-07-10T17:20:03.9212Z", + "claimType": "delete", + "target": "sha1-ab6dacb972eeee72df2a846aab7d751b5856a1a0", // the permanode or claim being deleted. + +} diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/claims/share.txt b/vendor/github.com/camlistore/camlistore/doc/schema/claims/share.txt new file mode 100644 index 00000000..99e41ee5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/claims/share.txt @@ -0,0 +1,31 @@ +A share claim makes blob(s) available to others. (that is, parties who are not +the owner of the Camlistore instance). +----- + +{"camliVersion": 1, + + // Type of authentication required to access the share. Currently only haveref + // is supported, which means that anyone with the claim blobref can access. + "authType": "haveref", + + "camliType": "claim", + "camliSigner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "claimDate": "2014-09-04T20:04:09.193945801Z", + "claimType": "share", + + // The blob or search to share. Exactly one of these must be present. It is an + // error to set neither or both. + "target": "sha1-543fbdfdbcb1297af8a4dc7d299c0cb90e2bea0f", + "search": , + + // If true, anything recursively reachable from target or search is also + // shared. Edges that are guaranteed to be followed for purposes of + // reachability are: + // - blobRef and bytesRef values of camliType="blob|file" + // - members of camliType="static-set" + // Currently reachability is implemented more loosely, but clients should not + // depend on that. + "transitive": false, + + +} diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/files/directory.txt b/vendor/github.com/camlistore/camlistore/doc/schema/files/directory.txt new file mode 100644 index 00000000..917bb7d7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/files/directory.txt @@ -0,0 +1,13 @@ +Directory schema + +{"camliVersion": 1, + "camliType": "directory", + + // + // INCLUDE ALL REQUIRED & ANY OPTIONAL FIELDS FROM file-common.txt + // + + // Required: + "entries": "digalg-blobref-to-static-set", +} + diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/files/fifo.txt b/vendor/github.com/camlistore/camlistore/doc/schema/files/fifo.txt new file mode 100644 index 00000000..160f4a84 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/files/fifo.txt @@ -0,0 +1,9 @@ +fifo schema + +{"camliVersion": 1, + "camliType": "fifo", + + // + // INCLUDE ALL REQUIRED & ANY OPTIONAL FIELDS FROM file-common.txt + // +} diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/files/file-common.txt b/vendor/github.com/camlistore/camlistore/doc/schema/files/file-common.txt new file mode 100644 index 00000000..7475ce07 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/files/file-common.txt @@ -0,0 +1,24 @@ +Fields common to files, directories, symlinks, FIFOs and sockets + +{"camliVersion": 1, + "camliType": "...", // one of "file", "directory", "symlink", "fifo", "socket" + + // At most one of these may be set. (zero may be present only for large files' subranges, + // represented as a tree of file schemas) But exactly one of these is required for + // top-level files, directories, symlinks, FIFOs, sockets, e.t.c. + "fileName": "if-it-is-utf8.txt", // only for utf-8 + "fileNameBytes": [65, 234, 234, 192, 23, 123], // if unknown charset (not recommended) + + // Optional: + "unixPermission": "0755", // no octal in JSON, so octal as string + "unixOwnerId": 1000, + "unixOwner": "bradfitz", + "unixGroupId": 500, + "unixGroup": "camliteam", + "unixXattrs": [....], // TBD + "unixMtime": "2010-07-10T17:14:51.5678Z", // UTC-- ISO 8601, as many significant digits as known + "unixCtime": "2010-07-10T17:20:03.9212Z", // UTC-- ISO 8601, best-effort to match unix meaning + + // Not recommended to include, but if you must: (atime is a bit silly) + "unixAtime": "2010-07-10T17:14:22.1234Z", // UTC-- ISO 8601 +} diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/files/file.txt b/vendor/github.com/camlistore/camlistore/doc/schema/files/file.txt new file mode 100644 index 00000000..c1389ba5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/files/file.txt @@ -0,0 +1,15 @@ +File schema + +{"camliVersion": 1, + "camliType": "file", + + // #include "file-common.txt" # metadata about the file + // #include "../bytes.txt" # describes the bytes of the file + + // Optional, if linkcount > 1, for representing hardlinks properly. + "inodeRef": "digalg-blobref", // to "inode" blobref, when the link count > 1 +} + +// TODO: Mac/NTFS-style resource forks? perhaps just a "streams" +// array of recursive file objects? + diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/files/inode.txt b/vendor/github.com/camlistore/camlistore/doc/schema/files/inode.txt new file mode 100644 index 00000000..26274cde --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/files/inode.txt @@ -0,0 +1,15 @@ +Inode schema. + +{"camliVersion": 1, + "camliType": "inode", + "inodeId": 12345 // st_ino + "deviceId": 53, // st_dev + "numLinks": 3, // st_nlink +} + +This is optional and probably rarely used, but lets two+ files be +represented as hardlinks with each other. If both files point to the +same inode object, they're hardlinks of each other. + +Note that unlink "directory", "file", and "schema", this does not +inherit fields from the "file-common" schema. diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/files/socket.txt b/vendor/github.com/camlistore/camlistore/doc/schema/files/socket.txt new file mode 100644 index 00000000..b13d3568 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/files/socket.txt @@ -0,0 +1,9 @@ +socket schema + +{"camliVersion": 1, + "camliType": "socket", + + // + // INCLUDE ALL REQUIRED & ANY OPTIONAL FIELDS FROM file-common.txt + // +} diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/files/symlink.txt b/vendor/github.com/camlistore/camlistore/doc/schema/files/symlink.txt new file mode 100644 index 00000000..3969f739 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/files/symlink.txt @@ -0,0 +1,18 @@ +Symlink schema + +{"camliVersion": 1, + "camliType": "symlink", + + // + // INCLUDE ALL REQUIRED & ANY OPTIONAL FIELDS FROM file-common.txt + // + + // Exactly one of: + + // If UTF-8: + "symlinkTarget": "../foo/blah", + + // If unknown charset & have raw 8-bit filenames and can't convert + // to UTF-8. The array is a mix of UTF-8 and/or non-UTF-8 bytes (0-255). + "symlinkTargetBytes": ["../foo/Am", 233, "lie.jpg"], // e.g. Amélie in ISO-8859-1 when charset unknown +} diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/objects/keep.txt b/vendor/github.com/camlistore/camlistore/doc/schema/objects/keep.txt new file mode 100644 index 00000000..65558538 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/objects/keep.txt @@ -0,0 +1,16 @@ +A signed "keep" edge for GC/indexing purposes. Expresses a user's +intent to keep an object. + +This is not the only way to keep an object alive for the purposes of +GC. Permanodes (signed by a user) are also part of that user's roots, +and anything they reference (including blobs via "become" claims on +those permanodes) + +This is just the most explicit way when you're not modeling the data +with permanodes. + +{"camliVersion": 1, + "camliType": "keep", + "target": "digalg-blobref-of-thing-to-keep", +} + diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/objects/permanode.txt b/vendor/github.com/camlistore/camlistore/doc/schema/objects/permanode.txt new file mode 100644 index 00000000..0bcfd863 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/objects/permanode.txt @@ -0,0 +1,15 @@ +The idea of a permanode is that it's the anchor from which you build +mutable objects. To serve as a reliable (consistently nameable) +object it must have no mutable state itself. + +{"camliVersion": 1, + "camliType": "permanode", + + // Required. Any random string, to force the sha1 of this + // node to be unique. Note that the date in the ASCII-armored + // GPG JSON signature will already help it be unique, so this + // doesn't need to be a great random. + "random": "615e05c68c8411df81a2001b639d041f" + +} + diff --git a/vendor/github.com/camlistore/camlistore/doc/schema/objects/static-set.txt b/vendor/github.com/camlistore/camlistore/doc/schema/objects/static-set.txt new file mode 100644 index 00000000..b6a06de0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/schema/objects/static-set.txt @@ -0,0 +1,23 @@ +Static set schema + +{"camliVersion": 1, + "camliType": "static-set", + + // Required. + // May be ordered to unordered, depending on context/needs. If unordered, + // it's recommended but not required to sort the blobrefs. + "members": [ + "digalg-blobref-item1", // maybe a file? + "digalg-blobref-item2", // maybe a directory? + "digalg-blobref-item3", // maybe a symlink? + "digalg-blobref-item4", // maybe a permanode? + "digalg-blobref-item5", // ... don't know until you fetch it + "digalg-blobref-item6", // ... and what's valid depends on context + "digalg-blobref-item7", // ... a permanode in a directory would + "digalg-blobref-item8" // ... be invalid, for instance. + ] +} + +Note: dynamic sets are structured differently, using a permanode and + membership claim nodes. The above is just for presenting a snapshot + of members. diff --git a/vendor/github.com/camlistore/camlistore/doc/search-ui.txt b/vendor/github.com/camlistore/camlistore/doc/search-ui.txt new file mode 100644 index 00000000..7a0308b6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/search-ui.txt @@ -0,0 +1,51 @@ +The User Interface's "Search" box accepts predicates of the form "[-]operator:value[:value]". +These predicates may be separated by 'and' or 'or' keywords, or spaces which mean the same +as 'and'. Expressions like this may be grouped with parenthesis. Grouped expressions are +evaluated first. Grouped expressions may be negated. +An 'and' besides an 'or' is evaluated first. This means for example that + +tag:foo or is:pano tag:bar + +will return all images having tag foo together with the panorama images having tag bar. + +Negation of a predicate is achieved by prepending a minus sign: -is:landscape will match +with pictures of not landscape ratio. + +For example + + -(after:"2010-01-01" before:"2010-03-02T12:33:44") or loc:"Amsterdam" + +will return all images having "modtime" outside the specified period, joined with +all images taken in Amsterdam. + +When you need to match a value containing a space, you need to use double quotes around +the value only. For example: tag:"Three word tagname" and not "tag:Three word tagname". +If your value contains double quotes you can use backslash escaping. +For example: attr:bar:"He said: \"Hi\"" + +Usable operators: + after: date format is RFC3339, but can be shortened as required. + before: i.e. 2011-01-01 is Jan 1 of year 2011 and "2011" means the same. + attr: match on attribute. Use attr:foo:bar to match nodes having their foo + attribute set to bar. + format: file's format (or MIME-type) such as jpg, pdf, tiff. + has:location image has a location (GPSLatitude and GPSLongitude can be + retrieved from the image's EXIF tags). + loc: uses the EXIF GPS fields to match images having a location near + the specified location. Locations are resolved using + maps.googleapis.com. For example: loc:"new york, new york" + is:image object is an image + is:landscape the image has a landscape aspect + is:pano the image's aspect ratio is over 2 - panorama picture. + is:portrait the image has a portrait aspect. + height: use height:min-max to match images having a height of at least min + and at most max. Use height:min- to specify only an underbound and + height:-max to specify only an upperbound. + Exact matches should use height:480 + tag: match on a tag + width: use width:min-max to match images having a width of at least min + and at most max. Use width:min- to specify only an underbound and + width:-max to specify only an upperbound. + Exact matches should use width:640 + childrenof: Find child permanodes of a parent permanode (or prefix + of a parent permanode): childrenof:sha1-527cf12 diff --git a/vendor/github.com/camlistore/camlistore/doc/terminology.txt b/vendor/github.com/camlistore/camlistore/doc/terminology.txt new file mode 100644 index 00000000..e5a0051c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/doc/terminology.txt @@ -0,0 +1,4 @@ +See: + http://camlistore.org/docs/terms +or + website/content/docs/terms diff --git a/vendor/github.com/camlistore/camlistore/internal/chanworker/chanworker.go b/vendor/github.com/camlistore/camlistore/internal/chanworker/chanworker.go new file mode 100644 index 00000000..c0da8cc5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/internal/chanworker/chanworker.go @@ -0,0 +1,120 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package chanworker + +import ( + "container/list" + "sync" +) + +type chanWorker struct { + c chan interface{} + + donec chan bool + workc chan interface{} + fn func(n interface{}, ok bool) + buf *list.List +} + +// TODO: make it configurable if need be. Although so far in camput it wasn't. +const buffered = 16 + +// NewWorker starts nWorkers goroutines running fn on incoming +// items sent on the returned channel. fn may block; writes to the +// channel will buffer. +// If nWorkers is negative, a new goroutine running fn is called for each +// item sent on the returned channel. +// When the returned channel is closed, fn is called with (nil, false) +// after all other calls to fn have completed. +// If nWorkers is zero, NewWorker will panic. +func NewWorker(nWorkers int, fn func(el interface{}, ok bool)) chan<- interface{} { + if nWorkers == 0 { + panic("NewChanWorker: invalid value of 0 for nWorkers") + } + retc := make(chan interface{}, buffered) + if nWorkers < 0 { + // Unbounded number of workers. + go func() { + var wg sync.WaitGroup + for w := range retc { + wg.Add(1) + go func(w interface{}) { + fn(w, true) + wg.Done() + }(w) + } + wg.Wait() + fn(nil, false) + }() + return retc + } + w := &chanWorker{ + c: retc, + workc: make(chan interface{}, buffered), + donec: make(chan bool), // when workers finish + fn: fn, + buf: list.New(), + } + go w.pump() + for i := 0; i < nWorkers; i++ { + go w.work() + } + go func() { + for i := 0; i < nWorkers; i++ { + <-w.donec + } + fn(nil, false) // final sentinel + }() + return retc +} + +func (w *chanWorker) pump() { + inc := w.c + for inc != nil || w.buf.Len() > 0 { + outc := w.workc + var frontNode interface{} + if e := w.buf.Front(); e != nil { + frontNode = e.Value + } else { + outc = nil + } + select { + case outc <- frontNode: + w.buf.Remove(w.buf.Front()) + case el, ok := <-inc: + if !ok { + inc = nil + continue + } + w.buf.PushBack(el) + } + } + close(w.workc) +} + +func (w *chanWorker) work() { + for { + select { + case n, ok := <-w.workc: + if !ok { + w.donec <- true + return + } + w.fn(n, true) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/lib/python/camli/__init__.py b/vendor/github.com/camlistore/camlistore/lib/python/camli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vendor/github.com/camlistore/camlistore/lib/python/camli/op.py b/vendor/github.com/camlistore/camlistore/lib/python/camli/op.py new file mode 100755 index 00000000..74026b88 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/camli/op.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python +# +# Camlistore uploader client for Python. +# +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Client library for Camlistore.""" + +__author__ = 'Brett Slatkin (bslatkin@gmail.com)' + +import base64 +import cStringIO +import hashlib +import httplib +import logging +import mimetools +import urllib +import urlparse + +import simplejson + +__all__ = ['Error', 'ServerError', 'PayloadError', 'BUFFER_SIZE', 'CamliOp'] + + +BUFFER_SIZE = 512 * 1024 + + +class Error(Exception): + """Base class for exceptions in this module.""" + + +class ServerError(Error): + """An unexpected error was returned by the server.""" + + +class PayloadError(ServerError): + """Something about a data payload was bad.""" + + +def buffered_sha1(data, buffer_size=BUFFER_SIZE): + """Calculates the sha1 hash of some data. + + Args: + data: A string of data to write or an open file-like object. File-like + objects will be seeked back to their original position before this + function returns. + buffer_size: How much data to munge at a time. + + Returns: + Hex sha1 string. + """ + compute = hashlib.sha1() + if isinstance(data, basestring): + compute.update(data) + else: + start = data.tell() + while True: + line = data.read(buffer_size) + if line == '': + break + compute.update(line) + data.seek(start) + return compute.hexdigest() + + +class CamliOp(object): + """Camlistore client class that is single threaded, using one socket.""" + + def __init__(self, + server_address, + buffer_size=BUFFER_SIZE, + create_connection=httplib.HTTPConnection, + auth=None, + basepath=""): + """Initializer. + + Args: + server_address: hostname:port for the server. + buffer_size: Byte size to use for in-memory buffering for various + client-related operations. + create_connection: Use for testing. + auth: Optional. 'username:password' to use for HTTP basic auth. + basepath: Optional path suffix. e.g. if the server is at + "localhost:3179/bs", the basepath should be "/bs". + """ + self.server_address = server_address + self.buffer_size = buffer_size + self._create_connection = create_connection + self._connection = None + self._authorization = '' + self.basepath = "" + if auth: + if len(auth.split(':')) != 2: + # Default to dummy username; current server doesn't care + # TODO(jrabbit): care when necessary + auth = "username:" + auth #If username not given use the implicit default, 'username' + self._authorization = ('Basic ' + base64.encodestring(auth).strip()) + if basepath: + if '/' not in basepath: + raise NameError("basepath must be in form '/bs'") + if basepath[-1] == '/': + basepath = basepath[:-1] + self.basepath = basepath + + def _setup_connection(self): + """Sets up the HTTP connection.""" + self.connection = self._create_connection(self.server_address) + + def put_blobs(self, blobs): + """Puts a set of blobs. + + Args: + blobs: List of (data, blobref) tuples; list of open files; or list of + blob data strings. + + Returns: + The set of blobs that were actually uploaded. If all blobs are already + present this set will be empty. + + Raises: + ServerError if the server response is bad. + PayloadError if the server response is not in the right format. + OSError or IOError if reading any blobs breaks. + """ + if isinstance(blobs, dict): + raise TypeError('Must pass iterable of tuples, open files, or strings.') + + blobref_dict = {} + for item in blobs: + if isinstance(item, tuple): + blob, blobref = item + else: + blob, blobref = item, None + if blobref is None: + blobref = 'sha1-' + buffered_sha1(blob, buffer_size=self.buffer_size) + blobref_dict[blobref] = blob + + preupload = {'camliversion': '1'} + for index, blobref in enumerate(blobref_dict.keys()): + preupload['blob%d' % (index+1)] = blobref + + # TODO: What is the max number of blobs that can be specified in a + # preupload request? The server probably has some reasonable limit and + # after that we need to do batching in smaller groups. + + self._setup_connection() + if self.basepath: + fullpath = self.basepath + '/camli/stat' + else: + fullpath = '/camli/stat' + self.connection.request( + 'POST', fullpath, urllib.urlencode(preupload), + {'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': self._authorization}) + response = self.connection.getresponse() + logging.debug('Preupload HTTP response: %d %s', + response.status, response.reason) + if response.status != 200: + raise ServerError('Bad preupload response status: %d %s' % + (response.status, response.reason)) + + data = response.read() + try: + response_dict = simplejson.loads(data) + except simplejson.decoder.JSONDecodeError: + raise PayloadError('Server returned bad preupload response: %r' % data) + + logging.debug('Parsed preupload response: %r', response_dict) + if 'stat' not in response_dict: + raise PayloadError( + 'Could not find "stat" in preupload response: %r' % + response_dict) + if 'uploadUrl' not in response_dict: + raise PayloadError( + 'Could not find "uploadUrl" in preupload response: %r' % + response_dict) + + already_have_blobrefs = set() + for blobref_json in response_dict['stat']: + if 'blobRef' not in blobref_json: + raise PayloadError( + 'Cannot find "blobRef" in preupload response: %r', + response_dict) + already_have_blobrefs.add(blobref_json['blobRef']) + logging.debug('Already have blobs: %r', already_have_blobrefs) + + missing_blobrefs = set(blobref_dict.iterkeys()) + missing_blobrefs.difference_update(already_have_blobrefs) + if not missing_blobrefs: + logging.debug('All blobs already present.') + return + + # TODO(bslatkin): Figure out the 'Content-Length' header value by looking + # at the size of the files by seeking; required for multipart POST. + out = cStringIO.StringIO() + boundary = mimetools.choose_boundary() + boundary_start = '--' + boundary + + blob_number = 0 + for blobref in blobref_dict.iterkeys(): + if blobref in already_have_blobrefs: + logging.debug('Already have blobref=%s', blobref) + continue + blob = blobref_dict[blobref] + blob_number += 1 + + out.write(boundary_start) + out.write('\r\nContent-Type: application/octet-stream\r\n') + out.write('Content-Disposition: form-data; name="%s"; ' + 'filename="%d"\r\n\r\n' % (blobref, blob_number)) + if isinstance(blob, basestring): + out.write(blob) + else: + while True: + buf = blob.read(self.buffer_size) + if buf == '': + break + out.write(buf) + out.write('\r\n') + out.write(boundary_start) + out.write('--\r\n') + request_body = out.getvalue() + + pieces = list(urlparse.urlparse(response_dict['uploadUrl'])) + # TODO: Support upload servers on another base URL. + pieces[0], pieces[1] = '', '' + relative_url = urlparse.urlunparse(pieces) + self.connection.request( + 'POST', relative_url, request_body, + {'Content-Type': 'multipart/form-data; boundary="%s"' % boundary, + 'Content-Length': str(len(request_body)), + 'Authorization': self._authorization}) + + response = self.connection.getresponse() + logging.debug('Upload response: %d %s', response.status, response.reason) + if response.status not in (200, 301, 302, 303): + raise ServerError('Bad upload response status: %d %s' % + (response.status, response.reason)) + + while response.status in (301, 302, 303): + # TODO(bslatkin): Support connections to servers on different addresses + # after redirects. For now just send another request to the same server. + location = response.getheader('Location') + pieces = list(urlparse.urlparse(location)) + pieces[0], pieces[1] = '', '' + new_relative_url = urlparse.urlunparse(pieces) + logging.debug('Redirect %s -> %s', relative_url, new_relative_url) + relative_url = new_relative_url + self.connection.request('GET', relative_url, headers={ + 'Authorization': self._authorization}) + response = self.connection.getresponse() + + if response.status != 200: + raise ServerError('Bad upload response status: %d %s' % + (response.status, response.reason)) + + data = response.read() + try: + response_dict = simplejson.loads(data) + except simplejson.decoder.JSONDecodeError: + raise PayloadError('Server returned bad upload response: %r' % data) + + if 'received' not in response_dict: + raise PayloadError('Could not find "received" in upload response: %r' % + response_dict) + + received_blobrefs = set() + for blobref_json in response_dict['received']: + if 'blobRef' not in blobref_json: + raise PayloadError( + 'Cannot find "blobRef" in upload response: %r', + response_dict) + received_blobrefs.add(blobref_json['blobRef']) + logging.debug('Received blobs: %r', received_blobrefs) + + missing_blobrefs.difference_update(received_blobrefs) + if missing_blobrefs: + # TODO: Try to upload the missing ones. + raise ServerError('Some blobs not uploaded: %r', missing_blobrefs) + + logging.debug('Upload of %d blobs successful.', len(blobref_dict)) + return received_blobrefs + + def get_blobs(self, + blobref_list, + start_out=None, + end_out=None, + check_sha1=True): + """Gets a set of blobs. + + Args: + blobref_list: A single blobref as a string or an iterable of strings that + are blobrefs. + start_out: Optional. A function taking the blobref's key, returns a + file-like object to which the blob should be written. Called before + the blob has started any writing. + end_out: Optional along with start_out. A function that takes the + blobref and open file-like object that does proper cleanup and closing + of the file. Called when all of the file's contents have been written. + check_sha1: Double-check that the file's contents match the blobref. + + Returns: + If start_out is not supplied, then all blobs will be kept in memory. If + blobref_list is a single blobref, then the return value will be a string + with the blob data or None if the blob was not present. If blobref_list + was iterable, the return value will be a dictionary mapping blobref to + blob data for each blob that was found. + + If start_out is supplied, the return value will be None. Callers can + check for missing blobs by comparing their own input of the blobref_list + argument to the blobrefs that are passed to start_out. + + Raises: + ServerError if the server response is invalid for whatever reason. + OSError or IOError if writing to any files breaks. + """ + multiple = not isinstance(blobref_list, basestring) + result = {} + if start_out is None: + def start_out(blobref): + buffer = cStringIO.StringIO() + return buffer + + def end_out(blobref, file_like): + result[blobref] = file_like.getvalue() + else: + result = None # Rely on user-supplied start_out for reporting blobrefs. + if end_out is None: + def end_out(blobref, file_like): + file_like.close() + + self._setup_connection() + + # Note, we could use a 'preupload' here as a quick, bulk existence check, + # but that may not always work depending on the access the user has. + # It's possible the user has read-only access, and thus can only do + # GET or HEAD on objects. + + for blobref in blobref_list: + logging.debug('Getting blobref=%s', blobref) + if self.basepath: + fullpath = self.basepath + '/camli/' + else: + fullpath = '/camli/' + self.connection.request('GET', fullpath + blobref, + headers={'Authorization': self._authorization}) + response = self.connection.getresponse() + if response.status == 404: + logging.debug('Server does not have blobref=%s', blobref) + continue + elif response.status != 200: + raise ServerError('Bad response status: %d %s' % + (response.status, response.reason)) + + if check_sha1: + compute_hash = hashlib.sha1() + + out_file = start_out(blobref) + while True: + buf = response.read(self.buffer_size) + if buf == '': + end_out(blobref, out_file) + break + + if check_sha1: + compute_hash.update(buf) + + out_file.write(buf) + + if check_sha1: + found = 'sha1-' + compute_hash.hexdigest() + if found != blobref: + raise ValueError('sha1 hash of blobref does not match; ' + 'found %s, expected %s' % (found, blobref)) + + if result and not multiple: + return result.values()[0] + return result diff --git a/vendor/github.com/camlistore/camlistore/lib/python/camli/schema.py b/vendor/github.com/camlistore/camlistore/lib/python/camli/schema.py new file mode 100644 index 00000000..8ee204d4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/camli/schema.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python +# +# Camlistore uploader client for Python. +# +# Copyright 2011 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Schema blob library for Camlistore.""" + +__author__ = 'Brett Slatkin (bslatkin@gmail.com)' + +import datetime +import re +import simplejson + +__all__ = [ + 'Error', 'DecodeError', 'SchemaBlob', 'FileCommon', 'File', + 'Directory', 'Symlink', 'decode'] + + +class Error(Exception): + """Base class for exceptions in this module.""" + +class DecodeError(Error): + """Could not decode the supplied schema blob.""" + + +# Maps 'camliType' to SchemaBlob sub-classes. +_TYPE_TO_CLASS = {} + + +def _camel_to_python(name): + """Converts camelcase to Python case.""" + return re.sub(r'([a-z]+)([A-Z])', r'\1_\2', name).lower() + + +class _SchemaMeta(type): + """Meta-class for schema blobs.""" + + def __init__(cls, name, bases, dict): + required_fields = set() + optional_fields = set() + json_to_python = {} + python_to_json = {} + serializers = {} + + def map_name(field): + if field.islower(): + return field + python_name = _camel_to_python(field) + json_to_python[field] = python_name + python_to_json[python_name] = field + return python_name + + for klz in bases + (cls,): + if hasattr(klz, '_json_to_python'): + json_to_python.update(klz._json_to_python) + if hasattr(klz, '_python_to_json'): + python_to_json.update(klz._python_to_json) + + if hasattr(klz, 'required_fields'): + for field in klz.required_fields: + field = map_name(field) + assert field not in required_fields, (klz, field) + assert field not in optional_fields, (klz, field) + required_fields.add(field) + + if hasattr(klz, 'optional_fields'): + for field in klz.optional_fields: + field = map_name(field) + assert field not in required_fields, (klz, field) + assert field not in optional_fields, (klz, field) + optional_fields.add(field) + + if hasattr(klz, '_serializers'): + for field, value in klz._serializers.iteritems(): + field = map_name(field) + assert (field in required_fields or + field in optional_fields), (klz, field) + if not isinstance(value, _FieldSerializer): + serializers[field] = value(field) + else: + serializers[field] = value + + setattr(cls, 'required_fields', frozenset(required_fields)) + setattr(cls, 'optional_fields', frozenset(optional_fields)) + setattr(cls, '_serializers', serializers) + setattr(cls, '_json_to_python', json_to_python) + setattr(cls, '_python_to_json', python_to_json) + if hasattr(cls, 'type'): + _TYPE_TO_CLASS[cls.type] = cls + + +class SchemaBlob(object): + """Base-class for schema blobs. + + Each sub-class should have these fields: + type: Required value of 'camliType'. + required_fields: Set of required field names. + optional_fields: Set of optional field names. + _serializers: Dictionary mapping field names to the _FieldSerializer + sub-class to use for serializing/deserializing the field's value. + """ + + __metaclass__ = _SchemaMeta + + required_fields = frozenset([ + 'camliVersion', + 'camliType', + ]) + optional_fields = frozenset([ + 'camliSigner', + 'camliSig', + ]) + _serializers = {} + + def __init__(self, blobref): + """Initializer. + + Args: + blobref: The blobref of the schema blob. + """ + self.blobref = blobref + self.unexpected_fields = {} + + @property + def all_fields(self): + """Returns the set of all potential fields for this blob.""" + all_fields = set() + all_fields.update(self.required_fields) + all_fields.update(self.optional_fields) + all_fields.update(self.unexpected_fields) + return all_fields + + def decode(self, blob_bytes, parsed=None): + """Decodes a schema blob's bytes and unmarshals its fields. + + Args: + blob_bytes: String with the bytes of the blob. + parsed: If not None, an already parsed version of the blob bytes. When + set, the blob_bytes argument is ignored. + + Raises: + DecodeError if the blob_bytes are bad or the parsed blob is missing + required fields. + """ + for field in self.all_fields: + if hasattr(self, field): + delattr(self, field) + + if parsed is None: + try: + parsed = simplejson.loads(blob_bytes) + except simplejson.JSONDecodeError, e: + raise DecodeError('Could not parse JSON. %s: %s' % (e.__class__, e)) + + for json_name, value in parsed.iteritems(): + name = self._json_to_python.get(json_name, json_name) + if not (name in self.required_fields or name in self.optional_fields): + self.unexpected_fields[name] = value + continue + serializer = self._serializers.get(name) + if serializer: + value = serializer.from_json(value) + setattr(self, name, value) + + for name in self.required_fields: + if not hasattr(self, name): + raise DecodeError('Missing required field: %s' % name) + + def encode(self): + """Encodes a schema blob's bytes and marshals its fields. + + Returns: + A UTF-8-encoding plain string containing the encoded blob bytes. + """ + out = {} + for python_name in self.all_fields: + if not hasattr(self, python_name): + continue + value = getattr(self, python_name) + serializer = self._serializers.get(python_name) + if serializer: + value = serializer.to_json(value) + json_name = self._python_to_json.get(python_name, python_name) + out[json_name] = value + return simplejson.dumps(out) + +################################################################################ +# Serializers for converting JSON fields to/from Python values + +class _FieldSerializer(object): + """Serializes a named field's value to and from JSON.""" + + def __init__(self, name): + """Initializer. + + Args: + name: The name of the field. + """ + self.name = name + + def from_json(self, value): + """Converts the JSON format of the field to the Python type. + + Args: + value: The JSON value. + + Returns: + The Python value. + """ + raise NotImplemented('Must implement from_json') + + def to_json(self, value): + """Converts the Python field value to the JSON format of the field. + + Args: + value: The Python value. + + Returns: + The JSON formatted-value. + """ + raise NotImplemented('Must implement to_json') + + +class _DateTimeSerializer(_FieldSerializer): + """Formats ISO 8601 strings to/from datetime.datetime instances.""" + + def from_json(self, value): + if '.' in value: + iso, micros = value.split('.') + micros = int((micros[:-1] + ('0' * 6))[:6]) + else: + iso, micros = value[:-1], 0 + + when = datetime.datetime.strptime(iso, '%Y-%m-%dT%H:%M:%S') + return when + datetime.timedelta(microseconds=micros) + + def to_json(self, value): + return value.isoformat() + 'Z' + +################################################################################ +# Concrete Schema Blobs + +class FileCommon(SchemaBlob): + """Common base-class for all unix-y files.""" + + required_fields = frozenset([]) + optional_fields = frozenset([ + 'fileName', + 'fileNameBytes', + 'unixPermission', + 'unixOwnerId', + 'unixGroupId', + 'unixGroup', + 'unixXattrs', + 'unixMtime', + 'unixCtime', + 'unixAtime', + ]) + _serializers = { + 'unixMtime': _DateTimeSerializer, + 'unixCtime': _DateTimeSerializer, + 'unixAtime': _DateTimeSerializer, + } + + +class File(FileCommon): + """A file.""" + + type = 'file' + required_fields = frozenset([ + 'size', + 'contentParts', + ]) + optional_fields = frozenset([ + 'inodeRef', + ]) + _serializers = {} + + +class Directory(FileCommon): + """A directory.""" + + type = 'directory' + required_fields = frozenset([ + 'entries', + ]) + optional_fields = frozenset([]) + _serializers = {} + + +class Symlink(FileCommon): + """A symlink.""" + + type = 'symlink' + required_fields = frozenset([]) + optional_fields = frozenset([ + 'symlinkTarget', + 'symlinkTargetBytes', + ]) + _serializers = {} + + +################################################################################ +# Helper methods + +def decode(blobref, blob_bytes): + """Decode any schema blob, validating all required fields for its time.""" + try: + parsed = simplejson.loads(blob_bytes) + except simplejson.JSONDecodeError, e: + raise DecodeError('Could not parse JSON. %s: %s' % (e.__class__, e)) + + if 'camliType' not in parsed: + raise DecodeError('Could not find "camliType" field.') + + camli_type = parsed['camliType'] + blob_class = _TYPE_TO_CLASS.get(camli_type) + if blob_class is None: + raise DecodeError( + 'Could not find SchemaBlob sub-class for camliType=%r' % camli_type) + + schema_blob = blob_class(blobref) + schema_blob.decode(None, parsed=parsed) + return schema_blob diff --git a/vendor/github.com/camlistore/camlistore/lib/python/camli/schema_test.py b/vendor/github.com/camlistore/camlistore/lib/python/camli/schema_test.py new file mode 100755 index 00000000..d38e4ea4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/camli/schema_test.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# +# Camlistore uploader client for Python. +# +# Copyright 2011 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Schema blob library for Camlistore.""" + +__author__ = 'Brett Slatkin (bslatkin@gmail.com)' + +import datetime +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + +import camli.schema +import simplejson + + +class SchemaTest(unittest.TestCase): + """End-to-end tests for Schema blobs.""" + + def testFile(self): + schema_blob = camli.schema.decode('asdf-myblobref', """{ + "camliVersion": 1, + "camliType": "file", + "size": 0, + "contentParts": [], + "unixMtime": "2010-07-10T17:14:51.5678Z", + "unixCtime": "2010-07-10T17:20:03Z" + }""") + self.assertTrue(isinstance(schema_blob, camli.schema.File)) + self.assertTrue(isinstance(schema_blob, camli.schema.FileCommon)) + self.assertTrue(isinstance(schema_blob, camli.schema.SchemaBlob)) + expected = { + 'unexpected_fields': {}, + 'unix_mtime': datetime.datetime(2010, 7, 10, 17, 14, 51, 567800), + 'content_parts': [], + 'blobref': 'asdf-myblobref', + 'unix_ctime': datetime.datetime(2010, 7, 10, 17, 20, 3), + 'camli_version': 1, + 'camli_type': u'file', + 'size': 0 + } + self.assertEquals(expected, schema_blob.__dict__) + result = schema_blob.encode() + result_parsed = simplejson.loads(result) + expected = { + 'camliType': 'file', + 'camliVersion': 1, + 'unixMtime': '2010-07-10T17:14:51.567800Z', + 'unixCtime': '2010-07-10T17:20:03Z', + 'contentParts': [], + 'size': 0, + } + self.assertEquals(expected, result_parsed) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/__init__.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/__init__.py new file mode 100644 index 00000000..4f327c18 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/__init__.py @@ -0,0 +1,8 @@ +import sys +pyver = sys.version_info[0:2] +if pyver <= (2, 4): + from fuse24 import * +elif pyver >= (3, 0): + from fuse3 import * +else: + from fuse import * diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/context.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/context.py new file mode 100755 index 00000000..2609aa05 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/context.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +from errno import ENOENT +from stat import S_IFDIR, S_IFREG +from sys import argv, exit +from time import time + +from fuse import FUSE, FuseOSError, Operations, LoggingMixIn, fuse_get_context + + +class Context(LoggingMixIn, Operations): + """Example filesystem to demonstrate fuse_get_context()""" + + def getattr(self, path, fh=None): + uid, gid, pid = fuse_get_context() + if path == '/': + st = dict(st_mode=(S_IFDIR | 0755), st_nlink=2) + elif path == '/uid': + size = len('%s\n' % uid) + st = dict(st_mode=(S_IFREG | 0444), st_size=size) + elif path == '/gid': + size = len('%s\n' % gid) + st = dict(st_mode=(S_IFREG | 0444), st_size=size) + elif path == '/pid': + size = len('%s\n' % pid) + st = dict(st_mode=(S_IFREG | 0444), st_size=size) + else: + raise FuseOSError(ENOENT) + st['st_ctime'] = st['st_mtime'] = st['st_atime'] = time() + return st + + def read(self, path, size, offset, fh): + uid, gid, pid = fuse_get_context() + if path == '/uid': + return '%s\n' % uid + elif path == '/gid': + return '%s\n' % gid + elif path == '/pid': + return '%s\n' % pid + return '' + + def readdir(self, path, fh): + return ['.', '..', 'uid', 'gid', 'pid'] + + # Disable unused operations: + access = None + flush = None + getxattr = None + listxattr = None + open = None + opendir = None + release = None + releasedir = None + statfs = None + + +if __name__ == "__main__": + if len(argv) != 2: + print 'usage: %s ' % argv[0] + exit(1) + fuse = FUSE(Context(), argv[1], foreground=True) \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/fuse.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/fuse.py new file mode 100644 index 00000000..b68e737a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/fuse.py @@ -0,0 +1,650 @@ +# Copyright (c) 2008 Giorgos Verigakis +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import division + +from ctypes import * +from ctypes.util import find_library +from errno import * +from functools import partial +from os import strerror +from platform import machine, system +from stat import S_IFDIR +from traceback import print_exc + + +class c_timespec(Structure): + _fields_ = [('tv_sec', c_long), ('tv_nsec', c_long)] + +class c_utimbuf(Structure): + _fields_ = [('actime', c_timespec), ('modtime', c_timespec)] + +class c_stat(Structure): + pass # Platform dependent + +_system = system() +if _system in ('Darwin', 'FreeBSD'): + _libiconv = CDLL(find_library("iconv"), RTLD_GLOBAL) # libfuse dependency + ENOTSUP = 45 + c_dev_t = c_int32 + c_fsblkcnt_t = c_ulong + c_fsfilcnt_t = c_ulong + c_gid_t = c_uint32 + c_mode_t = c_uint16 + c_off_t = c_int64 + c_pid_t = c_int32 + c_uid_t = c_uint32 + setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), + c_size_t, c_int, c_uint32) + getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), + c_size_t, c_uint32) + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_uint32), + ('st_mode', c_mode_t), + ('st_nlink', c_uint16), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec), + ('st_size', c_off_t), + ('st_blocks', c_int64), + ('st_blksize', c_int32)] +elif _system == 'Linux': + ENOTSUP = 95 + c_dev_t = c_ulonglong + c_fsblkcnt_t = c_ulonglong + c_fsfilcnt_t = c_ulonglong + c_gid_t = c_uint + c_mode_t = c_uint + c_off_t = c_longlong + c_pid_t = c_int + c_uid_t = c_uint + setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int) + getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t) + + _machine = machine() + if _machine == 'x86_64': + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_ulong), + ('st_nlink', c_ulong), + ('st_mode', c_mode_t), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('__pad0', c_int), + ('st_rdev', c_dev_t), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_long), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec)] + elif _machine == 'ppc': + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_ulonglong), + ('st_mode', c_mode_t), + ('st_nlink', c_uint), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('__pad2', c_ushort), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_longlong), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec)] + else: + # i686, use as fallback for everything else + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('__pad1', c_ushort), + ('__st_ino', c_ulong), + ('st_mode', c_mode_t), + ('st_nlink', c_uint), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('__pad2', c_ushort), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_longlong), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec), + ('st_ino', c_ulonglong)] +else: + raise NotImplementedError('%s is not supported.' % _system) + + +class c_statvfs(Structure): + _fields_ = [ + ('f_bsize', c_ulong), + ('f_frsize', c_ulong), + ('f_blocks', c_fsblkcnt_t), + ('f_bfree', c_fsblkcnt_t), + ('f_bavail', c_fsblkcnt_t), + ('f_files', c_fsfilcnt_t), + ('f_ffree', c_fsfilcnt_t), + ('f_favail', c_fsfilcnt_t)] + +if _system == 'FreeBSD': + c_fsblkcnt_t = c_uint64 + c_fsfilcnt_t = c_uint64 + setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int) + getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t) + class c_statvfs(Structure): + _fields_ = [ + ('f_bavail', c_fsblkcnt_t), + ('f_bfree', c_fsblkcnt_t), + ('f_blocks', c_fsblkcnt_t), + ('f_favail', c_fsfilcnt_t), + ('f_ffree', c_fsfilcnt_t), + ('f_files', c_fsfilcnt_t), + ('f_bsize', c_ulong), + ('f_flag', c_ulong), + ('f_frsize', c_ulong)] + +class fuse_file_info(Structure): + _fields_ = [ + ('flags', c_int), + ('fh_old', c_ulong), + ('writepage', c_int), + ('direct_io', c_uint, 1), + ('keep_cache', c_uint, 1), + ('flush', c_uint, 1), + ('padding', c_uint, 29), + ('fh', c_uint64), + ('lock_owner', c_uint64)] + +class fuse_context(Structure): + _fields_ = [ + ('fuse', c_voidp), + ('uid', c_uid_t), + ('gid', c_gid_t), + ('pid', c_pid_t), + ('private_data', c_voidp)] + +class fuse_operations(Structure): + _fields_ = [ + ('getattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat))), + ('readlink', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)), + ('getdir', c_voidp), # Deprecated, use readdir + ('mknod', CFUNCTYPE(c_int, c_char_p, c_mode_t, c_dev_t)), + ('mkdir', CFUNCTYPE(c_int, c_char_p, c_mode_t)), + ('unlink', CFUNCTYPE(c_int, c_char_p)), + ('rmdir', CFUNCTYPE(c_int, c_char_p)), + ('symlink', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('rename', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('link', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('chmod', CFUNCTYPE(c_int, c_char_p, c_mode_t)), + ('chown', CFUNCTYPE(c_int, c_char_p, c_uid_t, c_gid_t)), + ('truncate', CFUNCTYPE(c_int, c_char_p, c_off_t)), + ('utime', c_voidp), # Deprecated, use utimens + ('open', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('read', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, + POINTER(fuse_file_info))), + ('write', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, + POINTER(fuse_file_info))), + ('statfs', CFUNCTYPE(c_int, c_char_p, POINTER(c_statvfs))), + ('flush', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('release', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('fsync', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))), + ('setxattr', setxattr_t), + ('getxattr', getxattr_t), + ('listxattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)), + ('removexattr', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('opendir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('readdir', CFUNCTYPE(c_int, c_char_p, c_voidp, CFUNCTYPE(c_int, c_voidp, + c_char_p, POINTER(c_stat), c_off_t), c_off_t, POINTER(fuse_file_info))), + ('releasedir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('fsyncdir', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))), + ('init', CFUNCTYPE(c_voidp, c_voidp)), + ('destroy', CFUNCTYPE(c_voidp, c_voidp)), + ('access', CFUNCTYPE(c_int, c_char_p, c_int)), + ('create', CFUNCTYPE(c_int, c_char_p, c_mode_t, POINTER(fuse_file_info))), + ('ftruncate', CFUNCTYPE(c_int, c_char_p, c_off_t, POINTER(fuse_file_info))), + ('fgetattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat), + POINTER(fuse_file_info))), + ('lock', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info), c_int, c_voidp)), + ('utimens', CFUNCTYPE(c_int, c_char_p, POINTER(c_utimbuf))), + ('bmap', CFUNCTYPE(c_int, c_char_p, c_size_t, POINTER(c_ulonglong)))] + + +def time_of_timespec(ts): + return ts.tv_sec + ts.tv_nsec / 10 ** 9 + +def set_st_attrs(st, attrs): + for key, val in attrs.items(): + if key in ('st_atime', 'st_mtime', 'st_ctime'): + timespec = getattr(st, key + 'spec') + timespec.tv_sec = int(val) + timespec.tv_nsec = int((val - timespec.tv_sec) * 10 ** 9) + elif hasattr(st, key): + setattr(st, key, val) + + +_libfuse_path = find_library('fuse') +if not _libfuse_path: + raise EnvironmentError('Unable to find libfuse') +_libfuse = CDLL(_libfuse_path) +_libfuse.fuse_get_context.restype = POINTER(fuse_context) + + +def fuse_get_context(): + """Returns a (uid, gid, pid) tuple""" + ctxp = _libfuse.fuse_get_context() + ctx = ctxp.contents + return ctx.uid, ctx.gid, ctx.pid + + +class FuseOSError(OSError): + def __init__(self, errno): + super(FuseOSError, self).__init__(errno, strerror(errno)) + + +class FUSE(object): + """This class is the lower level interface and should not be subclassed + under normal use. Its methods are called by fuse. + Assumes API version 2.6 or later.""" + + def __init__(self, operations, mountpoint, raw_fi=False, **kwargs): + """Setting raw_fi to True will cause FUSE to pass the fuse_file_info + class as is to Operations, instead of just the fh field. + This gives you access to direct_io, keep_cache, etc.""" + + self.operations = operations + self.raw_fi = raw_fi + args = ['fuse'] + if kwargs.pop('foreground', False): + args.append('-f') + if kwargs.pop('debug', False): + args.append('-d') + if kwargs.pop('nothreads', False): + args.append('-s') + kwargs.setdefault('fsname', operations.__class__.__name__) + args.append('-o') + args.append(','.join(key if val == True else '%s=%s' % (key, val) + for key, val in kwargs.items())) + args.append(mountpoint) + argv = (c_char_p * len(args))(*args) + + fuse_ops = fuse_operations() + for name, prototype in fuse_operations._fields_: + if prototype != c_voidp and getattr(operations, name, None): + op = partial(self._wrapper_, getattr(self, name)) + setattr(fuse_ops, name, prototype(op)) + err = _libfuse.fuse_main_real(len(args), argv, pointer(fuse_ops), + sizeof(fuse_ops), None) + del self.operations # Invoke the destructor + if err: + raise RuntimeError(err) + + def _wrapper_(self, func, *args, **kwargs): + """Decorator for the methods that follow""" + try: + return func(*args, **kwargs) or 0 + except OSError, e: + return -(e.errno or EFAULT) + except: + print_exc() + return -EFAULT + + def getattr(self, path, buf): + return self.fgetattr(path, buf, None) + + def readlink(self, path, buf, bufsize): + ret = self.operations('readlink', path) + data = create_string_buffer(ret[:bufsize - 1]) + memmove(buf, data, len(data)) + return 0 + + def mknod(self, path, mode, dev): + return self.operations('mknod', path, mode, dev) + + def mkdir(self, path, mode): + return self.operations('mkdir', path, mode) + + def unlink(self, path): + return self.operations('unlink', path) + + def rmdir(self, path): + return self.operations('rmdir', path) + + def symlink(self, source, target): + return self.operations('symlink', target, source) + + def rename(self, old, new): + return self.operations('rename', old, new) + + def link(self, source, target): + return self.operations('link', target, source) + + def chmod(self, path, mode): + return self.operations('chmod', path, mode) + + def chown(self, path, uid, gid): + # Check if any of the arguments is a -1 that has overflowed + if c_uid_t(uid + 1).value == 0: + uid = -1 + if c_gid_t(gid + 1).value == 0: + gid = -1 + return self.operations('chown', path, uid, gid) + + def truncate(self, path, length): + return self.operations('truncate', path, length) + + def open(self, path, fip): + fi = fip.contents + if self.raw_fi: + return self.operations('open', path, fi) + else: + fi.fh = self.operations('open', path, fi.flags) + return 0 + + def read(self, path, buf, size, offset, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + ret = self.operations('read', path, size, offset, fh) + if not ret: + return 0 + data = create_string_buffer(ret[:size], size) + memmove(buf, data, size) + return size + + def write(self, path, buf, size, offset, fip): + data = string_at(buf, size) + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('write', path, data, offset, fh) + + def statfs(self, path, buf): + stv = buf.contents + attrs = self.operations('statfs', path) + for key, val in attrs.items(): + if hasattr(stv, key): + setattr(stv, key, val) + return 0 + + def flush(self, path, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('flush', path, fh) + + def release(self, path, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('release', path, fh) + + def fsync(self, path, datasync, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('fsync', path, datasync, fh) + + def setxattr(self, path, name, value, size, options, *args): + data = string_at(value, size) + return self.operations('setxattr', path, name, data, options, *args) + + def getxattr(self, path, name, value, size, *args): + ret = self.operations('getxattr', path, name, *args) + retsize = len(ret) + buf = create_string_buffer(ret, retsize) # Does not add trailing 0 + if bool(value): + if retsize > size: + return -ERANGE + memmove(value, buf, retsize) + return retsize + + def listxattr(self, path, namebuf, size): + ret = self.operations('listxattr', path) + buf = create_string_buffer('\x00'.join(ret)) if ret else '' + bufsize = len(buf) + if bool(namebuf): + if bufsize > size: + return -ERANGE + memmove(namebuf, buf, bufsize) + return bufsize + + def removexattr(self, path, name): + return self.operations('removexattr', path, name) + + def opendir(self, path, fip): + # Ignore raw_fi + fip.contents.fh = self.operations('opendir', path) + return 0 + + def readdir(self, path, buf, filler, offset, fip): + # Ignore raw_fi + for item in self.operations('readdir', path, fip.contents.fh): + if isinstance(item, str): + name, st, offset = item, None, 0 + else: + name, attrs, offset = item + if attrs: + st = c_stat() + set_st_attrs(st, attrs) + else: + st = None + if filler(buf, name, st, offset) != 0: + break + return 0 + + def releasedir(self, path, fip): + # Ignore raw_fi + return self.operations('releasedir', path, fip.contents.fh) + + def fsyncdir(self, path, datasync, fip): + # Ignore raw_fi + return self.operations('fsyncdir', path, datasync, fip.contents.fh) + + def init(self, conn): + return self.operations('init', '/') + + def destroy(self, private_data): + return self.operations('destroy', '/') + + def access(self, path, amode): + return self.operations('access', path, amode) + + def create(self, path, mode, fip): + fi = fip.contents + if self.raw_fi: + return self.operations('create', path, mode, fi) + else: + fi.fh = self.operations('create', path, mode) + return 0 + + def ftruncate(self, path, length, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('truncate', path, length, fh) + + def fgetattr(self, path, buf, fip): + memset(buf, 0, sizeof(c_stat)) + st = buf.contents + fh = fip and (fip.contents if self.raw_fi else fip.contents.fh) + attrs = self.operations('getattr', path, fh) + set_st_attrs(st, attrs) + return 0 + + def lock(self, path, fip, cmd, lock): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('lock', path, fh, cmd, lock) + + def utimens(self, path, buf): + if buf: + atime = time_of_timespec(buf.contents.actime) + mtime = time_of_timespec(buf.contents.modtime) + times = (atime, mtime) + else: + times = None + return self.operations('utimens', path, times) + + def bmap(self, path, blocksize, idx): + return self.operations('bmap', path, blocksize, idx) + + +class Operations(object): + """This class should be subclassed and passed as an argument to FUSE on + initialization. All operations should raise a FuseOSError exception + on error. + + When in doubt of what an operation should do, check the FUSE header + file or the corresponding system call man page.""" + + def __call__(self, op, *args): + if not hasattr(self, op): + raise FuseOSError(EFAULT) + return getattr(self, op)(*args) + + def access(self, path, amode): + return 0 + + bmap = None + + def chmod(self, path, mode): + raise FuseOSError(EROFS) + + def chown(self, path, uid, gid): + raise FuseOSError(EROFS) + + def create(self, path, mode, fi=None): + """When raw_fi is False (default case), fi is None and create should + return a numerical file handle. + When raw_fi is True the file handle should be set directly by create + and return 0.""" + raise FuseOSError(EROFS) + + def destroy(self, path): + """Called on filesystem destruction. Path is always /""" + pass + + def flush(self, path, fh): + return 0 + + def fsync(self, path, datasync, fh): + return 0 + + def fsyncdir(self, path, datasync, fh): + return 0 + + def getattr(self, path, fh=None): + """Returns a dictionary with keys identical to the stat C structure + of stat(2). + st_atime, st_mtime and st_ctime should be floats. + NOTE: There is an incombatibility between Linux and Mac OS X concerning + st_nlink of directories. Mac OS X counts all files inside the directory, + while Linux counts only the subdirectories.""" + + if path != '/': + raise FuseOSError(ENOENT) + return dict(st_mode=(S_IFDIR | 0755), st_nlink=2) + + def getxattr(self, path, name, position=0): + raise FuseOSError(ENOTSUP) + + def init(self, path): + """Called on filesystem initialization. Path is always / + Use it instead of __init__ if you start threads on initialization.""" + pass + + def link(self, target, source): + raise FuseOSError(EROFS) + + def listxattr(self, path): + return [] + + lock = None + + def mkdir(self, path, mode): + raise FuseOSError(EROFS) + + def mknod(self, path, mode, dev): + raise FuseOSError(EROFS) + + def open(self, path, flags): + """When raw_fi is False (default case), open should return a numerical + file handle. + When raw_fi is True the signature of open becomes: + open(self, path, fi) + and the file handle should be set directly.""" + return 0 + + def opendir(self, path): + """Returns a numerical file handle.""" + return 0 + + def read(self, path, size, offset, fh): + """Returns a string containing the data requested.""" + raise FuseOSError(EIO) + + def readdir(self, path, fh): + """Can return either a list of names, or a list of (name, attrs, offset) + tuples. attrs is a dict as in getattr.""" + return ['.', '..'] + + def readlink(self, path): + raise FuseOSError(ENOENT) + + def release(self, path, fh): + return 0 + + def releasedir(self, path, fh): + return 0 + + def removexattr(self, path, name): + raise FuseOSError(ENOTSUP) + + def rename(self, old, new): + raise FuseOSError(EROFS) + + def rmdir(self, path): + raise FuseOSError(EROFS) + + def setxattr(self, path, name, value, options, position=0): + raise FuseOSError(ENOTSUP) + + def statfs(self, path): + """Returns a dictionary with keys identical to the statvfs C structure + of statvfs(3). + On Mac OS X f_bsize and f_frsize must be a power of 2 (minimum 512).""" + return {} + + def symlink(self, target, source): + raise FuseOSError(EROFS) + + def truncate(self, path, length, fh=None): + raise FuseOSError(EROFS) + + def unlink(self, path): + raise FuseOSError(EROFS) + + def utimens(self, path, times=None): + """Times is a (atime, mtime) tuple. If None use current time.""" + return 0 + + def write(self, path, data, offset, fh): + raise FuseOSError(EROFS) + + +class LoggingMixIn: + def __call__(self, op, path, *args): + print '->', op, path, repr(args) + ret = '[Unhandled Exception]' + try: + ret = getattr(self, op)(path, *args) + return ret + except OSError, e: + ret = str(e) + raise + finally: + print '<-', op, repr(ret) diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/fuse24.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/fuse24.py new file mode 100644 index 00000000..8c20679a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/fuse24.py @@ -0,0 +1,669 @@ +# Copyright (c) 2008 Giorgos Verigakis +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import division + +from ctypes import * +from ctypes.util import find_library +from errno import * +from platform import machine, system +from stat import S_IFDIR +from traceback import print_exc + + +class c_timespec(Structure): + _fields_ = [('tv_sec', c_long), ('tv_nsec', c_long)] + +class c_utimbuf(Structure): + _fields_ = [('actime', c_timespec), ('modtime', c_timespec)] + +class c_stat(Structure): + pass # Platform dependent + +_system = system() +if _system in ('Darwin', 'FreeBSD'): + _libiconv = CDLL(find_library("iconv"), RTLD_GLOBAL) # libfuse dependency + ENOTSUP = 45 + c_dev_t = c_int32 + c_fsblkcnt_t = c_ulong + c_fsfilcnt_t = c_ulong + c_gid_t = c_uint32 + c_mode_t = c_uint16 + c_off_t = c_int64 + c_pid_t = c_int32 + c_uid_t = c_uint32 + setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), + c_size_t, c_int, c_uint32) + getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), + c_size_t, c_uint32) + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_uint32), + ('st_mode', c_mode_t), + ('st_nlink', c_uint16), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec), + ('st_size', c_off_t), + ('st_blocks', c_int64), + ('st_blksize', c_int32)] +elif _system == 'Linux': + ENOTSUP = 95 + c_dev_t = c_ulonglong + c_fsblkcnt_t = c_ulonglong + c_fsfilcnt_t = c_ulonglong + c_gid_t = c_uint + c_mode_t = c_uint + c_off_t = c_longlong + c_pid_t = c_int + c_uid_t = c_uint + setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int) + getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t) + + _machine = machine() + if _machine == 'x86_64': + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_ulong), + ('st_nlink', c_ulong), + ('st_mode', c_mode_t), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('__pad0', c_int), + ('st_rdev', c_dev_t), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_long), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec)] + elif _machine == 'ppc': + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_ulonglong), + ('st_mode', c_mode_t), + ('st_nlink', c_uint), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('__pad2', c_ushort), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_longlong), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec)] + else: + # i686, use as fallback for everything else + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('__pad1', c_ushort), + ('__st_ino', c_ulong), + ('st_mode', c_mode_t), + ('st_nlink', c_uint), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('__pad2', c_ushort), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_longlong), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec), + ('st_ino', c_ulonglong)] +else: + raise NotImplementedError('%s is not supported.' % _system) + + +class c_statvfs(Structure): + _fields_ = [ + ('f_bsize', c_ulong), + ('f_frsize', c_ulong), + ('f_blocks', c_fsblkcnt_t), + ('f_bfree', c_fsblkcnt_t), + ('f_bavail', c_fsblkcnt_t), + ('f_files', c_fsfilcnt_t), + ('f_ffree', c_fsfilcnt_t), + ('f_favail', c_fsfilcnt_t)] + +if _system == 'FreeBSD': + c_fsblkcnt_t = c_uint64 + c_fsfilcnt_t = c_uint64 + setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int) + getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t) + class c_statvfs(Structure): + _fields_ = [ + ('f_bavail', c_fsblkcnt_t), + ('f_bfree', c_fsblkcnt_t), + ('f_blocks', c_fsblkcnt_t), + ('f_favail', c_fsfilcnt_t), + ('f_ffree', c_fsfilcnt_t), + ('f_files', c_fsfilcnt_t), + ('f_bsize', c_ulong), + ('f_flag', c_ulong), + ('f_frsize', c_ulong)] + +class fuse_file_info(Structure): + _fields_ = [ + ('flags', c_int), + ('fh_old', c_ulong), + ('writepage', c_int), + ('direct_io', c_uint, 1), + ('keep_cache', c_uint, 1), + ('flush', c_uint, 1), + ('padding', c_uint, 29), + ('fh', c_uint64), + ('lock_owner', c_uint64)] + +class fuse_context(Structure): + _fields_ = [ + ('fuse', c_voidp), + ('uid', c_uid_t), + ('gid', c_gid_t), + ('pid', c_pid_t), + ('private_data', c_voidp)] + +class fuse_operations(Structure): + _fields_ = [ + ('getattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat))), + ('readlink', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)), + ('getdir', c_voidp), # Deprecated, use readdir + ('mknod', CFUNCTYPE(c_int, c_char_p, c_mode_t, c_dev_t)), + ('mkdir', CFUNCTYPE(c_int, c_char_p, c_mode_t)), + ('unlink', CFUNCTYPE(c_int, c_char_p)), + ('rmdir', CFUNCTYPE(c_int, c_char_p)), + ('symlink', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('rename', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('link', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('chmod', CFUNCTYPE(c_int, c_char_p, c_mode_t)), + ('chown', CFUNCTYPE(c_int, c_char_p, c_uid_t, c_gid_t)), + ('truncate', CFUNCTYPE(c_int, c_char_p, c_off_t)), + ('utime', c_voidp), # Deprecated, use utimens + ('open', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('read', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, + POINTER(fuse_file_info))), + ('write', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, + POINTER(fuse_file_info))), + ('statfs', CFUNCTYPE(c_int, c_char_p, POINTER(c_statvfs))), + ('flush', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('release', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('fsync', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))), + ('setxattr', setxattr_t), + ('getxattr', getxattr_t), + ('listxattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)), + ('removexattr', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('opendir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('readdir', CFUNCTYPE(c_int, c_char_p, c_voidp, CFUNCTYPE(c_int, c_voidp, + c_char_p, POINTER(c_stat), c_off_t), c_off_t, POINTER(fuse_file_info))), + ('releasedir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('fsyncdir', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))), + ('init', CFUNCTYPE(c_voidp, c_voidp)), + ('destroy', CFUNCTYPE(c_voidp, c_voidp)), + ('access', CFUNCTYPE(c_int, c_char_p, c_int)), + ('create', CFUNCTYPE(c_int, c_char_p, c_mode_t, POINTER(fuse_file_info))), + ('ftruncate', CFUNCTYPE(c_int, c_char_p, c_off_t, POINTER(fuse_file_info))), + ('fgetattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat), + POINTER(fuse_file_info))), + ('lock', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info), c_int, c_voidp)), + ('utimens', CFUNCTYPE(c_int, c_char_p, POINTER(c_utimbuf))), + ('bmap', CFUNCTYPE(c_int, c_char_p, c_size_t, POINTER(c_ulonglong)))] + + +def time_of_timespec(ts): + return ts.tv_sec + ts.tv_nsec / 10 ** 9 + +def set_st_attrs(st, attrs): + for key, val in attrs.items(): + if key in ('st_atime', 'st_mtime', 'st_ctime'): + timespec = getattr(st, key + 'spec') + timespec.tv_sec = int(val) + timespec.tv_nsec = int((val - timespec.tv_sec) * 10 ** 9) + elif hasattr(st, key): + setattr(st, key, val) + + +_libfuse_path = find_library('fuse') +if not _libfuse_path: + raise EnvironmentError('Unable to find libfuse') +_libfuse = CDLL(_libfuse_path) +_libfuse.fuse_get_context.restype = POINTER(fuse_context) + + +def fuse_get_context(): + """Returns a (uid, gid, pid) tuple""" + ctxp = _libfuse.fuse_get_context() + ctx = ctxp.contents + return ctx.uid, ctx.gid, ctx.pid + + +class FUSE(object): + """This class is the lower level interface and should not be subclassed + under normal use. Its methods are called by fuse. + Assumes API version 2.6 or later.""" + + def __init__(self, operations, mountpoint, raw_fi=False, **kwargs): + """Setting raw_fi to True will cause FUSE to pass the fuse_file_info + class as is to Operations, instead of just the fh field. + This gives you access to direct_io, keep_cache, etc.""" + + self.operations = operations + self.raw_fi = raw_fi + args = ['fuse'] + if kwargs.pop('foreground', False): + args.append('-f') + if kwargs.pop('debug', False): + args.append('-d') + if kwargs.pop('nothreads', False): + args.append('-s') + kwargs.setdefault('fsname', operations.__class__.__name__) + args.append('-o') + args.append(','.join(val is True and key or '%s=%s' % (key, val) + for key, val in kwargs.items())) + args.append(mountpoint) + argv = (c_char_p * len(args))(*args) + + fuse_ops = fuse_operations() + for name, prototype in fuse_operations._fields_: + if prototype != c_voidp and getattr(operations, name, None): + op = self._create_wrapper_(getattr(self, name)) + setattr(fuse_ops, name, prototype(op)) + _libfuse.fuse_main_real(len(args), argv, pointer(fuse_ops), + sizeof(fuse_ops), None) + del self.operations # Invoke the destructor + + @staticmethod + def _create_wrapper_(func): + def _wrapper_(*args, **kwargs): + """Decorator for the methods that follow""" + try: + return func(*args, **kwargs) or 0 + except OSError, e: + return -(e.errno or EFAULT) + except: + print_exc() + return -EFAULT + return _wrapper_ + + def getattr(self, path, buf): + return self.fgetattr(path, buf, None) + + def readlink(self, path, buf, bufsize): + ret = self.operations('readlink', path) + data = create_string_buffer(ret[:bufsize - 1]) + memmove(buf, data, len(data)) + return 0 + + def mknod(self, path, mode, dev): + return self.operations('mknod', path, mode, dev) + + def mkdir(self, path, mode): + return self.operations('mkdir', path, mode) + + def unlink(self, path): + return self.operations('unlink', path) + + def rmdir(self, path): + return self.operations('rmdir', path) + + def symlink(self, source, target): + return self.operations('symlink', target, source) + + def rename(self, old, new): + return self.operations('rename', old, new) + + def link(self, source, target): + return self.operations('link', target, source) + + def chmod(self, path, mode): + return self.operations('chmod', path, mode) + + def chown(self, path, uid, gid): + return self.operations('chown', path, uid, gid) + + def truncate(self, path, length): + return self.operations('truncate', path, length) + + def open(self, path, fip): + fi = fip.contents + if self.raw_fi: + return self.operations('open', path, fi) + else: + fi.fh = self.operations('open', path, fi.flags) + return 0 + + def read(self, path, buf, size, offset, fip): + if self.raw_fi: + fh = fip.contents + else: + fh = fip.contents.fh + ret = self.operations('read', path, size, offset, fh) + if not ret: + return 0 + data = create_string_buffer(ret[:size], size) + memmove(buf, data, size) + return size + + def write(self, path, buf, size, offset, fip): + data = string_at(buf, size) + if self.raw_fi: + fh = fip.contents + else: + fh = fip.contents.fh + return self.operations('write', path, data, offset, fh) + + def statfs(self, path, buf): + stv = buf.contents + attrs = self.operations('statfs', path) + for key, val in attrs.items(): + if hasattr(stv, key): + setattr(stv, key, val) + return 0 + + def flush(self, path, fip): + if self.raw_fi: + fh = fip.contents + else: + fh = fip.contents.fh + return self.operations('flush', path, fh) + + def release(self, path, fip): + if self.raw_fi: + fh = fip.contents + else: + fh = fip.contents.fh + return self.operations('release', path, fh) + + def fsync(self, path, datasync, fip): + if self.raw_fi: + fh = fip.contents + else: + fh = fip.contents.fh + return self.operations('fsync', path, datasync, fh) + + def setxattr(self, path, name, value, size, options, *args): + data = string_at(value, size) + return self.operations('setxattr', path, name, data, options, *args) + + def getxattr(self, path, name, value, size, *args): + ret = self.operations('getxattr', path, name, *args) + retsize = len(ret) + buf = create_string_buffer(ret, retsize) # Does not add trailing 0 + if bool(value): + if retsize > size: + return -ERANGE + memmove(value, buf, retsize) + return retsize + + def listxattr(self, path, namebuf, size): + ret = self.operations('listxattr', path) + if ret: + buf = create_string_buffer('\x00'.join(ret)) + else: + buf = '' + bufsize = len(buf) + if bool(namebuf): + if bufsize > size: + return -ERANGE + memmove(namebuf, buf, bufsize) + return bufsize + + def removexattr(self, path, name): + return self.operations('removexattr', path, name) + + def opendir(self, path, fip): + # Ignore raw_fi + fip.contents.fh = self.operations('opendir', path) + return 0 + + def readdir(self, path, buf, filler, offset, fip): + # Ignore raw_fi + for item in self.operations('readdir', path, fip.contents.fh): + if isinstance(item, str): + name, st, offset = item, None, 0 + else: + name, attrs, offset = item + if attrs: + st = c_stat() + set_st_attrs(st, attrs) + else: + st = None + if filler(buf, name, st, offset) != 0: + break + return 0 + + def releasedir(self, path, fip): + # Ignore raw_fi + return self.operations('releasedir', path, fip.contents.fh) + + def fsyncdir(self, path, datasync, fip): + # Ignore raw_fi + return self.operations('fsyncdir', path, datasync, fip.contents.fh) + + def init(self, conn): + return self.operations('init', '/') + + def destroy(self, private_data): + return self.operations('destroy', '/') + + def access(self, path, amode): + return self.operations('access', path, amode) + + def create(self, path, mode, fip): + fi = fip.contents + if self.raw_fi: + return self.operations('create', path, mode, fi) + else: + fi.fh = self.operations('create', path, mode) + return 0 + + def ftruncate(self, path, length, fip): + if self.raw_fi: + fh = fip.contents + else: + fh = fip.contents.fh + return self.operations('truncate', path, length, fh) + + def fgetattr(self, path, buf, fip): + memset(buf, 0, sizeof(c_stat)) + st = buf.contents + if not fip: + fh = fip + elif self.raw_fi: + fh = fip.contents + else: + fh = fip.contents.fh + attrs = self.operations('getattr', path, fh) + set_st_attrs(st, attrs) + return 0 + + def lock(self, path, fip, cmd, lock): + if self.raw_fi: + fh = fip.contents + else: + fh = fip.contents.fh + return self.operations('lock', path, fh, cmd, lock) + + def utimens(self, path, buf): + if buf: + atime = time_of_timespec(buf.contents.actime) + mtime = time_of_timespec(buf.contents.modtime) + times = (atime, mtime) + else: + times = None + return self.operations('utimens', path, times) + + def bmap(self, path, blocksize, idx): + return self.operations('bmap', path, blocksize, idx) + + +class Operations(object): + """This class should be subclassed and passed as an argument to FUSE on + initialization. All operations should raise an OSError exception on + error. + + When in doubt of what an operation should do, check the FUSE header + file or the corresponding system call man page.""" + + def __call__(self, op, *args): + if not hasattr(self, op): + raise OSError(EFAULT, '') + return getattr(self, op)(*args) + + def access(self, path, amode): + return 0 + + bmap = None + + def chmod(self, path, mode): + raise OSError(EROFS, '') + + def chown(self, path, uid, gid): + raise OSError(EROFS, '') + + def create(self, path, mode, fi=None): + """When raw_fi is False (default case), fi is None and create should + return a numerical file handle. + When raw_fi is True the file handle should be set directly by create + and return 0.""" + raise OSError(EROFS, '') + + def destroy(self, path): + """Called on filesystem destruction. Path is always /""" + pass + + def flush(self, path, fh): + return 0 + + def fsync(self, path, datasync, fh): + return 0 + + def fsyncdir(self, path, datasync, fh): + return 0 + + def getattr(self, path, fh=None): + """Returns a dictionary with keys identical to the stat C structure + of stat(2). + st_atime, st_mtime and st_ctime should be floats. + NOTE: There is an incombatibility between Linux and Mac OS X concerning + st_nlink of directories. Mac OS X counts all files inside the directory, + while Linux counts only the subdirectories.""" + + if path != '/': + raise OSError(ENOENT, '') + return dict(st_mode=(S_IFDIR | 0755), st_nlink=2) + + def getxattr(self, path, name, position=0): + raise OSError(ENOTSUP, '') + + def init(self, path): + """Called on filesystem initialization. Path is always / + Use it instead of __init__ if you start threads on initialization.""" + pass + + def link(self, target, source): + raise OSError(EROFS, '') + + def listxattr(self, path): + return [] + + lock = None + + def mkdir(self, path, mode): + raise OSError(EROFS, '') + + def mknod(self, path, mode, dev): + raise OSError(EROFS, '') + + def open(self, path, flags): + """When raw_fi is False (default case), open should return a numerical + file handle. + When raw_fi is True the signature of open becomes: + open(self, path, fi) + and the file handle should be set directly.""" + return 0 + + def opendir(self, path): + """Returns a numerical file handle.""" + return 0 + + def read(self, path, size, offset, fh): + """Returns a string containing the data requested.""" + raise OSError(ENOENT, '') + + def readdir(self, path, fh): + """Can return either a list of names, or a list of (name, attrs, offset) + tuples. attrs is a dict as in getattr.""" + return ['.', '..'] + + def readlink(self, path): + raise OSError(ENOENT, '') + + def release(self, path, fh): + return 0 + + def releasedir(self, path, fh): + return 0 + + def removexattr(self, path, name): + raise OSError(ENOTSUP, '') + + def rename(self, old, new): + raise OSError(EROFS, '') + + def rmdir(self, path): + raise OSError(EROFS, '') + + def setxattr(self, path, name, value, options, position=0): + raise OSError(ENOTSUP, '') + + def statfs(self, path): + """Returns a dictionary with keys identical to the statvfs C structure + of statvfs(3). + On Mac OS X f_bsize and f_frsize must be a power of 2 (minimum 512).""" + return {} + + def symlink(self, target, source): + raise OSError(EROFS, '') + + def truncate(self, path, length, fh=None): + raise OSError(EROFS, '') + + def unlink(self, path): + raise OSError(EROFS, '') + + def utimens(self, path, times=None): + """Times is a (atime, mtime) tuple. If None use current time.""" + return 0 + + def write(self, path, data, offset, fh): + raise OSError(EROFS, '') + + +class LoggingMixIn: + def __call__(self, op, path, *args): + print '->', op, path, repr(args) + ret = '[Unknown Error]' + try: + try: + ret = getattr(self, op)(path, *args) + return ret + except OSError, e: + ret = str(e) + raise + finally: + print '<-', op, repr(ret) diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/fuse3.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/fuse3.py new file mode 100644 index 00000000..8717b47a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/fuse3.py @@ -0,0 +1,637 @@ +# Copyright (c) 2008 Giorgos Verigakis +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from ctypes import * +from ctypes.util import find_library +from errno import * +from functools import partial +from platform import machine, system +from stat import S_IFDIR +from traceback import print_exc + +import logging + + +class c_timespec(Structure): + _fields_ = [('tv_sec', c_long), ('tv_nsec', c_long)] + +class c_utimbuf(Structure): + _fields_ = [('actime', c_timespec), ('modtime', c_timespec)] + +class c_stat(Structure): + pass # Platform dependent + +_system = system() +if _system in ('Darwin', 'FreeBSD'): + _libiconv = CDLL(find_library("iconv"), RTLD_GLOBAL) # libfuse dependency + ENOTSUP = 45 + c_dev_t = c_int32 + c_fsblkcnt_t = c_ulong + c_fsfilcnt_t = c_ulong + c_gid_t = c_uint32 + c_mode_t = c_uint16 + c_off_t = c_int64 + c_pid_t = c_int32 + c_uid_t = c_uint32 + setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), + c_size_t, c_int, c_uint32) + getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), + c_size_t, c_uint32) + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_uint32), + ('st_mode', c_mode_t), + ('st_nlink', c_uint16), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec), + ('st_size', c_off_t), + ('st_blocks', c_int64), + ('st_blksize', c_int32)] +elif _system == 'Linux': + ENOTSUP = 95 + c_dev_t = c_ulonglong + c_fsblkcnt_t = c_ulonglong + c_fsfilcnt_t = c_ulonglong + c_gid_t = c_uint + c_mode_t = c_uint + c_off_t = c_longlong + c_pid_t = c_int + c_uid_t = c_uint + setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int) + getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t) + + _machine = machine() + if _machine == 'x86_64': + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_ulong), + ('st_nlink', c_ulong), + ('st_mode', c_mode_t), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('__pad0', c_int), + ('st_rdev', c_dev_t), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_long), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec)] + elif _machine == 'ppc': + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_ulonglong), + ('st_mode', c_mode_t), + ('st_nlink', c_uint), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('__pad2', c_ushort), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_longlong), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec)] + else: + # i686, use as fallback for everything else + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('__pad1', c_ushort), + ('__st_ino', c_ulong), + ('st_mode', c_mode_t), + ('st_nlink', c_uint), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('__pad2', c_ushort), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_longlong), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec), + ('st_ino', c_ulonglong)] +else: + raise NotImplementedError('%s is not supported.' % _system) + + +class c_statvfs(Structure): + _fields_ = [ + ('f_bsize', c_ulong), + ('f_frsize', c_ulong), + ('f_blocks', c_fsblkcnt_t), + ('f_bfree', c_fsblkcnt_t), + ('f_bavail', c_fsblkcnt_t), + ('f_files', c_fsfilcnt_t), + ('f_ffree', c_fsfilcnt_t), + ('f_favail', c_fsfilcnt_t)] + +if _system == 'FreeBSD': + c_fsblkcnt_t = c_uint64 + c_fsfilcnt_t = c_uint64 + setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int) + getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t) + class c_statvfs(Structure): + _fields_ = [ + ('f_bavail', c_fsblkcnt_t), + ('f_bfree', c_fsblkcnt_t), + ('f_blocks', c_fsblkcnt_t), + ('f_favail', c_fsfilcnt_t), + ('f_ffree', c_fsfilcnt_t), + ('f_files', c_fsfilcnt_t), + ('f_bsize', c_ulong), + ('f_flag', c_ulong), + ('f_frsize', c_ulong)] + +class fuse_file_info(Structure): + _fields_ = [ + ('flags', c_int), + ('fh_old', c_ulong), + ('writepage', c_int), + ('direct_io', c_uint, 1), + ('keep_cache', c_uint, 1), + ('flush', c_uint, 1), + ('padding', c_uint, 29), + ('fh', c_uint64), + ('lock_owner', c_uint64)] + +class fuse_context(Structure): + _fields_ = [ + ('fuse', c_voidp), + ('uid', c_uid_t), + ('gid', c_gid_t), + ('pid', c_pid_t), + ('private_data', c_voidp)] + +class fuse_operations(Structure): + _fields_ = [ + ('getattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat))), + ('readlink', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)), + ('getdir', c_voidp), # Deprecated, use readdir + ('mknod', CFUNCTYPE(c_int, c_char_p, c_mode_t, c_dev_t)), + ('mkdir', CFUNCTYPE(c_int, c_char_p, c_mode_t)), + ('unlink', CFUNCTYPE(c_int, c_char_p)), + ('rmdir', CFUNCTYPE(c_int, c_char_p)), + ('symlink', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('rename', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('link', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('chmod', CFUNCTYPE(c_int, c_char_p, c_mode_t)), + ('chown', CFUNCTYPE(c_int, c_char_p, c_uid_t, c_gid_t)), + ('truncate', CFUNCTYPE(c_int, c_char_p, c_off_t)), + ('utime', c_voidp), # Deprecated, use utimens + ('open', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('read', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, + POINTER(fuse_file_info))), + ('write', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, + POINTER(fuse_file_info))), + ('statfs', CFUNCTYPE(c_int, c_char_p, POINTER(c_statvfs))), + ('flush', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('release', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('fsync', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))), + ('setxattr', setxattr_t), + ('getxattr', getxattr_t), + ('listxattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)), + ('removexattr', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('opendir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('readdir', CFUNCTYPE(c_int, c_char_p, c_voidp, CFUNCTYPE(c_int, c_voidp, + c_char_p, POINTER(c_stat), c_off_t), c_off_t, POINTER(fuse_file_info))), + ('releasedir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('fsyncdir', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))), + ('init', CFUNCTYPE(c_voidp, c_voidp)), + ('destroy', CFUNCTYPE(c_voidp, c_voidp)), + ('access', CFUNCTYPE(c_int, c_char_p, c_int)), + ('create', CFUNCTYPE(c_int, c_char_p, c_mode_t, POINTER(fuse_file_info))), + ('ftruncate', CFUNCTYPE(c_int, c_char_p, c_off_t, POINTER(fuse_file_info))), + ('fgetattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat), + POINTER(fuse_file_info))), + ('lock', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info), c_int, c_voidp)), + ('utimens', CFUNCTYPE(c_int, c_char_p, POINTER(c_utimbuf))), + ('bmap', CFUNCTYPE(c_int, c_char_p, c_size_t, POINTER(c_ulonglong)))] + + +def time_of_timespec(ts): + return ts.tv_sec + ts.tv_nsec / 10 ** 9 + +def set_st_attrs(st, attrs): + for key, val in attrs.items(): + if key in ('st_atime', 'st_mtime', 'st_ctime'): + timespec = getattr(st, key + 'spec') + timespec.tv_sec = int(val) + timespec.tv_nsec = int((val - timespec.tv_sec) * 10 ** 9) + elif hasattr(st, key): + setattr(st, key, val) + + +_libfuse_path = find_library('fuse') +if not _libfuse_path: + raise EnvironmentError('Unable to find libfuse') +_libfuse = CDLL(_libfuse_path) +_libfuse.fuse_get_context.restype = POINTER(fuse_context) + + +def fuse_get_context(): + """Returns a (uid, gid, pid) tuple""" + ctxp = _libfuse.fuse_get_context() + ctx = ctxp.contents + return ctx.uid, ctx.gid, ctx.pid + + +class FUSE(object): + """This class is the lower level interface and should not be subclassed + under normal use. Its methods are called by fuse. + Assumes API version 2.6 or later.""" + + def __init__(self, operations, mountpoint, raw_fi=False, **kwargs): + """Setting raw_fi to True will cause FUSE to pass the fuse_file_info + class as is to Operations, instead of just the fh field. + This gives you access to direct_io, keep_cache, etc.""" + + self.operations = operations + self.raw_fi = raw_fi + args = ['fuse'] + if kwargs.pop('foreground', False): + args.append('-f') + if kwargs.pop('debug', False): + args.append('-d') + if kwargs.pop('nothreads', False): + args.append('-s') + kwargs.setdefault('fsname', operations.__class__.__name__) + args.append('-o') + args.append(','.join(key if val == True else '%s=%s' % (key, val) + for key, val in kwargs.items())) + args.append(mountpoint) + argv = (c_char_p * len(args))(*args) + + fuse_ops = fuse_operations() + for name, prototype in fuse_operations._fields_: + if prototype != c_voidp and getattr(operations, name, None): + op = partial(self._wrapper_, getattr(self, name)) + setattr(fuse_ops, name, prototype(op)) + _libfuse.fuse_main_real(len(args), argv, pointer(fuse_ops), + sizeof(fuse_ops), None) + del self.operations # Invoke the destructor + + def _wrapper_(self, func, *args, **kwargs): + """Decorator for the methods that follow""" + try: + return func(*args, **kwargs) or 0 + except OSError as e: + return -(e.errno or EFAULT) + except: + print_exc() + return -EFAULT + + def getattr(self, path, buf): + return self.fgetattr(path, buf, None) + + def readlink(self, path, buf, bufsize): + ret = self.operations('readlink', path).encode('utf-8') + data = create_string_buffer(ret[:bufsize - 1]) + memmove(buf, data, len(data)) + return 0 + + def mknod(self, path, mode, dev): + return self.operations('mknod', path, mode, dev) + + def mkdir(self, path, mode): + return self.operations('mkdir', path, mode) + + def unlink(self, path): + return self.operations('unlink', path) + + def rmdir(self, path): + return self.operations('rmdir', path) + + def symlink(self, source, target): + return self.operations('symlink', target, source) + + def rename(self, old, new): + return self.operations('rename', old, new) + + def link(self, source, target): + return self.operations('link', target, source) + + def chmod(self, path, mode): + return self.operations('chmod', path, mode) + + def chown(self, path, uid, gid): + return self.operations('chown', path, uid, gid) + + def truncate(self, path, length): + return self.operations('truncate', path, length) + + def open(self, path, fip): + fi = fip.contents + if self.raw_fi: + return self.operations('open', path, fi) + else: + fi.fh = self.operations('open', path, fi.flags) + return 0 + + def read(self, path, buf, size, offset, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + ret = self.operations('read', path, size, offset, fh) + if not ret: + return 0 + data = create_string_buffer(ret[:size], size) + memmove(buf, data, size) + return size + + def write(self, path, buf, size, offset, fip): + data = string_at(buf, size) + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('write', path, data, offset, fh) + + def statfs(self, path, buf): + stv = buf.contents + attrs = self.operations('statfs', path) + for key, val in attrs.items(): + if hasattr(stv, key): + setattr(stv, key, val) + return 0 + + def flush(self, path, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('flush', path, fh) + + def release(self, path, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('release', path, fh) + + def fsync(self, path, datasync, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('fsync', path, datasync, fh) + + def setxattr(self, path, name, value, size, options, *args): + data = string_at(value, size) + return self.operations('setxattr', path, name, data, options, *args) + + def getxattr(self, path, name, value, size, *args): + ret = self.operations('getxattr', path, name, *args) + retsize = len(ret) + buf = create_string_buffer(ret, retsize) # Does not add trailing 0 + if bool(value): + if retsize > size: + return -ERANGE + memmove(value, buf, retsize) + return retsize + + def listxattr(self, path, namebuf, size): + ret = self.operations('listxattr', path) + buf = create_string_buffer('\x00'.join(ret)) if ret else '' + bufsize = len(buf) + if bool(namebuf): + if bufsize > size: + return -ERANGE + memmove(namebuf, buf, bufsize) + return bufsize + + def removexattr(self, path, name): + return self.operations('removexattr', path, name) + + def opendir(self, path, fip): + # Ignore raw_fi + fip.contents.fh = self.operations('opendir', path) + return 0 + + def readdir(self, path, buf, filler, offset, fip): + # Ignore raw_fi + for item in self.operations('readdir', path, fip.contents.fh): + if isinstance(item, str): + name, st, offset = item, None, 0 + else: + name, attrs, offset = item + if attrs: + st = c_stat() + set_st_attrs(st, attrs) + else: + st = None + if filler(buf, name.encode('utf-8'), st, offset) != 0: + break + return 0 + + def releasedir(self, path, fip): + # Ignore raw_fi + return self.operations('releasedir', path, fip.contents.fh) + + def fsyncdir(self, path, datasync, fip): + # Ignore raw_fi + return self.operations('fsyncdir', path, datasync, fip.contents.fh) + + def init(self, conn): + return self.operations('init', '/') + + def destroy(self, private_data): + return self.operations('destroy', '/') + + def access(self, path, amode): + return self.operations('access', path, amode) + + def create(self, path, mode, fip): + fi = fip.contents + if self.raw_fi: + return self.operations('create', path, mode, fi) + else: + fi.fh = self.operations('create', path, mode) + return 0 + + def ftruncate(self, path, length, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('truncate', path, length, fh) + + def fgetattr(self, path, buf, fip): + memset(buf, 0, sizeof(c_stat)) + st = buf.contents + fh = fip and (fip.contents if self.raw_fi else fip.contents.fh) + attrs = self.operations('getattr', path, fh) + set_st_attrs(st, attrs) + return 0 + + def lock(self, path, fip, cmd, lock): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('lock', path, fh, cmd, lock) + + def utimens(self, path, buf): + if buf: + atime = time_of_timespec(buf.contents.actime) + mtime = time_of_timespec(buf.contents.modtime) + times = (atime, mtime) + else: + times = None + return self.operations('utimens', path, times) + + def bmap(self, path, blocksize, idx): + return self.operations('bmap', path, blocksize, idx) + + +class Operations(object): + """This class should be subclassed and passed as an argument to FUSE on + initialization. All operations should raise an OSError exception on + error. + + When in doubt of what an operation should do, check the FUSE header + file or the corresponding system call man page.""" + + def __call__(self, op, *args): + if not hasattr(self, op): + raise OSError(EFAULT, '') + return getattr(self, op)(*args) + + def access(self, path, amode): + return 0 + + bmap = None + + def chmod(self, path, mode): + raise OSError(EROFS, '') + + def chown(self, path, uid, gid): + raise OSError(EROFS, '') + + def create(self, path, mode, fi=None): + """When raw_fi is False (default case), fi is None and create should + return a numerical file handle. + When raw_fi is True the file handle should be set directly by create + and return 0.""" + raise OSError(EROFS, '') + + def destroy(self, path): + """Called on filesystem destruction. Path is always /""" + pass + + def flush(self, path, fh): + return 0 + + def fsync(self, path, datasync, fh): + return 0 + + def fsyncdir(self, path, datasync, fh): + return 0 + + def getattr(self, path, fh=None): + """Returns a dictionary with keys identical to the stat C structure + of stat(2). + st_atime, st_mtime and st_ctime should be floats. + NOTE: There is an incombatibility between Linux and Mac OS X concerning + st_nlink of directories. Mac OS X counts all files inside the directory, + while Linux counts only the subdirectories.""" + + if path != '/': + raise OSError(ENOENT, '') + return dict(st_mode=(S_IFDIR | 0o755), st_nlink=2) + + def getxattr(self, path, name, position=0): + raise OSError(ENOTSUP, '') + + def init(self, path): + """Called on filesystem initialization. Path is always / + Use it instead of __init__ if you start threads on initialization.""" + pass + + def link(self, target, source): + raise OSError(EROFS, '') + + def listxattr(self, path): + return [] + + lock = None + + def mkdir(self, path, mode): + raise OSError(EROFS, '') + + def mknod(self, path, mode, dev): + raise OSError(EROFS, '') + + def open(self, path, flags): + """When raw_fi is False (default case), open should return a numerical + file handle. + When raw_fi is True the signature of open becomes: + open(self, path, fi) + and the file handle should be set directly.""" + return 0 + + def opendir(self, path): + """Returns a numerical file handle.""" + return 0 + + def read(self, path, size, offset, fh): + """Returns a string containing the data requested.""" + raise OSError(ENOENT, '') + + def readdir(self, path, fh): + """Can return either a list of names, or a list of (name, attrs, offset) + tuples. attrs is a dict as in getattr.""" + return ['.', '..'] + + def readlink(self, path): + raise OSError(ENOENT, '') + + def release(self, path, fh): + return 0 + + def releasedir(self, path, fh): + return 0 + + def removexattr(self, path, name): + raise OSError(ENOTSUP, '') + + def rename(self, old, new): + raise OSError(EROFS, '') + + def rmdir(self, path): + raise OSError(EROFS, '') + + def setxattr(self, path, name, value, options, position=0): + raise OSError(ENOTSUP, '') + + def statfs(self, path): + """Returns a dictionary with keys identical to the statvfs C structure + of statvfs(3). + On Mac OS X f_bsize and f_frsize must be a power of 2 (minimum 512).""" + return {} + + def symlink(self, target, source): + raise OSError(EROFS, '') + + def truncate(self, path, length, fh=None): + raise OSError(EROFS, '') + + def unlink(self, path): + raise OSError(EROFS, '') + + def utimens(self, path, times=None): + """Times is a (atime, mtime) tuple. If None use current time.""" + return 0 + + def write(self, path, data, offset, fh): + raise OSError(EROFS, '') + + +class LoggingMixIn: + def __call__(self, op, path, *args): + logging.debug('-> %s %s %s', op, path, repr(args)) + ret = '[Unknown Error]' + try: + ret = getattr(self, op)(path, *args) + return ret + except OSError as e: + ret = str(e) + raise + finally: + logging.debug('<- %s %s', op, repr(ret)) diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/fusell.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/fusell.py new file mode 100644 index 00000000..d0bc25ea --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/fusell.py @@ -0,0 +1,619 @@ +# Copyright (c) 2010 Giorgos Verigakis +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import division + +from ctypes import * +from ctypes.util import find_library +from errno import * +from functools import partial, wraps +from inspect import getmembers, ismethod +from platform import machine, system +from stat import S_IFDIR, S_IFREG + + +_system = system() +_machine = machine() + +class LibFUSE(CDLL): + def __init__(self): + if _system == 'Darwin': + self.libiconv = CDLL(find_library('iconv'), RTLD_GLOBAL) + super(LibFUSE, self).__init__(find_library('fuse')) + + self.fuse_mount.argtypes = (c_char_p, POINTER(fuse_args)) + self.fuse_mount.restype = c_void_p + self.fuse_lowlevel_new.argtypes = (POINTER(fuse_args), POINTER(fuse_lowlevel_ops), + c_size_t, c_void_p) + self.fuse_lowlevel_new.restype = c_void_p + self.fuse_set_signal_handlers.argtypes = (c_void_p,) + self.fuse_session_add_chan.argtypes = (c_void_p, c_void_p) + self.fuse_session_loop.argtypes = (c_void_p,) + self.fuse_remove_signal_handlers.argtypes = (c_void_p,) + self.fuse_session_remove_chan.argtypes = (c_void_p,) + self.fuse_session_destroy.argtypes = (c_void_p,) + self.fuse_unmount.argtypes = (c_char_p, c_void_p) + + self.fuse_req_ctx.restype = POINTER(fuse_ctx) + self.fuse_req_ctx.argtypes = (fuse_req_t,) + + self.fuse_reply_err.argtypes = (fuse_req_t, c_int) + self.fuse_reply_attr.argtypes = (fuse_req_t, c_void_p, c_double) + self.fuse_reply_entry.argtypes = (fuse_req_t, c_void_p) + self.fuse_reply_open.argtypes = (fuse_req_t, c_void_p) + self.fuse_reply_buf.argtypes = (fuse_req_t, c_char_p, c_size_t) + self.fuse_reply_write.argtypes = (fuse_req_t, c_size_t) + + self.fuse_add_direntry.argtypes = (c_void_p, c_char_p, c_size_t, c_char_p, + c_stat_p, c_off_t) + +class fuse_args(Structure): + _fields_ = [('argc', c_int), ('argv', POINTER(c_char_p)), ('allocated', c_int)] + +class c_timespec(Structure): + _fields_ = [('tv_sec', c_long), ('tv_nsec', c_long)] + +class c_stat(Structure): + pass # Platform dependent + +if _system == 'Darwin': + ENOTSUP = 45 + c_dev_t = c_int32 + c_fsblkcnt_t = c_ulong + c_fsfilcnt_t = c_ulong + c_gid_t = c_uint32 + c_mode_t = c_uint16 + c_off_t = c_int64 + c_pid_t = c_int32 + c_uid_t = c_uint32 + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_uint32), + ('st_mode', c_mode_t), + ('st_nlink', c_uint16), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec), + ('st_size', c_off_t), + ('st_blocks', c_int64), + ('st_blksize', c_int32)] +elif _system == 'Linux': + ENOTSUP = 95 + c_dev_t = c_ulonglong + c_fsblkcnt_t = c_ulonglong + c_fsfilcnt_t = c_ulonglong + c_gid_t = c_uint + c_mode_t = c_uint + c_off_t = c_longlong + c_pid_t = c_int + c_uid_t = c_uint + + if _machine == 'x86_64': + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_ulong), + ('st_nlink', c_ulong), + ('st_mode', c_mode_t), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('__pad0', c_int), + ('st_rdev', c_dev_t), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_long), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec)] + elif _machine == 'ppc': + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_ulonglong), + ('st_mode', c_mode_t), + ('st_nlink', c_uint), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('__pad2', c_ushort), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_longlong), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec)] + else: + # i686, use as fallback for everything else + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('__pad1', c_ushort), + ('__st_ino', c_ulong), + ('st_mode', c_mode_t), + ('st_nlink', c_uint), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('__pad2', c_ushort), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_longlong), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec), + ('st_ino', c_ulonglong)] +else: + raise NotImplementedError('%s is not supported.' % _system) + +class c_statvfs(Structure): + _fields_ = [ + ('f_bsize', c_ulong), + ('f_frsize', c_ulong), + ('f_blocks', c_fsblkcnt_t), + ('f_bfree', c_fsblkcnt_t), + ('f_bavail', c_fsblkcnt_t), + ('f_files', c_fsfilcnt_t), + ('f_ffree', c_fsfilcnt_t), + ('f_favail', c_fsfilcnt_t)] + +class fuse_file_info(Structure): + _fields_ = [ + ('flags', c_int), + ('fh_old', c_ulong), + ('writepage', c_int), + ('direct_io', c_uint, 1), + ('keep_cache', c_uint, 1), + ('flush', c_uint, 1), + ('padding', c_uint, 29), + ('fh', c_uint64), + ('lock_owner', c_uint64)] + +class fuse_ctx(Structure): + _fields_ = [('uid', c_uid_t), ('gid', c_gid_t), ('pid', c_pid_t)] + +fuse_ino_t = c_ulong +fuse_req_t = c_void_p +c_stat_p = POINTER(c_stat) +fuse_file_info_p = POINTER(fuse_file_info) + +FUSE_SET_ATTR = ('st_mode', 'st_uid', 'st_gid', 'st_size', 'st_atime', 'st_mtime') + +class fuse_entry_param(Structure): + _fields_ = [ + ('ino', fuse_ino_t), + ('generation', c_ulong), + ('attr', c_stat), + ('attr_timeout', c_double), + ('entry_timeout', c_double)] + +class fuse_lowlevel_ops(Structure): + _fields_ = [ + ('init', CFUNCTYPE(None, c_void_p, c_void_p)), + ('destroy', CFUNCTYPE(None, c_void_p)), + ('lookup', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_char_p)), + ('forget', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_ulong)), + ('getattr', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, fuse_file_info_p)), + ('setattr', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_stat_p, c_int, fuse_file_info_p)), + ('readlink', CFUNCTYPE(None, fuse_req_t, fuse_ino_t)), + ('mknod', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_char_p, c_mode_t, c_dev_t)), + ('mkdir', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_char_p, c_mode_t)), + ('unlink', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_char_p)), + ('rmdir', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_char_p)), + ('symlink', CFUNCTYPE(None, fuse_req_t, c_char_p, fuse_ino_t, c_char_p)), + ('rename', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_char_p, fuse_ino_t, c_char_p)), + ('link', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, fuse_ino_t, c_char_p)), + ('open', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, fuse_file_info_p)), + ('read', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_size_t, c_off_t, fuse_file_info_p)), + ('write', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_char_p, c_size_t, c_off_t, + fuse_file_info_p)), + ('flush', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, fuse_file_info_p)), + ('release', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, fuse_file_info_p)), + ('fsync', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_int, fuse_file_info_p)), + ('opendir', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, fuse_file_info_p)), + ('readdir', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_size_t, c_off_t, fuse_file_info_p)), + ('releasedir', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, fuse_file_info_p)), + ('fsyncdir', CFUNCTYPE(None, fuse_req_t, fuse_ino_t, c_int, fuse_file_info_p))] + + +def struct_to_dict(p): + try: + x = p.contents + return dict((key, getattr(x, key)) for key, type in x._fields_) + except ValueError: + return {} + +def stat_to_dict(p): + try: + d = {} + x = p.contents + for key, type in x._fields_: + if key in ('st_atimespec', 'st_mtimespec', 'st_ctimespec'): + ts = getattr(x, key) + key = key[:-4] # Lose the "spec" + d[key] = ts.tv_sec + ts.tv_nsec / 10 ** 9 + else: + d[key] = getattr(x, key) + return d + except ValueError: + return {} + +def dict_to_stat(d): + for key in ('st_atime', 'st_mtime', 'st_ctime'): + if key in d: + val = d[key] + sec = int(val) + nsec = int((val - sec) * 10 ** 9) + d[key + 'spec'] = c_timespec(sec, nsec) + return c_stat(**d) + +def setattr_mask_to_list(mask): + return [FUSE_SET_ATTR[i] for i in range(len(FUSE_SET_ATTR)) if mask & (1 << i)] + +class FUSELL(object): + def __init__(self, mountpoint): + self.libfuse = LibFUSE() + + fuse_ops = fuse_lowlevel_ops() + + for name, prototype in fuse_lowlevel_ops._fields_: + method = getattr(self, 'fuse_' + name, None) or getattr(self, name, None) + if method: + setattr(fuse_ops, name, prototype(method)) + + args = ['fuse'] + argv = fuse_args(len(args), (c_char_p * len(args))(*args), 0) + + # TODO: handle initialization errors + + chan = self.libfuse.fuse_mount(mountpoint, argv) + assert chan + + session = self.libfuse.fuse_lowlevel_new(argv, byref(fuse_ops), sizeof(fuse_ops), None) + assert session + + err = self.libfuse.fuse_set_signal_handlers(session) + assert err == 0 + + self.libfuse.fuse_session_add_chan(session, chan) + + err = self.libfuse.fuse_session_loop(session) + assert err == 0 + + err = self.libfuse.fuse_remove_signal_handlers(session) + assert err == 0 + + self.libfuse.fuse_session_remove_chan(chan) + self.libfuse.fuse_session_destroy(session) + self.libfuse.fuse_unmount(mountpoint, chan) + + def reply_err(self, req, err): + return self.libfuse.fuse_reply_err(req, err) + + def reply_none(self, req): + self.libfuse.fuse_reply_none(req) + + def reply_entry(self, req, entry): + entry['attr'] = c_stat(**entry['attr']) + e = fuse_entry_param(**entry) + self.libfuse.fuse_reply_entry(req, byref(e)) + + def reply_create(self, req, *args): + pass # XXX + + def reply_attr(self, req, attr, attr_timeout): + st = dict_to_stat(attr) + return self.libfuse.fuse_reply_attr(req, byref(st), c_double(attr_timeout)) + + def reply_readlink(self, req, *args): + pass # XXX + + def reply_open(self, req, d): + fi = fuse_file_info(**d) + return self.libfuse.fuse_reply_open(req, byref(fi)) + + def reply_write(self, req, count): + return self.libfuse.fuse_reply_write(req, count) + + def reply_buf(self, req, buf): + return self.libfuse.fuse_reply_buf(req, buf, len(buf)) + + def reply_readdir(self, req, size, off, entries): + bufsize = 0 + sized_entries = [] + for name, attr in entries: + entsize = self.libfuse.fuse_add_direntry(req, None, 0, name, None, 0) + sized_entries.append((name, attr, entsize)) + bufsize += entsize + + next = 0 + buf = create_string_buffer(bufsize) + for name, attr, entsize in sized_entries: + entbuf = cast(addressof(buf) + next, c_char_p) + st = c_stat(**attr) + next += entsize + self.libfuse.fuse_add_direntry(req, entbuf, entsize, name, byref(st), next) + + if off < bufsize: + buf = cast(addressof(buf) + off, c_char_p) if off else buf + return self.libfuse.fuse_reply_buf(req, buf, min(bufsize - off, size)) + else: + return self.libfuse.fuse_reply_buf(req, None, 0) + + + # If you override the following methods you should reply directly + # with the self.libfuse.fuse_reply_* methods. + + def fuse_getattr(self, req, ino, fi): + self.getattr(req, ino, struct_to_dict(fi)) + + def fuse_setattr(self, req, ino, attr, to_set, fi): + attr_dict = stat_to_dict(attr) + to_set_list = setattr_mask_to_list(to_set) + fi_dict = struct_to_dict(fi) + self.setattr(req, ino, attr_dict, to_set_list, fi_dict) + + def fuse_open(self, req, ino, fi): + self.open(req, ino, struct_to_dict(fi)) + + def fuse_read(self, req, ino, size, off, fi): + self.read(req, ino, size, off, fi) + + def fuse_write(self, req, ino, buf, size, off, fi): + buf_str = string_at(buf, size) + fi_dict = struct_to_dict(fi) + self.write(req, ino, buf_str, off, fi_dict) + + def fuse_flush(self, req, ino, fi): + self.flush(req, ino, struct_to_dict(fi)) + + def fuse_release(self, req, ino, fi): + self.release(req, ino, struct_to_dict(fi)) + + def fuse_fsync(self, req, ino, datasync, fi): + self.fsyncdir(req, ino, datasync, struct_to_dict(fi)) + + def fuse_opendir(self, req, ino, fi): + self.opendir(req, ino, struct_to_dict(fi)) + + def fuse_readdir(self, req, ino, size, off, fi): + self.readdir(req, ino, size, off, struct_to_dict(fi)) + + def fuse_releasedir(self, req, ino, fi): + self.releasedir(req, ino, struct_to_dict(fi)) + + def fuse_fsyncdir(self, req, ino, datasync, fi): + self.fsyncdir(req, ino, datasync, struct_to_dict(fi)) + + + # Utility methods + + def req_ctx(self, req): + ctx = self.libfuse.fuse_req_ctx(req) + return struct_to_dict(ctx) + + + # Methods to be overridden in subclasses. + # Reply with the self.reply_* methods. + + def init(self, userdata, conn): + """Initialize filesystem + + There's no reply to this method + """ + pass + + def destroy(self, userdata): + """Clean up filesystem + + There's no reply to this method + """ + pass + + def lookup(self, req, parent, name): + """Look up a directory entry by name and get its attributes. + + Valid replies: + reply_entry + reply_err + """ + self.reply_err(req, ENOENT) + + def forget(self, req, ino, nlookup): + """Forget about an inode + + Valid replies: + reply_none + """ + self.reply_none(req) + + def getattr(self, req, ino, fi): + """Get file attributes + + Valid replies: + reply_attr + reply_err + """ + if ino == 1: + attr = {'st_ino': 1, 'st_mode': S_IFDIR | 0755, 'st_nlink': 2} + self.reply_attr(req, attr, 1.0) + else: + self.reply_err(req, ENOENT) + + def setattr(self, req, ino, attr, to_set, fi): + """Set file attributes + + Valid replies: + reply_attr + reply_err + """ + self.reply_err(req, EROFS) + + def readlink(self, req, ino): + """Read symbolic link + + Valid replies: + reply_readlink + reply_err + """ + self.reply_err(req, ENOENT) + + def mknod(self, req, parent, name, mode, rdev): + """Create file node + + Valid replies: + reply_entry + reply_err + """ + self.reply_err(req, EROFS) + + def mkdir(self, req, parent, name, mode): + """Create a directory + + Valid replies: + reply_entry + reply_err + """ + self.reply_err(req, EROFS) + + def unlink(self, req, parent, name): + """Remove a file + + Valid replies: + reply_err + """ + self.reply_err(req, EROFS) + + def rmdir(self, req, parent, name): + """Remove a directory + + Valid replies: + reply_err + """ + self.reply_err(req, EROFS) + + def symlink(self, req, link, parent, name): + """Create a symbolic link + + Valid replies: + reply_entry + reply_err + """ + self.reply_err(req, EROFS) + + def rename(self, req, parent, name, newparent, newname): + """Rename a file + + Valid replies: + reply_err + """ + self.reply_err(req, EROFS) + + def link(self, req, ino, newparent, newname): + """Create a hard link + + Valid replies: + reply_entry + reply_err + """ + self.reply_err(req, EROFS) + + def open(self, req, ino, fi): + """Open a file + + Valid replies: + reply_open + reply_err + """ + self.reply_open(req, fi) + + def read(self, req, ino, size, off, fi): + """Read data + + Valid replies: + reply_buf + reply_err + """ + self.reply_err(req, EIO) + + def write(self, req, ino, buf, off, fi): + """Write data + + Valid replies: + reply_write + reply_err + """ + self.reply_err(req, EROFS) + + def flush(self, req, ino, fi): + """Flush method + + Valid replies: + reply_err + """ + self.reply_err(req, 0) + + def release(self, req, ino, fi): + """Release an open file + + Valid replies: + reply_err + """ + self.reply_err(req, 0) + + def fsync(self, req, ino, datasync, fi): + """Synchronize file contents + + Valid replies: + reply_err + """ + self.reply_err(req, 0) + + def opendir(self, req, ino, fi): + """Open a directory + + Valid replies: + reply_open + reply_err + """ + self.reply_open(req, fi) + + def readdir(self, req, ino, size, off, fi): + """Read directory + + Valid replies: + reply_readdir + reply_err + """ + if ino == 1: + attr = {'st_ino': 1, 'st_mode': S_IFDIR} + entries = [('.', attr), ('..', attr)] + self.reply_readdir(req, size, off, entries) + else: + self.reply_err(req, ENOENT) + + def releasedir(self, req, ino, fi): + """Release an open directory + + Valid replies: + reply_err + """ + self.reply_err(req, 0) + + def fsyncdir(self, req, ino, datasync, fi): + """Synchronize directory contents + + Valid replies: + reply_err + """ + self.reply_err(req, 0) \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/loopback.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/loopback.py new file mode 100755 index 00000000..5ce16ed1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/loopback.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python + +from __future__ import with_statement + +from errno import EACCES +from os.path import realpath +from sys import argv, exit +from threading import Lock + +import os + +from fuse import FUSE, FuseOSError, Operations, LoggingMixIn + + +class Loopback(LoggingMixIn, Operations): + def __init__(self, root): + self.root = realpath(root) + self.rwlock = Lock() + + def __call__(self, op, path, *args): + return super(Loopback, self).__call__(op, self.root + path, *args) + + def access(self, path, mode): + if not os.access(path, mode): + raise FuseOSError(EACCES) + + chmod = os.chmod + chown = os.chown + + def create(self, path, mode): + return os.open(path, os.O_WRONLY | os.O_CREAT, mode) + + def flush(self, path, fh): + return os.fsync(fh) + + def fsync(self, path, datasync, fh): + return os.fsync(fh) + + def getattr(self, path, fh=None): + st = os.lstat(path) + return dict((key, getattr(st, key)) for key in ('st_atime', 'st_ctime', + 'st_gid', 'st_mode', 'st_mtime', 'st_nlink', 'st_size', 'st_uid')) + + getxattr = None + + def link(self, target, source): + return os.link(source, target) + + listxattr = None + mkdir = os.mkdir + mknod = os.mknod + open = os.open + + def read(self, path, size, offset, fh): + with self.rwlock: + os.lseek(fh, offset, 0) + return os.read(fh, size) + + def readdir(self, path, fh): + return ['.', '..'] + os.listdir(path) + + readlink = os.readlink + + def release(self, path, fh): + return os.close(fh) + + def rename(self, old, new): + return os.rename(old, self.root + new) + + rmdir = os.rmdir + + def statfs(self, path): + stv = os.statvfs(path) + return dict((key, getattr(stv, key)) for key in ('f_bavail', 'f_bfree', + 'f_blocks', 'f_bsize', 'f_favail', 'f_ffree', 'f_files', 'f_flag', + 'f_frsize', 'f_namemax')) + + def symlink(self, target, source): + return os.symlink(source, target) + + def truncate(self, path, length, fh=None): + with open(path, 'r+') as f: + f.truncate(length) + + unlink = os.unlink + utimens = os.utime + + def write(self, path, data, offset, fh): + with self.rwlock: + os.lseek(fh, offset, 0) + return os.write(fh, data) + + +if __name__ == "__main__": + if len(argv) != 3: + print 'usage: %s ' % argv[0] + exit(1) + fuse = FUSE(Loopback(argv[1]), argv[2], foreground=True) \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/.project b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/.project new file mode 100644 index 00000000..929b4087 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/.project @@ -0,0 +1,17 @@ + + + llfuse + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/.pydevproject b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/.pydevproject new file mode 100644 index 00000000..e7f99ef6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/.pydevproject @@ -0,0 +1,10 @@ + + + + +python 2.6 +Default + +/llfuse + + diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/README.txt b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/README.txt new file mode 100644 index 00000000..03f4df0f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/README.txt @@ -0,0 +1,22 @@ +Note that the low-level API needs to generate the Python interface +to the local FUSE library before it can be used. For that, +you have to have both the FUSE headers and the GCC-XML +(http://www.gccxml.org) compiler installed. + +The interface is generated by running + +# python setup.py build_ctypes + +this will create the file llfuse/ctypes_api.py + +Please keep in mind that it's probably not wise to ship this file +with your application, because it has been generated for your +system only. + + +Note that the fuse_daemonize() function is deliberately not exported +by this module. If you want to daemonize a Python process, you have to +do so from within Python or you will get into trouble. See + - http://bugs.python.org/issue7931 + - http://www.python.org/dev/peps/pep-3143/ + \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/ctypeslib.zip b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/ctypeslib.zip new file mode 100644 index 0000000000000000000000000000000000000000..f125d984f4bd6ed9941f40fec4220f658a6a8635 GIT binary patch literal 135190 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 +#include +#include + diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse/__init__.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse/__init__.py new file mode 100644 index 00000000..589e2462 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse/__init__.py @@ -0,0 +1,24 @@ +''' +$Id: __init__.py 47 2010-01-29 17:11:23Z nikratio $ + +Copyright (c) 2010, Nikolaus Rath +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the main author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' + +from __future__ import division, print_function, absolute_import + +__all__ = [ 'ctypes_api', 'interface', 'operations' ] + +# Wildcard imports desired +#pylint: disable-msg=W0401 +from llfuse.operations import * +from llfuse.interface import * + diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse/interface.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse/interface.py new file mode 100644 index 00000000..07dc84aa --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse/interface.py @@ -0,0 +1,897 @@ +''' +$Id: interface.py 54 2010-02-22 02:33:10Z nikratio $ + +Copyright (c) 2010, Nikolaus Rath +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the main author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +This module defines the interface between the FUSE C and Python API. The actual file system +is implemented as an `Operations` instance whose methods will +be called by the fuse library. + +Note that all "string-like" quantities (e.g. file names, extended attribute names & values) are +represented as bytes, since POSIX doesn't require any of them to be valid unicode strings. + + +Exception Handling +------------------ + +Since Python exceptions cannot be forwarded to the FUSE kernel module, +the FUSE Python API catches all exceptions that are generated during +request processing. + +If the exception is of type `FUSEError`, the appropriate errno is returned +to the kernel module and the exception is discarded. + +For any other exceptions, a warning is logged and a generic error signaled +to the kernel module. Then the `handle_exc` method of the `Operations` +instance is called, so that the file system itself has a chance to react +to the problem (e.g. by marking the file system as needing a check). + +The return value and any raised exceptions of `handle_exc` are ignored. + +''' + +# Since we are using ctype Structures, we often have to +# access attributes that are not defined in __init__ +# (since they are defined in _fields_ instead) +#pylint: disable-msg=W0212 + +# We need globals +#pylint: disable-msg=W0603 + +from __future__ import division, print_function, absolute_import + +# Using .. as libfuse makes PyDev really unhappy. +from . import ctypes_api +libfuse = ctypes_api + +from ctypes import c_char_p, sizeof, create_string_buffer, addressof, string_at, POINTER, c_char, cast +from functools import partial +import errno +import logging +import sys + + +__all__ = [ 'FUSEError', 'ENOATTR', 'ENOTSUP', 'init', 'main', 'close', + 'fuse_version' ] + + +# These should really be defined in the errno module, but +# unfortunately they are missing +ENOATTR = libfuse.ENOATTR +ENOTSUP = libfuse.ENOTSUP + +log = logging.getLogger("fuse") + +# Init globals +operations = None +fuse_ops = None +mountpoint = None +session = None +channel = None + +class DiscardedRequest(Exception): + '''Request was interrupted and reply discarded. + + ''' + + pass + +class ReplyError(Exception): + '''Unable to send reply to fuse kernel module. + + ''' + + pass + +class FUSEError(Exception): + '''Wrapped errno value to be returned to the fuse kernel module + + This exception can store only an errno. Request handlers should raise + to return a specific errno to the fuse kernel module. + ''' + + __slots__ = [ 'errno' ] + + def __init__(self, errno_): + super(FUSEError, self).__init__() + self.errno = errno_ + + def __str__(self): + # errno may not have strings for all error codes + return errno.errorcode.get(self.errno, str(self.errno)) + + + +def check_reply_result(result, func, *args): + '''Check result of a call to a fuse_reply_* foreign function + + If `result` is 0, it is assumed that the call succeeded and the function does nothing. + + If result is `-errno.ENOENT`, this means that the request has been discarded and `DiscardedRequest` + is raised. + + In all other cases, `ReplyError` is raised. + + (We do not try to call `fuse_reply_err` or any other reply method as well, because the first reply + function may have already invalidated the `req` object and it seems better to (possibly) let the + request pend than to crash the server application.) + ''' + + if result == 0: + return None + + elif result == -errno.ENOENT: + raise DiscardedRequest() + + elif result > 0: + raise ReplyError('Foreign function %s returned unexpected value %d' + % (func.name, result)) + elif result < 0: + raise ReplyError('Foreign function %s returned error %s' + % (func.name, errno.errorcode.get(-result, str(-result)))) + + +# +# Set return checker for common ctypes calls +# +reply_functions = [ 'fuse_reply_err', 'fuse_reply_entry', + 'fuse_reply_create', 'fuse_reply_readlink', 'fuse_reply_open', + 'fuse_reply_write', 'fuse_reply_attr', 'fuse_reply_buf', + 'fuse_reply_iov', 'fuse_reply_statfs', 'fuse_reply_xattr', + 'fuse_reply_lock' ] +for fname in reply_functions: + getattr(libfuse, fname).errcheck = check_reply_result + + # Name isn't stored by ctypes + getattr(libfuse, fname).name = fname + + +def dict_to_entry(attr): + '''Convert dict to fuse_entry_param''' + + entry = libfuse.fuse_entry_param() + + entry.ino = attr['st_ino'] + entry.generation = attr.pop('generation') + entry.entry_timeout = attr.pop('entry_timeout') + entry.attr_timeout = attr.pop('attr_timeout') + + entry.attr = dict_to_stat(attr) + + return entry + +def dict_to_stat(attr): + '''Convert dict to struct stat''' + + stat = libfuse.stat() + + # Determine correct way to store times + if hasattr(stat, 'st_atim'): # Linux + get_timespec_key = lambda key: key[:-1] + elif hasattr(stat, 'st_atimespec'): # FreeBSD + get_timespec_key = lambda key: key + 'spec' + else: + get_timespec_key = False + + # Raises exception if there are any unknown keys + for (key, val) in attr.iteritems(): + if val is None: # do not set undefined items + continue + if get_timespec_key and key in ('st_atime', 'st_mtime', 'st_ctime'): + key = get_timespec_key(key) + spec = libfuse.timespec() + spec.tv_sec = int(val) + spec.tv_nsec = int((val - int(val)) * 10 ** 9) + val = spec + setattr(stat, key, val) + + return stat + + +def stat_to_dict(stat): + '''Convert ``struct stat`` to dict''' + + attr = dict() + for (name, dummy) in libfuse.stat._fields_: + if name.startswith('__'): + continue + + if name in ('st_atim', 'st_mtim', 'st_ctim'): + key = name + 'e' + attr[key] = getattr(stat, name).tv_sec + getattr(stat, name).tv_nsec / 10 ** 9 + elif name in ('st_atimespec', 'st_mtimespec', 'st_ctimespec'): + key = name[:-4] + attr[key] = getattr(stat, name).tv_sec + getattr(stat, name).tv_nsec / 10 ** 9 + else: + attr[name] = getattr(stat, name) + + return attr + + +def op_wrapper(func, req, *args): + '''Catch all exceptions and call fuse_reply_err instead''' + + try: + func(req, *args) + except FUSEError as e: + log.debug('op_wrapper caught FUSEError, calling fuse_reply_err(%s)', + errno.errorcode.get(e.errno, str(e.errno))) + try: + libfuse.fuse_reply_err(req, e.errno) + except DiscardedRequest: + pass + except Exception as exc: + log.exception('FUSE handler raised exception.') + + # Report error to filesystem + if hasattr(operations, 'handle_exc'): + try: + operations.handle_exc(exc) + except: + pass + + # Send error reply, unless the error occured when replying + if not isinstance(exc, ReplyError): + log.debug('Calling fuse_reply_err(EIO)') + libfuse.fuse_reply_err(req, errno.EIO) + +def fuse_version(): + '''Return version of loaded fuse library''' + + return libfuse.fuse_version() + + +def init(operations_, mountpoint_, args): + '''Initialize and mount FUSE file system + + `operations_` has to be an instance of the `Operations` class (or another + class defining the same methods). + + `args` has to be a list of strings. Valid options are listed in struct fuse_opt fuse_mount_opts[] + (mount.c:68) and struct fuse_opt fuse_ll_opts[] (fuse_lowlevel_c:1526). + ''' + + log.debug('Initializing llfuse') + + global operations + global fuse_ops + global mountpoint + global session + global channel + + # Give operations instance a chance to check and change + # the FUSE options + operations_.check_args(args) + + mountpoint = mountpoint_ + operations = operations_ + fuse_ops = libfuse.fuse_lowlevel_ops() + fuse_args = make_fuse_args(args) + + # Init fuse_ops + module = globals() + for (name, prototype) in libfuse.fuse_lowlevel_ops._fields_: + if hasattr(operations, name): + method = partial(op_wrapper, module['fuse_' + name]) + setattr(fuse_ops, name, prototype(method)) + + log.debug('Calling fuse_mount') + channel = libfuse.fuse_mount(mountpoint, fuse_args) + if not channel: + raise RuntimeError('fuse_mount failed') + try: + log.debug('Calling fuse_lowlevel_new') + session = libfuse.fuse_lowlevel_new(fuse_args, fuse_ops, sizeof(fuse_ops), None) + if not session: + raise RuntimeError("fuse_lowlevel_new() failed") + try: + log.debug('Calling fuse_set_signal_handlers') + if libfuse.fuse_set_signal_handlers(session) == -1: + raise RuntimeError("fuse_set_signal_handlers() failed") + try: + log.debug('Calling fuse_session_add_chan') + libfuse.fuse_session_add_chan(session, channel) + session = session + channel = channel + return + + except: + log.debug('Calling fuse_remove_signal_handlers') + libfuse.fuse_remove_signal_handlers(session) + raise + + except: + log.debug('Calling fuse_session_destroy') + libfuse.fuse_session_destroy(session) + raise + except: + log.debug('Calling fuse_unmount') + libfuse.fuse_unmount(mountpoint, channel) + raise + +def make_fuse_args(args): + '''Create fuse_args Structure for given mount options''' + + args1 = [ sys.argv[0] ] + for opt in args: + args1.append(b'-o') + args1.append(opt) + + # Init fuse_args struct + fuse_args = libfuse.fuse_args() + fuse_args.allocated = 0 + fuse_args.argc = len(args1) + fuse_args.argv = (POINTER(c_char) * len(args1))(*[cast(c_char_p(x), POINTER(c_char)) + for x in args1]) + return fuse_args + +def main(single=False): + '''Run FUSE main loop''' + + if not session: + raise RuntimeError('Need to call init() before main()') + + if single: + log.debug('Calling fuse_session_loop') + if libfuse.fuse_session_loop(session) != 0: + raise RuntimeError("fuse_session_loop() failed") + else: + log.debug('Calling fuse_session_loop_mt') + if libfuse.fuse_session_loop_mt(session) != 0: + raise RuntimeError("fuse_session_loop_mt() failed") + +def close(): + '''Unmount file system and clean up''' + + global operations + global fuse_ops + global mountpoint + global session + global channel + + log.debug('Calling fuse_session_remove_chan') + libfuse.fuse_session_remove_chan(channel) + log.debug('Calling fuse_remove_signal_handlers') + libfuse.fuse_remove_signal_handlers(session) + log.debug('Calling fuse_session_destroy') + libfuse.fuse_session_destroy(session) + log.debug('Calling fuse_unmount') + libfuse.fuse_unmount(mountpoint, channel) + + operations = None + fuse_ops = None + mountpoint = None + session = None + channel = None + + +def fuse_lookup(req, parent_inode, name): + '''Look up a directory entry by name and get its attributes''' + + log.debug('Handling lookup(%d, %s)', parent_inode, string_at(name)) + + attr = operations.lookup(parent_inode, string_at(name)) + entry = dict_to_entry(attr) + + log.debug('Calling fuse_reply_entry') + try: + libfuse.fuse_reply_entry(req, entry) + except DiscardedRequest: + pass + +def fuse_init(userdata_p, conn_info_p): + '''Initialize Operations''' + operations.init() + +def fuse_destroy(userdata_p): + '''Cleanup Operations''' + operations.destroy() + +def fuse_getattr(req, ino, _unused): + '''Get attributes for `ino`''' + + log.debug('Handling getattr(%d)', ino) + + attr = operations.getattr(ino) + + attr_timeout = attr.pop('attr_timeout') + stat = dict_to_stat(attr) + + log.debug('Calling fuse_reply_attr') + try: + libfuse.fuse_reply_attr(req, stat, attr_timeout) + except DiscardedRequest: + pass + +def fuse_access(req, ino, mask): + '''Check if calling user has `mask` rights for `ino`''' + + log.debug('Handling access(%d, %o)', ino, mask) + + # Get UID + ctx = libfuse.fuse_req_ctx(req).contents + + # Define a function that returns a list of the GIDs + def get_gids(): + # Get GID list if FUSE supports it + # Weird syntax to prevent PyDev from complaining + getgroups = getattr(libfuse, "fuse_req_getgroups") + gid_t = getattr(libfuse, 'gid_t') + no = 10 + buf = (gid_t * no)(range(no)) + ret = getgroups(req, no, buf) + if ret > no: + no = ret + buf = (gid_t * no)(range(no)) + ret = getgroups(req, no, buf) + + return [ buf[i].value for i in range(ret) ] + + ret = operations.access(ino, mask, ctx, get_gids) + + log.debug('Calling fuse_reply_err') + try: + if ret: + libfuse.fuse_reply_err(req, 0) + else: + libfuse.fuse_reply_err(req, errno.EPERM) + except DiscardedRequest: + pass + + +def fuse_create(req, ino_parent, name, mode, fi): + '''Create and open a file''' + + log.debug('Handling create(%d, %s, %o)', ino_parent, string_at(name), mode) + (fh, attr) = operations.create(ino_parent, string_at(name), mode, + libfuse.fuse_req_ctx(req).contents) + fi.contents.fh = fh + fi.contents.keep_cache = 1 + entry = dict_to_entry(attr) + + log.debug('Calling fuse_reply_create') + try: + libfuse.fuse_reply_create(req, entry, fi) + except DiscardedRequest: + operations.release(fh) + + +def fuse_flush(req, ino, fi): + '''Handle close() system call + + May be called multiple times for the same open file. + ''' + + log.debug('Handling flush(%d)', fi.contents.fh) + operations.flush(fi.contents.fh) + log.debug('Calling fuse_reply_err(0)') + try: + libfuse.fuse_reply_err(req, 0) + except DiscardedRequest: + pass + + +def fuse_fsync(req, ino, datasync, fi): + '''Flush buffers for `ino` + + If the datasync parameter is non-zero, then only the user data + is flushed (and not the meta data). + ''' + + log.debug('Handling fsync(%d, %s)', fi.contents.fh, datasync != 0) + operations.fsync(fi.contents.fh, datasync != 0) + log.debug('Calling fuse_reply_err(0)') + try: + libfuse.fuse_reply_err(req, 0) + except DiscardedRequest: + pass + + +def fuse_fsyncdir(req, ino, datasync, fi): + '''Synchronize directory contents + + If the datasync parameter is non-zero, then only the directory contents + are flushed (and not the meta data about the directory itself). + ''' + + log.debug('Handling fsyncdir(%d, %s)', fi.contents.fh, datasync != 0) + operations.fsyncdir(fi.contents.fh, datasync != 0) + log.debug('Calling fuse_reply_err(0)') + try: + libfuse.fuse_reply_err(req, 0) + except DiscardedRequest: + pass + + +def fuse_getxattr(req, ino, name, size): + '''Get an extended attribute. + ''' + + log.debug('Handling getxattr(%d, %r, %d)', ino, string_at(name), size) + val = operations.getxattr(ino, string_at(name)) + if not isinstance(val, bytes): + raise TypeError("getxattr return value must be of type bytes") + + try: + if size == 0: + log.debug('Calling fuse_reply_xattr') + libfuse.fuse_reply_xattr(req, len(val)) + elif size >= len(val): + log.debug('Calling fuse_reply_buf') + libfuse.fuse_reply_buf(req, val, len(val)) + else: + raise FUSEError(errno.ERANGE) + except DiscardedRequest: + pass + + +def fuse_link(req, ino, new_parent_ino, new_name): + '''Create a hard link''' + + log.debug('Handling fuse_link(%d, %d, %s)', ino, new_parent_ino, string_at(new_name)) + attr = operations.link(ino, new_parent_ino, string_at(new_name)) + entry = dict_to_entry(attr) + + log.debug('Calling fuse_reply_entry') + try: + libfuse.fuse_reply_entry(req, entry) + except DiscardedRequest: + pass + +def fuse_listxattr(req, inode, size): + '''List extended attributes for `inode`''' + + log.debug('Handling listxattr(%d)', inode) + names = operations.listxattr(inode) + + if not all([ isinstance(name, bytes) for name in names]): + raise TypeError("listxattr return value must be list of bytes") + + # Size of the \0 separated buffer + act_size = (len(names) - 1) + sum([ len(name) for name in names ]) + + if size == 0: + try: + log.debug('Calling fuse_reply_xattr') + libfuse.fuse_reply_xattr(req, len(names)) + except DiscardedRequest: + pass + + elif act_size > size: + raise FUSEError(errno.ERANGE) + + else: + try: + log.debug('Calling fuse_reply_buf') + libfuse.fuse_reply_buf(req, b'\0'.join(names), act_size) + except DiscardedRequest: + pass + + +def fuse_mkdir(req, inode_parent, name, mode): + '''Create directory''' + + log.debug('Handling mkdir(%d, %s, %o)', inode_parent, string_at(name), mode) + attr = operations.mkdir(inode_parent, string_at(name), mode, + libfuse.fuse_req_ctx(req).contents) + entry = dict_to_entry(attr) + + log.debug('Calling fuse_reply_entry') + try: + libfuse.fuse_reply_entry(req, entry) + except DiscardedRequest: + pass + +def fuse_mknod(req, inode_parent, name, mode, rdev): + '''Create (possibly special) file''' + + log.debug('Handling mknod(%d, %s, %o, %d)', inode_parent, string_at(name), + mode, rdev) + attr = operations.mknod(inode_parent, string_at(name), mode, rdev, + libfuse.fuse_req_ctx(req).contents) + entry = dict_to_entry(attr) + + log.debug('Calling fuse_reply_entry') + try: + libfuse.fuse_reply_entry(req, entry) + except DiscardedRequest: + pass + +def fuse_open(req, inode, fi): + '''Open a file''' + log.debug('Handling open(%d, %d)', inode, fi.contents.flags) + fi.contents.fh = operations.open(inode, fi.contents.flags) + fi.contents.keep_cache = 1 + + log.debug('Calling fuse_reply_open') + try: + libfuse.fuse_reply_open(req, fi) + except DiscardedRequest: + operations.release(inode, fi.contents.fh) + +def fuse_opendir(req, inode, fi): + '''Open a directory''' + + log.debug('Handling opendir(%d)', inode) + fi.contents.fh = operations.opendir(inode) + + log.debug('Calling fuse_reply_open') + try: + libfuse.fuse_reply_open(req, fi) + except DiscardedRequest: + operations.releasedir(fi.contents.fh) + + +def fuse_read(req, ino, size, off, fi): + '''Read data from file''' + + log.debug('Handling read(ino=%d, off=%d, size=%d)', fi.contents.fh, off, size) + data = operations.read(fi.contents.fh, off, size) + + if not isinstance(data, bytes): + raise TypeError("read() must return bytes") + + if len(data) > size: + raise ValueError('read() must not return more than `size` bytes') + + log.debug('Calling fuse_reply_buf') + try: + libfuse.fuse_reply_buf(req, data, len(data)) + except DiscardedRequest: + pass + + +def fuse_readlink(req, inode): + '''Read target of symbolic link''' + + log.debug('Handling readlink(%d)', inode) + target = operations.readlink(inode) + log.debug('Calling fuse_reply_readlink') + try: + libfuse.fuse_reply_readlink(req, target) + except DiscardedRequest: + pass + + +def fuse_readdir(req, ino, bufsize, off, fi): + '''Read directory entries''' + + log.debug('Handling readdir(%d, %d, %d, %d)', ino, bufsize, off, fi.contents.fh) + + # Collect as much entries as we can return + entries = list() + size = 0 + for (name, attr) in operations.readdir(fi.contents.fh, off): + if not isinstance(name, bytes): + raise TypeError("readdir() must return entry names as bytes") + + stat = dict_to_stat(attr) + + entry_size = libfuse.fuse_add_direntry(req, None, 0, name, stat, 0) + if size + entry_size > bufsize: + break + + entries.append((name, stat)) + size += entry_size + + log.debug('Gathered %d entries, total size %d', len(entries), size) + + # If there are no entries left, return empty buffer + if not entries: + try: + log.debug('Calling fuse_reply_buf') + libfuse.fuse_reply_buf(req, None, 0) + except DiscardedRequest: + pass + return + + # Create and fill buffer + log.debug('Adding entries to buffer') + buf = create_string_buffer(size) + next_ = off + addr_off = 0 + for (name, stat) in entries: + next_ += 1 + addr_off += libfuse.fuse_add_direntry(req, cast(addressof(buf) + addr_off, POINTER(c_char)), + bufsize, name, stat, next_) + + # Return buffer + log.debug('Calling fuse_reply_buf') + try: + libfuse.fuse_reply_buf(req, buf, size) + except DiscardedRequest: + pass + + +def fuse_release(req, inode, fi): + '''Release open file''' + + log.debug('Handling release(%d)', fi.contents.fh) + operations.release(fi.contents.fh) + log.debug('Calling fuse_reply_err(0)') + try: + libfuse.fuse_reply_err(req, 0) + except DiscardedRequest: + pass + +def fuse_releasedir(req, inode, fi): + '''Release open directory''' + + log.debug('Handling releasedir(%d)', fi.contents.fh) + operations.releasedir(fi.contents.fh) + log.debug('Calling fuse_reply_err(0)') + try: + libfuse.fuse_reply_err(req, 0) + except DiscardedRequest: + pass + +def fuse_removexattr(req, inode, name): + '''Remove extended attribute''' + + log.debug('Handling removexattr(%d, %s)', inode, string_at(name)) + operations.removexattr(inode, string_at(name)) + log.debug('Calling fuse_reply_err(0)') + try: + libfuse.fuse_reply_err(req, 0) + except DiscardedRequest: + pass + +def fuse_rename(req, parent_inode_old, name_old, parent_inode_new, name_new): + '''Rename a directory entry''' + + log.debug('Handling rename(%d, %r, %d, %r)', parent_inode_old, string_at(name_old), + parent_inode_new, string_at(name_new)) + operations.rename(parent_inode_old, string_at(name_old), parent_inode_new, + string_at(name_new)) + log.debug('Calling fuse_reply_err(0)') + try: + libfuse.fuse_reply_err(req, 0) + except DiscardedRequest: + pass + +def fuse_rmdir(req, inode_parent, name): + '''Remove a directory''' + + log.debug('Handling rmdir(%d, %r)', inode_parent, string_at(name)) + operations.rmdir(inode_parent, string_at(name)) + log.debug('Calling fuse_reply_err(0)') + try: + libfuse.fuse_reply_err(req, 0) + except DiscardedRequest: + pass + +def fuse_setattr(req, inode, stat, to_set, fi): + '''Change directory entry attributes''' + + log.debug('Handling fuse_setattr(%d)', inode) + + # Note: We can't check if we know all possible flags, + # because the part of to_set that is not "covered" + # by flags seems to be undefined rather than zero. + + attr_all = stat_to_dict(stat.contents) + attr = dict() + + if (to_set & libfuse.FUSE_SET_ATTR_MTIME) != 0: + attr['st_mtime'] = attr_all['st_mtime'] + + if (to_set & libfuse.FUSE_SET_ATTR_ATIME) != 0: + attr['st_atime'] = attr_all['st_atime'] + + if (to_set & libfuse.FUSE_SET_ATTR_MODE) != 0: + attr['st_mode'] = attr_all['st_mode'] + + if (to_set & libfuse.FUSE_SET_ATTR_UID) != 0: + attr['st_uid'] = attr_all['st_uid'] + + if (to_set & libfuse.FUSE_SET_ATTR_GID) != 0: + attr['st_gid'] = attr_all['st_gid'] + + if (to_set & libfuse.FUSE_SET_ATTR_SIZE) != 0: + attr['st_size'] = attr_all['st_size'] + + attr = operations.setattr(inode, attr) + + attr_timeout = attr.pop('attr_timeout') + stat = dict_to_stat(attr) + + log.debug('Calling fuse_reply_attr') + try: + libfuse.fuse_reply_attr(req, stat, attr_timeout) + except DiscardedRequest: + pass + +def fuse_setxattr(req, inode, name, val, size, flags): + '''Set an extended attribute''' + + log.debug('Handling setxattr(%d, %r, %r, %d)', inode, string_at(name), + string_at(val, size), flags) + + # Make sure we know all the flags + if (flags & ~(libfuse.XATTR_CREATE | libfuse.XATTR_REPLACE)) != 0: + raise ValueError('unknown flag') + + if (flags & libfuse.XATTR_CREATE) != 0: + try: + operations.getxattr(inode, string_at(name)) + except FUSEError as e: + if e.errno == ENOATTR: + pass + raise + else: + raise FUSEError(errno.EEXIST) + elif (flags & libfuse.XATTR_REPLACE) != 0: + # Exception can be passed on if the attribute does not exist + operations.getxattr(inode, string_at(name)) + + operations.setxattr(inode, string_at(name), string_at(val, size)) + + log.debug('Calling fuse_reply_err(0)') + try: + libfuse.fuse_reply_err(req, 0) + except DiscardedRequest: + pass + +def fuse_statfs(req, inode): + '''Return filesystem statistics''' + + log.debug('Handling statfs(%d)', inode) + attr = operations.statfs() + statfs = libfuse.statvfs() + + for (key, val) in attr.iteritems(): + setattr(statfs, key, val) + + log.debug('Calling fuse_reply_statfs') + try: + libfuse.fuse_reply_statfs(req, statfs) + except DiscardedRequest: + pass + +def fuse_symlink(req, target, parent_inode, name): + '''Create a symbolic link''' + + log.debug('Handling symlink(%d, %r, %r)', parent_inode, string_at(name), string_at(target)) + attr = operations.symlink(parent_inode, string_at(name), string_at(target), + libfuse.fuse_req_ctx(req).contents) + entry = dict_to_entry(attr) + + log.debug('Calling fuse_reply_entry') + try: + libfuse.fuse_reply_entry(req, entry) + except DiscardedRequest: + pass + + +def fuse_unlink(req, parent_inode, name): + '''Delete a file''' + + log.debug('Handling unlink(%d, %r)', parent_inode, string_at(name)) + operations.unlink(parent_inode, string_at(name)) + log.debug('Calling fuse_reply_err(0)') + try: + libfuse.fuse_reply_err(req, 0) + except DiscardedRequest: + pass + +def fuse_write(req, inode, buf, size, off, fi): + '''Write into an open file handle''' + + log.debug('Handling write(fh=%d, off=%d, size=%d)', fi.contents.fh, off, size) + written = operations.write(fi.contents.fh, off, string_at(buf, size)) + + log.debug('Calling fuse_reply_write') + try: + libfuse.fuse_reply_write(req, written) + except DiscardedRequest: + pass diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse/operations.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse/operations.py new file mode 100644 index 00000000..4510016b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse/operations.py @@ -0,0 +1,348 @@ +''' +$Id: operations.py 47 2010-01-29 17:11:23Z nikratio $ + +Copyright (c) 2010, Nikolaus Rath +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the main author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' + +from __future__ import division, print_function, absolute_import + +from .interface import FUSEError +import errno + +class Operations(object): + ''' + This is a dummy class that just documents the possible methods that + a file system may declare. + ''' + + # This is a dummy class, so all the methods could of course + # be functions + #pylint: disable-msg=R0201 + + def handle_exc(self, exc): + '''Handle exceptions that occured during request processing. + + This method returns nothing and does not raise any exceptions itself. + ''' + + pass + + def init(self): + '''Initialize operations + + This function has to be called before any request has been received, + but after the mountpoint has been set up and the process has + daemonized. + ''' + + pass + + def destroy(self): + '''Clean up operations. + + This method has to be called after the last request has been + received, when the file system is about to be unmounted. + ''' + + pass + + def check_args(self, fuse_args): + '''Review FUSE arguments + + This method checks if the FUSE options `fuse_args` are compatible + with the way that the file system operations are implemented. + It raises an exception if incompatible options are encountered and + silently adds required options if they are missing. + ''' + + pass + + def readdir(self, fh, off): + '''Read directory entries + + This method returns an iterator over the contents of directory `fh`, + starting at entry `off`. The iterator yields tuples of the form + ``(name, attr)``, where ``attr` is a dict with keys corresponding to + the elements of ``struct stat``. + + Iteration may be stopped as soon as enough elements have been + retrieved and does not have to be continued until `StopIteration` + is raised. + ''' + + raise FUSEError(errno.ENOSYS) + + + def read(self, fh, off, size): + '''Read `size` bytes from `fh` at position `off` + + Unless the file has been opened in direct_io mode or EOF is reached, + this function returns exactly `size` bytes. + ''' + + raise FUSEError(errno.ENOSYS) + + def link(self, inode, new_parent_inode, new_name): + '''Create a hard link. + + Returns a dict with the attributes of the newly created directory + entry. The keys are the same as for `lookup`. + ''' + + raise FUSEError(errno.ENOSYS) + + def open(self, inode, flags): + '''Open a file. + + Returns an (integer) file handle. `flags` is a bitwise or of the open flags + described in open(2) and defined in the `os` module (with the exception of + ``O_CREAT``, ``O_EXCL``, ``O_NOCTTY`` and ``O_TRUNC``) + ''' + + raise FUSEError(errno.ENOSYS) + + def opendir(self, inode): + '''Open a directory. + + Returns an (integer) file handle. + ''' + + raise FUSEError(errno.ENOSYS) + + + def mkdir(self, parent_inode, name, mode, ctx): + '''Create a directory + + `ctx` must be a context object that contains pid, uid and + primary gid of the requesting process. + + Returns a dict with the attributes of the newly created directory + entry. The keys are the same as for `lookup`. + ''' + + raise FUSEError(errno.ENOSYS) + + def mknod(self, parent_inode, name, mode, rdev, ctx): + '''Create (possibly special) file + + `ctx` must be a context object that contains pid, uid and + primary gid of the requesting process. + + Returns a dict with the attributes of the newly created directory + entry. The keys are the same as for `lookup`. + ''' + + raise FUSEError(errno.ENOSYS) + + + def lookup(self, parent_inode, name): + '''Look up a directory entry by name and get its attributes. + + Returns a dict with keys corresponding to the elements in + ``struct stat`` and the following additional keys: + + :generation: The inode generation number + :attr_timeout: Validity timeout (in seconds) for the attributes + :entry_timeout: Validity timeout (in seconds) for the name + + Note also that the ``st_Xtime`` entries support floating point numbers + to allow for nano second resolution. + + The returned dict can be modified at will by the caller without + influencing the internal state of the file system. + + If the entry does not exist, raises `FUSEError(errno.ENOENT)`. + ''' + + raise FUSEError(errno.ENOSYS) + + def listxattr(self, inode): + '''Get list of extended attribute names''' + + raise FUSEError(errno.ENOSYS) + + def getattr(self, inode): + '''Get attributes for `inode` + + Returns a dict with keys corresponding to the elements in + ``struct stat`` and the following additional keys: + + :attr_timeout: Validity timeout (in seconds) for the attributes + + The returned dict can be modified at will by the caller without + influencing the internal state of the file system. + + Note that the ``st_Xtime`` entries support floating point numbers + to allow for nano second resolution. + ''' + + raise FUSEError(errno.ENOSYS) + + def getxattr(self, inode, name): + '''Return extended attribute value + + If the attribute does not exist, raises `FUSEError(ENOATTR)` + ''' + + raise FUSEError(errno.ENOSYS) + + def access(self, inode, mode, ctx, get_sup_gids): + '''Check if requesting process has `mode` rights on `inode`. + + Returns a boolean value. `get_sup_gids` must be a function that + returns a list of the supplementary group ids of the requester. + + `ctx` must be a context object that contains pid, uid and + primary gid of the requesting process. + ''' + + raise FUSEError(errno.ENOSYS) + + def create(self, inode_parent, name, mode, ctx): + '''Create a file and open it + + `ctx` must be a context object that contains pid, uid and + primary gid of the requesting process. + + Returns a tuple of the form ``(fh, attr)``. `fh` is + integer file handle that is used to identify the open file and + `attr` is a dict similar to the one returned by `lookup`. + ''' + + raise FUSEError(errno.ENOSYS) + + def flush(self, fh): + '''Handle close() syscall. + + May be called multiple times for the same open file (e.g. if the file handle + has been duplicated). + + If the filesystem supports file locking operations, all locks belonging + to the file handle's owner are cleared. + ''' + + raise FUSEError(errno.ENOSYS) + + def fsync(self, fh, datasync): + '''Flush buffers for file `fh` + + If `datasync` is true, only the user data is flushed (and no meta data). + ''' + + raise FUSEError(errno.ENOSYS) + + + def fsyncdir(self, fh, datasync): + '''Flush buffers for directory `fh` + + If the `datasync` is true, then only the directory contents + are flushed (and not the meta data about the directory itself). + ''' + + raise FUSEError(errno.ENOSYS) + + def readlink(self, inode): + '''Return target of symbolic link''' + + raise FUSEError(errno.ENOSYS) + + def release(self, fh): + '''Release open file + + This method must be called exactly once for each `open` call. + ''' + + raise FUSEError(errno.ENOSYS) + + def releasedir(self, fh): + '''Release open directory + + This method must be called exactly once for each `opendir` call. + ''' + + raise FUSEError(errno.ENOSYS) + + def removexattr(self, inode, name): + '''Remove extended attribute + + If the attribute does not exist, raises FUSEError(ENOATTR) + ''' + + raise FUSEError(errno.ENOSYS) + + def rename(self, inode_parent_old, name_old, inode_parent_new, name_new): + '''Rename a directory entry''' + + raise FUSEError(errno.ENOSYS) + + def rmdir(self, inode_parent, name): + '''Remove a directory''' + + raise FUSEError(errno.ENOSYS) + + def setattr(self, inode, attr): + '''Change directory entry attributes + + `attr` must be a dict with keys corresponding to the attributes of + ``struct stat``. `attr` may also include a new value for ``st_size`` which + means that the file should be truncated or extended. + + Returns a dict with the new attributs of the directory entry, + similar to the one returned by `getattr()` + ''' + + raise FUSEError(errno.ENOSYS) + + def setxattr(self, inode, name, value): + '''Set an extended attribute. + + The attribute may or may not exist already. + ''' + + raise FUSEError(errno.ENOSYS) + + def statfs(self): + '''Get file system statistics + + Returns a `dict` with keys corresponding to the attributes of + ``struct statfs``. + ''' + + raise FUSEError(errno.ENOSYS) + + def symlink(self, inode_parent, name, target, ctx): + '''Create a symbolic link + + `ctx` must be a context object that contains pid, uid and + primary gid of the requesting process. + + Returns a dict with the attributes of the newly created directory + entry. The keys are the same as for `lookup`. + ''' + + raise FUSEError(errno.ENOSYS) + + def unlink(self, parent_inode, name): + '''Remove a (possibly special) file''' + + raise FUSEError(errno.ENOSYS) + + def write(self, fh, off, data): + '''Write data into an open file + + Returns the number of bytes written. + Unless the file was opened in ``direct_io`` mode, this is always equal to + `len(data)`. + ''' + + raise FUSEError(errno.ENOSYS) + diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse_example.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse_example.py new file mode 100755 index 00000000..8b4d7041 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/llfuse_example.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +''' +$Id: llfuse_example.py 46 2010-01-29 17:10:10Z nikratio $ + +Copyright (c) 2010, Nikolaus Rath +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the main author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' + +from __future__ import division, print_function, absolute_import + +import llfuse +import errno +import stat +import sys + +class Operations(llfuse.Operations): + '''A very simple example filesystem''' + + def __init__(self): + super(Operations, self).__init__() + self.entries = [ + # name, attr + (b'.', { 'st_ino': 1, + 'st_mode': stat.S_IFDIR | 0755, + 'st_nlink': 2}), + (b'..', { 'st_ino': 1, + 'st_mode': stat.S_IFDIR | 0755, + 'st_nlink': 2}), + (b'file1', { 'st_ino': 2, 'st_nlink': 1, + 'st_mode': stat.S_IFREG | 0644 }), + (b'file2', { 'st_ino': 3, 'st_nlink': 1, + 'st_mode': stat.S_IFREG | 0644 }) ] + + self.contents = { # Inode: Contents + 2: b'Hello, World\n', + 3: b'Some more file contents\n' + } + + self.by_inode = dict() + self.by_name = dict() + + for entry in self.entries: + (name, attr) = entry + if attr['st_ino'] in self.contents: + attr['st_size'] = len(self.contents[attr['st_ino']]) + + + self.by_inode[attr['st_ino']] = attr + self.by_name[name] = attr + + + + def lookup(self, parent_inode, name): + try: + attr = self.by_name[name].copy() + except KeyError: + raise llfuse.FUSEError(errno.ENOENT) + + attr['attr_timeout'] = 1 + attr['entry_timeout'] = 1 + attr['generation'] = 1 + + return attr + + + def getattr(self, inode): + attr = self.by_inode[inode].copy() + attr['attr_timeout'] = 1 + return attr + + def readdir(self, fh, off): + for entry in self.entries: + if off > 0: + off -= 1 + continue + + yield entry + + + def read(self, fh, off, size): + return self.contents[fh][off:off+size] + + def open(self, inode, flags): + if inode in self.contents: + return inode + else: + raise RuntimeError('Attempted to open() a directory') + + def opendir(self, inode): + return inode + + def access(self, inode, mode, ctx, get_sup_gids): + return True + + + +if __name__ == '__main__': + + if len(sys.argv) != 2: + raise SystemExit('Usage: %s ' % sys.argv[0]) + + mountpoint = sys.argv[1] + operations = Operations() + + llfuse.init(operations, mountpoint, [ b"nonempty", b'fsname=llfuses_xmp' ]) + llfuse.main() + \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/setup.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/setup.py new file mode 100755 index 00000000..88a5018f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/low-level/setup.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +''' +$Id: setup.py 53 2010-02-22 01:48:45Z nikratio $ + +Copyright (c) 2010, Nikolaus Rath +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the main author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' + +from __future__ import division, print_function + +from distutils.core import setup, Command +import distutils.command.build +import sys +import os +import tempfile +import subprocess +import re +import logging +import ctypes.util + +# These are the definitions that we need +fuse_export_regex = ['^FUSE_SET_.*', '^XATTR_.*', 'fuse_reply_.*' ] +fuse_export_symbols = ['fuse_mount', 'fuse_lowlevel_new', 'fuse_add_direntry', + 'fuse_set_signal_handlers', 'fuse_session_add_chan', + 'fuse_session_loop_mt', 'fuse_session_remove_chan', + 'fuse_remove_signal_handlers', 'fuse_session_destroy', + 'fuse_unmount', 'fuse_req_ctx', 'fuse_lowlevel_ops', + 'fuse_session_loop', 'ENOATTR', 'ENOTSUP', + 'fuse_version' ] + +class build_ctypes(Command): + + description = "Build ctypes interfaces" + user_options = [] + boolean_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + '''Create ctypes API to local FUSE headers''' + + # Import ctypeslib + basedir = os.path.abspath(os.path.dirname(sys.argv[0])) + sys.path.insert(0, os.path.join(basedir, 'ctypeslib.zip')) + from ctypeslib import h2xml, xml2py + from ctypeslib.codegen import codegenerator as ctypeslib + + print('Creating ctypes API from local fuse headers...') + + cflags = self.get_cflags() + print('Using cflags: %s' % ' '.join(cflags)) + + fuse_path = 'fuse' + if not ctypes.util.find_library(fuse_path): + print('Could not find fuse library', file=sys.stderr) + sys.exit(1) + + + # Create temporary XML file + tmp_fh = tempfile.NamedTemporaryFile() + tmp_name = tmp_fh.name + + print('Calling h2xml...') + argv = [ 'h2xml.py', '-o', tmp_name, '-c', '-q', '-I', basedir, 'fuse_ctypes.h' ] + argv += cflags + ctypeslib.ASSUME_STRINGS = False + ctypeslib.CDLL_SET_ERRNO = False + ctypeslib.PREFIX = ('# Code autogenerated by ctypeslib. Any changes will be lost!\n\n' + '#pylint: disable-all\n' + '#@PydevCodeAnalysisIgnore\n\n') + h2xml.main(argv) + + print('Calling xml2py...') + api_file = os.path.join(basedir, 'llfuse', 'ctypes_api.py') + argv = [ 'xml2py.py', tmp_name, '-o', api_file, '-l', fuse_path ] + for el in fuse_export_regex: + argv.append('-r') + argv.append(el) + for el in fuse_export_symbols: + argv.append('-s') + argv.append(el) + xml2py.main(argv) + + # Delete temporary XML file + tmp_fh.close() + + print('Code generation complete.') + + def get_cflags(self): + '''Get cflags required to compile with fuse library''' + + proc = subprocess.Popen(['pkg-config', 'fuse', '--cflags'], stdout=subprocess.PIPE) + cflags = proc.stdout.readline().rstrip() + proc.stdout.close() + if proc.wait() != 0: + sys.stderr.write('Failed to execute pkg-config. Exit code: %d.\n' + % proc.returncode) + sys.stderr.write('Check that the FUSE development package been installed properly.\n') + sys.exit(1) + return cflags.split() + + +# Add as subcommand of build +distutils.command.build.build.sub_commands.insert(0, ('build_ctypes', None)) + + +setup(name='llfuse_example', + version='1.0', + author='Nikolaus Rath', + author_email='Nikolaus@rath.org', + url='http://code.google.com/p/fusepy/', + packages=[ 'llfuse' ], + provides=['llfuse'], + cmdclass={ 'build_ctypes': build_ctypes} + ) diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/memory.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/memory.py new file mode 100755 index 00000000..246b305d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/memory.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python + +from collections import defaultdict +from errno import ENOENT +from stat import S_IFDIR, S_IFLNK, S_IFREG +from sys import argv, exit +from time import time + +from fuse import FUSE, FuseOSError, Operations, LoggingMixIn + + +class Memory(LoggingMixIn, Operations): + """Example memory filesystem. Supports only one level of files.""" + + def __init__(self): + self.files = {} + self.data = defaultdict(str) + self.fd = 0 + now = time() + self.files['/'] = dict(st_mode=(S_IFDIR | 0755), st_ctime=now, + st_mtime=now, st_atime=now, st_nlink=2) + + def chmod(self, path, mode): + self.files[path]['st_mode'] &= 0770000 + self.files[path]['st_mode'] |= mode + return 0 + + def chown(self, path, uid, gid): + self.files[path]['st_uid'] = uid + self.files[path]['st_gid'] = gid + + def create(self, path, mode): + self.files[path] = dict(st_mode=(S_IFREG | mode), st_nlink=1, + st_size=0, st_ctime=time(), st_mtime=time(), st_atime=time()) + self.fd += 1 + return self.fd + + def getattr(self, path, fh=None): + if path not in self.files: + raise FuseOSError(ENOENT) + st = self.files[path] + return st + + def getxattr(self, path, name, position=0): + attrs = self.files[path].get('attrs', {}) + try: + return attrs[name] + except KeyError: + return '' # Should return ENOATTR + + def listxattr(self, path): + attrs = self.files[path].get('attrs', {}) + return attrs.keys() + + def mkdir(self, path, mode): + self.files[path] = dict(st_mode=(S_IFDIR | mode), st_nlink=2, + st_size=0, st_ctime=time(), st_mtime=time(), st_atime=time()) + self.files['/']['st_nlink'] += 1 + + def open(self, path, flags): + self.fd += 1 + return self.fd + + def read(self, path, size, offset, fh): + return self.data[path][offset:offset + size] + + def readdir(self, path, fh): + return ['.', '..'] + [x[1:] for x in self.files if x != '/'] + + def readlink(self, path): + return self.data[path] + + def removexattr(self, path, name): + attrs = self.files[path].get('attrs', {}) + try: + del attrs[name] + except KeyError: + pass # Should return ENOATTR + + def rename(self, old, new): + self.files[new] = self.files.pop(old) + + def rmdir(self, path): + self.files.pop(path) + self.files['/']['st_nlink'] -= 1 + + def setxattr(self, path, name, value, options, position=0): + # Ignore options + attrs = self.files[path].setdefault('attrs', {}) + attrs[name] = value + + def statfs(self, path): + return dict(f_bsize=512, f_blocks=4096, f_bavail=2048) + + def symlink(self, target, source): + self.files[target] = dict(st_mode=(S_IFLNK | 0777), st_nlink=1, + st_size=len(source)) + self.data[target] = source + + def truncate(self, path, length, fh=None): + self.data[path] = self.data[path][:length] + self.files[path]['st_size'] = length + + def unlink(self, path): + self.files.pop(path) + + def utimens(self, path, times=None): + now = time() + atime, mtime = times if times else (now, now) + self.files[path]['st_atime'] = atime + self.files[path]['st_mtime'] = mtime + + def write(self, path, data, offset, fh): + self.data[path] = self.data[path][:offset] + data + self.files[path]['st_size'] = len(self.data[path]) + return len(data) + + +if __name__ == "__main__": + if len(argv) != 2: + print 'usage: %s ' % argv[0] + exit(1) + fuse = FUSE(Memory(), argv[1], foreground=True) \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/memory3.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/memory3.py new file mode 100755 index 00000000..e5cbad72 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/memory3.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python + +from fuse3 import FUSE, Operations, LoggingMixIn + +from collections import defaultdict +from errno import ENOENT +from stat import S_IFDIR, S_IFLNK, S_IFREG +from sys import argv, exit +from time import time + +import logging + + +class Memory(LoggingMixIn, Operations): + """Example memory filesystem. Supports only one level of files.""" + + def __init__(self): + self.files = {} + self.data = defaultdict(bytearray) + self.fd = 0 + now = time() + self.files['/'] = dict(st_mode=(S_IFDIR | 0o755), st_ctime=now, + st_mtime=now, st_atime=now, st_nlink=2) + + def chmod(self, path, mode): + self.files[path]['st_mode'] &= 0o770000 + self.files[path]['st_mode'] |= mode + return 0 + + def chown(self, path, uid, gid): + self.files[path]['st_uid'] = uid + self.files[path]['st_gid'] = gid + + def create(self, path, mode): + self.files[path] = dict(st_mode=(S_IFREG | mode), st_nlink=1, + st_size=0, st_ctime=time(), st_mtime=time(), st_atime=time()) + self.fd += 1 + return self.fd + + def getattr(self, path, fh=None): + if path not in self.files: + raise OSError(ENOENT, '') + st = self.files[path] + return st + + def getxattr(self, path, name, position=0): + attrs = self.files[path].get('attrs', {}) + try: + return attrs[name] + except KeyError: + return '' # Should return ENOATTR + + def listxattr(self, path): + attrs = self.files[path].get('attrs', {}) + return attrs.keys() + + def mkdir(self, path, mode): + self.files[path] = dict(st_mode=(S_IFDIR | mode), st_nlink=2, + st_size=0, st_ctime=time(), st_mtime=time(), st_atime=time()) + self.files['/']['st_nlink'] += 1 + + def open(self, path, flags): + self.fd += 1 + return self.fd + + def read(self, path, size, offset, fh): + return bytes(self.data[path][offset:offset + size]) + + def readdir(self, path, fh): + return ['.', '..'] + [x[1:] for x in self.files if x != '/'] + + def readlink(self, path): + return self.data[path].decode('utf-8') + + def removexattr(self, path, name): + attrs = self.files[path].get('attrs', {}) + try: + del attrs[name] + except KeyError: + pass # Should return ENOATTR + + def rename(self, old, new): + self.files[new] = self.files.pop(old) + + def rmdir(self, path): + self.files.pop(path) + self.files['/']['st_nlink'] -= 1 + + def setxattr(self, path, name, value, options, position=0): + # Ignore options + attrs = self.files[path].setdefault('attrs', {}) + attrs[name] = value + + def statfs(self, path): + return dict(f_bsize=512, f_blocks=4096, f_bavail=2048) + + def symlink(self, target, source): + source = source.encode('utf-8') + self.files[target] = dict(st_mode=(S_IFLNK | 0o777), st_nlink=1, + st_size=len(source)) + self.data[target] = bytearray(source) + + def truncate(self, path, length, fh=None): + del self.data[path][length:] + self.files[path]['st_size'] = length + + def unlink(self, path): + self.files.pop(path) + + def utimens(self, path, times=None): + now = time() + atime, mtime = times if times else (now, now) + self.files[path]['st_atime'] = atime + self.files[path]['st_mtime'] = mtime + + def write(self, path, data, offset, fh): + del self.data[path][offset:] + self.data[path].extend(data) + self.files[path]['st_size'] = len(self.data[path]) + return len(data) + + +if __name__ == "__main__": + if len(argv) != 2: + print('usage: %s ' % argv[0]) + exit(1) + logging.getLogger().setLevel(logging.DEBUG) + fuse = FUSE(Memory(), argv[1], foreground=True) \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/memoryll.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/memoryll.py new file mode 100755 index 00000000..307a1af0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/memoryll.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python + +from collections import defaultdict +from errno import ENOENT, EROFS +from stat import S_IFMT, S_IMODE, S_IFDIR, S_IFREG +from sys import argv, exit +from time import time + +from fusell import FUSELL + + +class Memory(FUSELL): + def create_ino(self): + self.ino += 1 + return self.ino + + def init(self, userdata, conn): + self.ino = 1 + self.attr = defaultdict(dict) + self.data = defaultdict(str) + self.parent = {} + self.children = defaultdict(dict) + + self.attr[1] = {'st_ino': 1, 'st_mode': S_IFDIR | 0777, 'st_nlink': 2} + self.parent[1] = 1 + + forget = None + + def getattr(self, req, ino, fi): + print 'getattr:', ino + attr = self.attr[ino] + if attr: + self.reply_attr(req, attr, 1.0) + else: + self.reply_err(req, ENOENT) + + def lookup(self, req, parent, name): + print 'lookup:', parent, name + children = self.children[parent] + ino = children.get(name, 0) + attr = self.attr[ino] + + if attr: + entry = {'ino': ino, 'attr': attr, 'atttr_timeout': 1.0, 'entry_timeout': 1.0} + self.reply_entry(req, entry) + else: + self.reply_err(req, ENOENT) + + def mkdir(self, req, parent, name, mode): + print 'mkdir:', parent, name + ino = self.create_ino() + ctx = self.req_ctx(req) + now = time() + attr = { + 'st_ino': ino, + 'st_mode': S_IFDIR | mode, + 'st_nlink': 2, + 'st_uid': ctx['uid'], + 'st_gid': ctx['gid'], + 'st_atime': now, + 'st_mtime': now, + 'st_ctime': now} + + self.attr[ino] = attr + self.attr[parent]['st_nlink'] += 1 + self.parent[ino] = parent + self.children[parent][name] = ino + + entry = {'ino': ino, 'attr': attr, 'atttr_timeout': 1.0, 'entry_timeout': 1.0} + self.reply_entry(req, entry) + + def mknod(self, req, parent, name, mode, rdev): + print 'mknod:', parent, name + ino = self.create_ino() + ctx = self.req_ctx(req) + now = time() + attr = { + 'st_ino': ino, + 'st_mode': mode, + 'st_nlink': 1, + 'st_uid': ctx['uid'], + 'st_gid': ctx['gid'], + 'st_rdev': rdev, + 'st_atime': now, + 'st_mtime': now, + 'st_ctime': now} + + self.attr[ino] = attr + self.attr[parent]['st_nlink'] += 1 + self.children[parent][name] = ino + + entry = {'ino': ino, 'attr': attr, 'atttr_timeout': 1.0, 'entry_timeout': 1.0} + self.reply_entry(req, entry) + + def open(self, req, ino, fi): + print 'open:', ino + self.reply_open(req, fi) + + def read(self, req, ino, size, off, fi): + print 'read:', ino, size, off + buf = self.data[ino][off:(off + size)] + self.reply_buf(req, buf) + + def readdir(self, req, ino, size, off, fi): + print 'readdir:', ino + parent = self.parent[ino] + entries = [('.', {'st_ino': ino, 'st_mode': S_IFDIR}), + ('..', {'st_ino': parent, 'st_mode': S_IFDIR})] + for name, child in self.children[ino].items(): + entries.append((name, self.attr[child])) + self.reply_readdir(req, size, off, entries) + + def rename(self, req, parent, name, newparent, newname): + print 'rename:', parent, name, newparent, newname + ino = self.children[parent].pop(name) + self.children[newparent][newname] = ino + self.parent[ino] = newparent + self.reply_err(req, 0) + + def setattr(self, req, ino, attr, to_set, fi): + print 'setattr:', ino, to_set + a = self.attr[ino] + for key in to_set: + if key == 'st_mode': + # Keep the old file type bit fields + a['st_mode'] = S_IFMT(a['st_mode']) | S_IMODE(attr['st_mode']) + else: + a[key] = attr[key] + self.attr[ino] = a + self.reply_attr(req, a, 1.0) + + def write(self, req, ino, buf, off, fi): + print 'write:', ino, off, len(buf) + self.data[ino] = self.data[ino][:off] + buf + self.attr[ino]['st_size'] = len(self.data[ino]) + self.reply_write(req, len(buf)) + +if __name__ == '__main__': + if len(argv) != 2: + print 'usage: %s ' % argv[0] + exit(1) + fuse = Memory(argv[1]) diff --git a/vendor/github.com/camlistore/camlistore/lib/python/fusepy/sftp.py b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/sftp.py new file mode 100755 index 00000000..019fb29d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/fusepy/sftp.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python + +from sys import argv, exit +from time import time + +from paramiko import SSHClient + +from fuse import FUSE, Operations + + +class SFTP(Operations): + """A simple SFTP filesystem. Requires paramiko: + http://www.lag.net/paramiko/ + + You need to be able to login to remote host without entering a password. + """ + def __init__(self, host, path='.'): + self.client = SSHClient() + self.client.load_system_host_keys() + self.client.connect(host) + self.sftp = self.client.open_sftp() + self.root = path + + def __del__(self): + self.sftp.close() + self.client.close() + + def __call__(self, op, path, *args): + print '->', op, path, args[0] if args else '' + ret = '[Unhandled Exception]' + try: + ret = getattr(self, op)(self.root + path, *args) + return ret + except OSError, e: + ret = str(e) + raise + except IOError, e: + ret = str(e) + raise OSError(*e.args) + finally: + print '<-', op + + def chmod(self, path, mode): + return self.sftp.chmod(path, mode) + + def chown(self, path, uid, gid): + return self.sftp.chown(path, uid, gid) + + def create(self, path, mode): + f = self.sftp.open(path, 'w') + f.chmod(mode) + f.close() + return 0 + + def getattr(self, path, fh=None): + st = self.sftp.lstat(path) + return dict((key, getattr(st, key)) for key in ('st_atime', 'st_gid', + 'st_mode', 'st_mtime', 'st_size', 'st_uid')) + + def mkdir(self, path, mode): + return self.sftp.mkdir(path, mode) + + def read(self, path, size, offset, fh): + f = self.sftp.open(path) + f.seek(offset, 0) + buf = f.read(size) + f.close() + return buf + + def readdir(self, path, fh): + return ['.', '..'] + [name.encode('utf-8') for name in self.sftp.listdir(path)] + + def readlink(self, path): + return self.sftp.readlink(path) + + def rename(self, old, new): + return self.sftp.rename(old, self.root + new) + + def rmdir(self, path): + return self.sftp.rmdir(path) + + def symlink(self, target, source): + return self.sftp.symlink(source, target) + + def truncate(self, path, length, fh=None): + return self.sftp.truncate(path, length) + + def unlink(self, path): + return self.sftp.unlink(path) + + def utimens(self, path, times=None): + return self.sftp.utime(path, times) + + def write(self, path, data, offset, fh): + f = self.sftp.open(path, 'r+') + f.seek(offset, 0) + f.write(data) + f.close() + return len(data) + + +if __name__ == "__main__": + if len(argv) != 3: + print 'usage: %s ' % argv[0] + exit(1) + fuse = FUSE(SFTP(argv[1]), argv[2], foreground=True, nothreads=True) \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/lib/python/setup.py b/vendor/github.com/camlistore/camlistore/lib/python/setup.py new file mode 100644 index 00000000..cefac2d8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup +setup( + name='camlistore-client', + version='1.0.3dev', + author='Brett Slatkin', + author_email='bslatkin@gmail.com', + maintainer='Jack Laxson', + maintainer_email='jackjrabbit+camli@gmail.com', + description="Client library for Camlistore.", + url='http://camlistore.org', + license='Apache v2', + long_description='A convience library for python developers wishing to explore camlistore.', + packages=['camli'], + install_requires=['simplejson'], + classifiers=['Environment :: Console', 'Topic :: Internet :: WWW/HTTP'] +) \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/lib/python/simplejson/__init__.py b/vendor/github.com/camlistore/camlistore/lib/python/simplejson/__init__.py new file mode 100644 index 00000000..dcfd5413 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/simplejson/__init__.py @@ -0,0 +1,437 @@ +r"""JSON (JavaScript Object Notation) is a subset of +JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data +interchange format. + +:mod:`simplejson` exposes an API familiar to users of the standard library +:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained +version of the :mod:`json` library contained in Python 2.6, but maintains +compatibility with Python 2.4 and Python 2.5 and (currently) has +significant performance advantages, even without using the optional C +extension for speedups. + +Encoding basic Python object hierarchies:: + + >>> import simplejson as json + >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) + '["foo", {"bar": ["baz", null, 1.0, 2]}]' + >>> print json.dumps("\"foo\bar") + "\"foo\bar" + >>> print json.dumps(u'\u1234') + "\u1234" + >>> print json.dumps('\\') + "\\" + >>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) + {"a": 0, "b": 0, "c": 0} + >>> from StringIO import StringIO + >>> io = StringIO() + >>> json.dump(['streaming API'], io) + >>> io.getvalue() + '["streaming API"]' + +Compact encoding:: + + >>> import simplejson as json + >>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':')) + '[1,2,3,{"4":5,"6":7}]' + +Pretty printing:: + + >>> import simplejson as json + >>> s = json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=' ') + >>> print '\n'.join([l.rstrip() for l in s.splitlines()]) + { + "4": 5, + "6": 7 + } + +Decoding JSON:: + + >>> import simplejson as json + >>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] + >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj + True + >>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar' + True + >>> from StringIO import StringIO + >>> io = StringIO('["streaming API"]') + >>> json.load(io)[0] == 'streaming API' + True + +Specializing JSON object decoding:: + + >>> import simplejson as json + >>> def as_complex(dct): + ... if '__complex__' in dct: + ... return complex(dct['real'], dct['imag']) + ... return dct + ... + >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}', + ... object_hook=as_complex) + (1+2j) + >>> from decimal import Decimal + >>> json.loads('1.1', parse_float=Decimal) == Decimal('1.1') + True + +Specializing JSON object encoding:: + + >>> import simplejson as json + >>> def encode_complex(obj): + ... if isinstance(obj, complex): + ... return [obj.real, obj.imag] + ... raise TypeError(repr(o) + " is not JSON serializable") + ... + >>> json.dumps(2 + 1j, default=encode_complex) + '[2.0, 1.0]' + >>> json.JSONEncoder(default=encode_complex).encode(2 + 1j) + '[2.0, 1.0]' + >>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j)) + '[2.0, 1.0]' + + +Using simplejson.tool from the shell to validate and pretty-print:: + + $ echo '{"json":"obj"}' | python -m simplejson.tool + { + "json": "obj" + } + $ echo '{ 1.2:3.4}' | python -m simplejson.tool + Expecting property name: line 1 column 2 (char 2) +""" +__version__ = '2.1.1' +__all__ = [ + 'dump', 'dumps', 'load', 'loads', + 'JSONDecoder', 'JSONDecodeError', 'JSONEncoder', + 'OrderedDict', +] + +__author__ = 'Bob Ippolito ' + +from decimal import Decimal + +from decoder import JSONDecoder, JSONDecodeError +from encoder import JSONEncoder +def _import_OrderedDict(): + import collections + try: + return collections.OrderedDict + except AttributeError: + import ordered_dict + return ordered_dict.OrderedDict +OrderedDict = _import_OrderedDict() + +def _import_c_make_encoder(): + try: + from simplejson._speedups import make_encoder + return make_encoder + except ImportError: + return None + +_default_encoder = JSONEncoder( + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + encoding='utf-8', + default=None, + use_decimal=False, +) + +def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, use_decimal=False, **kw): + """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a + ``.write()``-supporting file-like object). + + If ``skipkeys`` is true then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the some chunks written to ``fp`` + may be ``unicode`` instances, subject to normal Python ``str`` to + ``unicode`` coercion rules. Unless ``fp.write()`` explicitly + understands ``unicode`` (as in ``codecs.getwriter()``) this is likely + to cause an error. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) + in strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If *indent* is a string, then JSON array elements and object members + will be pretty-printed with a newline followed by that string repeated + for each level of nesting. ``None`` (the default) selects the most compact + representation without any newlines. For backwards compatibility with + versions of simplejson earlier than 2.1.0, an integer is also accepted + and is converted to a string with that many spaces. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + If *use_decimal* is true (default: ``False``) then decimal.Decimal + will be natively serialized to JSON with full precision. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not kw): + iterable = _default_encoder.iterencode(obj) + else: + if cls is None: + cls = JSONEncoder + iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, + default=default, use_decimal=use_decimal, **kw).iterencode(obj) + # could accelerate with writelines in some versions of Python, at + # a debuggability cost + for chunk in iterable: + fp.write(chunk) + + +def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, use_decimal=False, **kw): + """Serialize ``obj`` to a JSON formatted ``str``. + + If ``skipkeys`` is false then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the return value will be a + ``unicode`` instance subject to normal Python ``str`` to ``unicode`` + coercion rules instead of being escaped to an ASCII ``str``. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in + strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a string, then JSON array elements and object members + will be pretty-printed with a newline followed by that string repeated + for each level of nesting. ``None`` (the default) selects the most compact + representation without any newlines. For backwards compatibility with + versions of simplejson earlier than 2.1.0, an integer is also accepted + and is converted to a string with that many spaces. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + If *use_decimal* is true (default: ``False``) then decimal.Decimal + will be natively serialized to JSON with full precision. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not use_decimal + and not kw): + return _default_encoder.encode(obj) + if cls is None: + cls = JSONEncoder + return cls( + skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, default=default, + use_decimal=use_decimal, **kw).encode(obj) + + +_default_decoder = JSONDecoder(encoding=None, object_hook=None, + object_pairs_hook=None) + + +def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, object_pairs_hook=None, + use_decimal=False, **kw): + """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing + a JSON document) to a Python object. + + *encoding* determines the encoding used to interpret any + :class:`str` objects decoded by this instance (``'utf-8'`` by + default). It has no effect when decoding :class:`unicode` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as :class:`unicode`. + + *object_hook*, if specified, will be called with the result of every + JSON object decoded and its return value will be used in place of the + given :class:`dict`. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + *object_pairs_hook* is an optional function that will be called with + the result of any object literal decode with an ordered list of pairs. + The return value of *object_pairs_hook* will be used instead of the + :class:`dict`. This feature can be used to implement custom decoders + that rely on the order that the key and value pairs are decoded (for + example, :func:`collections.OrderedDict` will remember the order of + insertion). If *object_hook* is also defined, the *object_pairs_hook* + takes priority. + + *parse_float*, if specified, will be called with the string of every + JSON float to be decoded. By default, this is equivalent to + ``float(num_str)``. This can be used to use another datatype or parser + for JSON floats (e.g. :class:`decimal.Decimal`). + + *parse_int*, if specified, will be called with the string of every + JSON int to be decoded. By default, this is equivalent to + ``int(num_str)``. This can be used to use another datatype or parser + for JSON integers (e.g. :class:`float`). + + *parse_constant*, if specified, will be called with one of the + following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This + can be used to raise an exception if invalid JSON numbers are + encountered. + + If *use_decimal* is true (default: ``False``) then it implies + parse_float=decimal.Decimal for parity with ``dump``. + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + return loads(fp.read(), + encoding=encoding, cls=cls, object_hook=object_hook, + parse_float=parse_float, parse_int=parse_int, + parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, + use_decimal=use_decimal, **kw) + + +def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, object_pairs_hook=None, + use_decimal=False, **kw): + """Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON + document) to a Python object. + + *encoding* determines the encoding used to interpret any + :class:`str` objects decoded by this instance (``'utf-8'`` by + default). It has no effect when decoding :class:`unicode` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as :class:`unicode`. + + *object_hook*, if specified, will be called with the result of every + JSON object decoded and its return value will be used in place of the + given :class:`dict`. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + *object_pairs_hook* is an optional function that will be called with + the result of any object literal decode with an ordered list of pairs. + The return value of *object_pairs_hook* will be used instead of the + :class:`dict`. This feature can be used to implement custom decoders + that rely on the order that the key and value pairs are decoded (for + example, :func:`collections.OrderedDict` will remember the order of + insertion). If *object_hook* is also defined, the *object_pairs_hook* + takes priority. + + *parse_float*, if specified, will be called with the string of every + JSON float to be decoded. By default, this is equivalent to + ``float(num_str)``. This can be used to use another datatype or parser + for JSON floats (e.g. :class:`decimal.Decimal`). + + *parse_int*, if specified, will be called with the string of every + JSON int to be decoded. By default, this is equivalent to + ``int(num_str)``. This can be used to use another datatype or parser + for JSON integers (e.g. :class:`float`). + + *parse_constant*, if specified, will be called with one of the + following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This + can be used to raise an exception if invalid JSON numbers are + encountered. + + If *use_decimal* is true (default: ``False``) then it implies + parse_float=decimal.Decimal for parity with ``dump``. + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + if (cls is None and encoding is None and object_hook is None and + parse_int is None and parse_float is None and + parse_constant is None and object_pairs_hook is None + and not use_decimal and not kw): + return _default_decoder.decode(s) + if cls is None: + cls = JSONDecoder + if object_hook is not None: + kw['object_hook'] = object_hook + if object_pairs_hook is not None: + kw['object_pairs_hook'] = object_pairs_hook + if parse_float is not None: + kw['parse_float'] = parse_float + if parse_int is not None: + kw['parse_int'] = parse_int + if parse_constant is not None: + kw['parse_constant'] = parse_constant + if use_decimal: + if parse_float is not None: + raise TypeError("use_decimal=True implies parse_float=Decimal") + kw['parse_float'] = Decimal + return cls(encoding=encoding, **kw).decode(s) + + +def _toggle_speedups(enabled): + import simplejson.decoder as dec + import simplejson.encoder as enc + import simplejson.scanner as scan + c_make_encoder = _import_c_make_encoder() + if enabled: + dec.scanstring = dec.c_scanstring or dec.py_scanstring + enc.c_make_encoder = c_make_encoder + enc.encode_basestring_ascii = (enc.c_encode_basestring_ascii or + enc.py_encode_basestring_ascii) + scan.make_scanner = scan.c_make_scanner or scan.py_make_scanner + else: + dec.scanstring = dec.py_scanstring + enc.c_make_encoder = None + enc.encode_basestring_ascii = enc.py_encode_basestring_ascii + scan.make_scanner = scan.py_make_scanner + dec.make_scanner = scan.make_scanner + global _default_decoder + _default_decoder = JSONDecoder( + encoding=None, + object_hook=None, + object_pairs_hook=None, + ) + global _default_encoder + _default_encoder = JSONEncoder( + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + encoding='utf-8', + default=None, + ) diff --git a/vendor/github.com/camlistore/camlistore/lib/python/simplejson/decoder.py b/vendor/github.com/camlistore/camlistore/lib/python/simplejson/decoder.py new file mode 100644 index 00000000..4cf4015f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/simplejson/decoder.py @@ -0,0 +1,421 @@ +"""Implementation of JSONDecoder +""" +import re +import sys +import struct + +from simplejson.scanner import make_scanner +def _import_c_scanstring(): + try: + from simplejson._speedups import scanstring + return scanstring + except ImportError: + return None +c_scanstring = _import_c_scanstring() + +__all__ = ['JSONDecoder'] + +FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL + +def _floatconstants(): + _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') + # The struct module in Python 2.4 would get frexp() out of range here + # when an endian is specified in the format string. Fixed in Python 2.5+ + if sys.byteorder != 'big': + _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] + nan, inf = struct.unpack('dd', _BYTES) + return nan, inf, -inf + +NaN, PosInf, NegInf = _floatconstants() + + +class JSONDecodeError(ValueError): + """Subclass of ValueError with the following additional properties: + + msg: The unformatted error message + doc: The JSON document being parsed + pos: The start index of doc where parsing failed + end: The end index of doc where parsing failed (may be None) + lineno: The line corresponding to pos + colno: The column corresponding to pos + endlineno: The line corresponding to end (may be None) + endcolno: The column corresponding to end (may be None) + + """ + def __init__(self, msg, doc, pos, end=None): + ValueError.__init__(self, errmsg(msg, doc, pos, end=end)) + self.msg = msg + self.doc = doc + self.pos = pos + self.end = end + self.lineno, self.colno = linecol(doc, pos) + if end is not None: + self.endlineno, self.endcolno = linecol(doc, pos) + else: + self.endlineno, self.endcolno = None, None + + +def linecol(doc, pos): + lineno = doc.count('\n', 0, pos) + 1 + if lineno == 1: + colno = pos + else: + colno = pos - doc.rindex('\n', 0, pos) + return lineno, colno + + +def errmsg(msg, doc, pos, end=None): + # Note that this function is called from _speedups + lineno, colno = linecol(doc, pos) + if end is None: + #fmt = '{0}: line {1} column {2} (char {3})' + #return fmt.format(msg, lineno, colno, pos) + fmt = '%s: line %d column %d (char %d)' + return fmt % (msg, lineno, colno, pos) + endlineno, endcolno = linecol(doc, end) + #fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})' + #return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end) + fmt = '%s: line %d column %d - line %d column %d (char %d - %d)' + return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end) + + +_CONSTANTS = { + '-Infinity': NegInf, + 'Infinity': PosInf, + 'NaN': NaN, +} + +STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) +BACKSLASH = { + '"': u'"', '\\': u'\\', '/': u'/', + 'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t', +} + +DEFAULT_ENCODING = "utf-8" + +def py_scanstring(s, end, encoding=None, strict=True, + _b=BACKSLASH, _m=STRINGCHUNK.match): + """Scan the string s for a JSON string. End is the index of the + character in s after the quote that started the JSON string. + Unescapes all valid JSON string escape sequences and raises ValueError + on attempt to decode an invalid string. If strict is False then literal + control characters are allowed in the string. + + Returns a tuple of the decoded string and the index of the character in s + after the end quote.""" + if encoding is None: + encoding = DEFAULT_ENCODING + chunks = [] + _append = chunks.append + begin = end - 1 + while 1: + chunk = _m(s, end) + if chunk is None: + raise JSONDecodeError( + "Unterminated string starting at", s, begin) + end = chunk.end() + content, terminator = chunk.groups() + # Content is contains zero or more unescaped string characters + if content: + if not isinstance(content, unicode): + content = unicode(content, encoding) + _append(content) + # Terminator is the end of string, a literal control character, + # or a backslash denoting that an escape sequence follows + if terminator == '"': + break + elif terminator != '\\': + if strict: + msg = "Invalid control character %r at" % (terminator,) + #msg = "Invalid control character {0!r} at".format(terminator) + raise JSONDecodeError(msg, s, end) + else: + _append(terminator) + continue + try: + esc = s[end] + except IndexError: + raise JSONDecodeError( + "Unterminated string starting at", s, begin) + # If not a unicode escape sequence, must be in the lookup table + if esc != 'u': + try: + char = _b[esc] + except KeyError: + msg = "Invalid \\escape: " + repr(esc) + raise JSONDecodeError(msg, s, end) + end += 1 + else: + # Unicode escape sequence + esc = s[end + 1:end + 5] + next_end = end + 5 + if len(esc) != 4: + msg = "Invalid \\uXXXX escape" + raise JSONDecodeError(msg, s, end) + uni = int(esc, 16) + # Check for surrogate pair on UCS-4 systems + if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535: + msg = "Invalid \\uXXXX\\uXXXX surrogate pair" + if not s[end + 5:end + 7] == '\\u': + raise JSONDecodeError(msg, s, end) + esc2 = s[end + 7:end + 11] + if len(esc2) != 4: + raise JSONDecodeError(msg, s, end) + uni2 = int(esc2, 16) + uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) + next_end += 6 + char = unichr(uni) + end = next_end + # Append the unescaped character + _append(char) + return u''.join(chunks), end + + +# Use speedup if available +scanstring = c_scanstring or py_scanstring + +WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) +WHITESPACE_STR = ' \t\n\r' + +def JSONObject((s, end), encoding, strict, scan_once, object_hook, + object_pairs_hook, memo=None, + _w=WHITESPACE.match, _ws=WHITESPACE_STR): + # Backwards compatibility + if memo is None: + memo = {} + memo_get = memo.setdefault + pairs = [] + # Use a slice to prevent IndexError from being raised, the following + # check will raise a more specific ValueError if the string is empty + nextchar = s[end:end + 1] + # Normally we expect nextchar == '"' + if nextchar != '"': + if nextchar in _ws: + end = _w(s, end).end() + nextchar = s[end:end + 1] + # Trivial empty object + if nextchar == '}': + if object_pairs_hook is not None: + result = object_pairs_hook(pairs) + return result, end + pairs = {} + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end + 1 + elif nextchar != '"': + raise JSONDecodeError("Expecting property name", s, end) + end += 1 + while True: + key, end = scanstring(s, end, encoding, strict) + key = memo_get(key, key) + + # To skip some function call overhead we optimize the fast paths where + # the JSON key separator is ": " or just ":". + if s[end:end + 1] != ':': + end = _w(s, end).end() + if s[end:end + 1] != ':': + raise JSONDecodeError("Expecting : delimiter", s, end) + + end += 1 + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + try: + value, end = scan_once(s, end) + except StopIteration: + raise JSONDecodeError("Expecting object", s, end) + pairs.append((key, value)) + + try: + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + end += 1 + + if nextchar == '}': + break + elif nextchar != ',': + raise JSONDecodeError("Expecting , delimiter", s, end - 1) + + try: + nextchar = s[end] + if nextchar in _ws: + end += 1 + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + + end += 1 + if nextchar != '"': + raise JSONDecodeError("Expecting property name", s, end - 1) + + if object_pairs_hook is not None: + result = object_pairs_hook(pairs) + return result, end + pairs = dict(pairs) + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end + +def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + values = [] + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + # Look-ahead for trivial empty array + if nextchar == ']': + return values, end + 1 + _append = values.append + while True: + try: + value, end = scan_once(s, end) + except StopIteration: + raise JSONDecodeError("Expecting object", s, end) + _append(value) + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + end += 1 + if nextchar == ']': + break + elif nextchar != ',': + raise JSONDecodeError("Expecting , delimiter", s, end) + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + return values, end + +class JSONDecoder(object): + """Simple JSON decoder + + Performs the following translations in decoding by default: + + +---------------+-------------------+ + | JSON | Python | + +===============+===================+ + | object | dict | + +---------------+-------------------+ + | array | list | + +---------------+-------------------+ + | string | unicode | + +---------------+-------------------+ + | number (int) | int, long | + +---------------+-------------------+ + | number (real) | float | + +---------------+-------------------+ + | true | True | + +---------------+-------------------+ + | false | False | + +---------------+-------------------+ + | null | None | + +---------------+-------------------+ + + It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as + their corresponding ``float`` values, which is outside the JSON spec. + + """ + + def __init__(self, encoding=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, strict=True, + object_pairs_hook=None): + """ + *encoding* determines the encoding used to interpret any + :class:`str` objects decoded by this instance (``'utf-8'`` by + default). It has no effect when decoding :class:`unicode` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as :class:`unicode`. + + *object_hook*, if specified, will be called with the result of every + JSON object decoded and its return value will be used in place of the + given :class:`dict`. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + *object_pairs_hook* is an optional function that will be called with + the result of any object literal decode with an ordered list of pairs. + The return value of *object_pairs_hook* will be used instead of the + :class:`dict`. This feature can be used to implement custom decoders + that rely on the order that the key and value pairs are decoded (for + example, :func:`collections.OrderedDict` will remember the order of + insertion). If *object_hook* is also defined, the *object_pairs_hook* + takes priority. + + *parse_float*, if specified, will be called with the string of every + JSON float to be decoded. By default, this is equivalent to + ``float(num_str)``. This can be used to use another datatype or parser + for JSON floats (e.g. :class:`decimal.Decimal`). + + *parse_int*, if specified, will be called with the string of every + JSON int to be decoded. By default, this is equivalent to + ``int(num_str)``. This can be used to use another datatype or parser + for JSON integers (e.g. :class:`float`). + + *parse_constant*, if specified, will be called with one of the + following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This + can be used to raise an exception if invalid JSON numbers are + encountered. + + *strict* controls the parser's behavior when it encounters an + invalid control character in a string. The default setting of + ``True`` means that unescaped control characters are parse errors, if + ``False`` then control characters will be allowed in strings. + + """ + self.encoding = encoding + self.object_hook = object_hook + self.object_pairs_hook = object_pairs_hook + self.parse_float = parse_float or float + self.parse_int = parse_int or int + self.parse_constant = parse_constant or _CONSTANTS.__getitem__ + self.strict = strict + self.parse_object = JSONObject + self.parse_array = JSONArray + self.parse_string = scanstring + self.memo = {} + self.scan_once = make_scanner(self) + + def decode(self, s, _w=WHITESPACE.match): + """Return the Python representation of ``s`` (a ``str`` or ``unicode`` + instance containing a JSON document) + + """ + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + end = _w(s, end).end() + if end != len(s): + raise JSONDecodeError("Extra data", s, end, len(s)) + return obj + + def raw_decode(self, s, idx=0): + """Decode a JSON document from ``s`` (a ``str`` or ``unicode`` + beginning with a JSON document) and return a 2-tuple of the Python + representation and the index in ``s`` where the document ended. + + This can be used to decode a JSON document from a string that may + have extraneous data at the end. + + """ + try: + obj, end = self.scan_once(s, idx) + except StopIteration: + raise JSONDecodeError("No JSON object could be decoded", s, idx) + return obj, end diff --git a/vendor/github.com/camlistore/camlistore/lib/python/simplejson/encoder.py b/vendor/github.com/camlistore/camlistore/lib/python/simplejson/encoder.py new file mode 100644 index 00000000..cab84565 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/lib/python/simplejson/encoder.py @@ -0,0 +1,501 @@ +"""Implementation of JSONEncoder +""" +import re +from decimal import Decimal + +def _import_speedups(): + try: + from simplejson import _speedups + return _speedups.encode_basestring_ascii, _speedups.make_encoder + except ImportError: + return None, None +c_encode_basestring_ascii, c_make_encoder = _import_speedups() + +from simplejson.decoder import PosInf + +ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]') +ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') +HAS_UTF8 = re.compile(r'[\x80-\xff]') +ESCAPE_DCT = { + '\\': '\\\\', + '"': '\\"', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', +} +for i in range(0x20): + #ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i)) + ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) + +FLOAT_REPR = repr + +def encode_basestring(s): + """Return a JSON representation of a Python string + + """ + if isinstance(s, str) and HAS_UTF8.search(s) is not None: + s = s.decode('utf-8') + def replace(match): + return ESCAPE_DCT[match.group(0)] + return u'"' + ESCAPE.sub(replace, s) + u'"' + + +def py_encode_basestring_ascii(s): + """Return an ASCII-only JSON representation of a Python string + + """ + if isinstance(s, str) and HAS_UTF8.search(s) is not None: + s = s.decode('utf-8') + def replace(match): + s = match.group(0) + try: + return ESCAPE_DCT[s] + except KeyError: + n = ord(s) + if n < 0x10000: + #return '\\u{0:04x}'.format(n) + return '\\u%04x' % (n,) + else: + # surrogate pair + n -= 0x10000 + s1 = 0xd800 | ((n >> 10) & 0x3ff) + s2 = 0xdc00 | (n & 0x3ff) + #return '\\u{0:04x}\\u{1:04x}'.format(s1, s2) + return '\\u%04x\\u%04x' % (s1, s2) + return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' + + +encode_basestring_ascii = ( + c_encode_basestring_ascii or py_encode_basestring_ascii) + +class JSONEncoder(object): + """Extensible JSON encoder for Python data structures. + + Supports the following objects and types by default: + + +-------------------+---------------+ + | Python | JSON | + +===================+===============+ + | dict | object | + +-------------------+---------------+ + | list, tuple | array | + +-------------------+---------------+ + | str, unicode | string | + +-------------------+---------------+ + | int, long, float | number | + +-------------------+---------------+ + | True | true | + +-------------------+---------------+ + | False | false | + +-------------------+---------------+ + | None | null | + +-------------------+---------------+ + + To extend this to recognize other objects, subclass and implement a + ``.default()`` method with another method that returns a serializable + object for ``o`` if possible, otherwise it should call the superclass + implementation (to raise ``TypeError``). + + """ + item_separator = ', ' + key_separator = ': ' + def __init__(self, skipkeys=False, ensure_ascii=True, + check_circular=True, allow_nan=True, sort_keys=False, + indent=None, separators=None, encoding='utf-8', default=None, + use_decimal=False): + """Constructor for JSONEncoder, with sensible defaults. + + If skipkeys is false, then it is a TypeError to attempt + encoding of keys that are not str, int, long, float or None. If + skipkeys is True, such items are simply skipped. + + If ensure_ascii is true, the output is guaranteed to be str + objects with all incoming unicode characters escaped. If + ensure_ascii is false, the output will be unicode object. + + If check_circular is true, then lists, dicts, and custom encoded + objects will be checked for circular references during encoding to + prevent an infinite recursion (which would cause an OverflowError). + Otherwise, no such check takes place. + + If allow_nan is true, then NaN, Infinity, and -Infinity will be + encoded as such. This behavior is not JSON specification compliant, + but is consistent with most JavaScript based encoders and decoders. + Otherwise, it will be a ValueError to encode such floats. + + If sort_keys is true, then the output of dictionaries will be + sorted by key; this is useful for regression tests to ensure + that JSON serializations can be compared on a day-to-day basis. + + If indent is a string, then JSON array elements and object members + will be pretty-printed with a newline followed by that string repeated + for each level of nesting. ``None`` (the default) selects the most compact + representation without any newlines. For backwards compatibility with + versions of simplejson earlier than 2.1.0, an integer is also accepted + and is converted to a string with that many spaces. + + If specified, separators should be a (item_separator, key_separator) + tuple. The default is (', ', ': '). To get the most compact JSON + representation you should specify (',', ':') to eliminate whitespace. + + If specified, default is a function that gets called for objects + that can't otherwise be serialized. It should return a JSON encodable + version of the object or raise a ``TypeError``. + + If encoding is not None, then all input strings will be + transformed into unicode using that encoding prior to JSON-encoding. + The default is UTF-8. + + If use_decimal is true (not the default), ``decimal.Decimal`` will + be supported directly by the encoder. For the inverse, decode JSON + with ``parse_float=decimal.Decimal``. + + """ + + self.skipkeys = skipkeys + self.ensure_ascii = ensure_ascii + self.check_circular = check_circular + self.allow_nan = allow_nan + self.sort_keys = sort_keys + self.use_decimal = use_decimal + if isinstance(indent, (int, long)): + indent = ' ' * indent + self.indent = indent + if separators is not None: + self.item_separator, self.key_separator = separators + if default is not None: + self.default = default + self.encoding = encoding + + def default(self, o): + """Implement this method in a subclass such that it returns + a serializable object for ``o``, or calls the base implementation + (to raise a ``TypeError``). + + For example, to support arbitrary iterators, you could + implement default like this:: + + def default(self, o): + try: + iterable = iter(o) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, o) + + """ + raise TypeError(repr(o) + " is not JSON serializable") + + def encode(self, o): + """Return a JSON string representation of a Python data structure. + + >>> from simplejson import JSONEncoder + >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) + '{"foo": ["bar", "baz"]}' + + """ + # This is for extremely simple cases and benchmarks. + if isinstance(o, basestring): + if isinstance(o, str): + _encoding = self.encoding + if (_encoding is not None + and not (_encoding == 'utf-8')): + o = o.decode(_encoding) + if self.ensure_ascii: + return encode_basestring_ascii(o) + else: + return encode_basestring(o) + # This doesn't pass the iterator directly to ''.join() because the + # exceptions aren't as detailed. The list call should be roughly + # equivalent to the PySequence_Fast that ''.join() would do. + chunks = self.iterencode(o, _one_shot=True) + if not isinstance(chunks, (list, tuple)): + chunks = list(chunks) + if self.ensure_ascii: + return ''.join(chunks) + else: + return u''.join(chunks) + + def iterencode(self, o, _one_shot=False): + """Encode the given object and yield each string + representation as available. + + For example:: + + for chunk in JSONEncoder().iterencode(bigobject): + mysocket.write(chunk) + + """ + if self.check_circular: + markers = {} + else: + markers = None + if self.ensure_ascii: + _encoder = encode_basestring_ascii + else: + _encoder = encode_basestring + if self.encoding != 'utf-8': + def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding): + if isinstance(o, str): + o = o.decode(_encoding) + return _orig_encoder(o) + + def floatstr(o, allow_nan=self.allow_nan, + _repr=FLOAT_REPR, _inf=PosInf, _neginf=-PosInf): + # Check for specials. Note that this type of test is processor + # and/or platform-specific, so do tests which don't depend on + # the internals. + + if o != o: + text = 'NaN' + elif o == _inf: + text = 'Infinity' + elif o == _neginf: + text = '-Infinity' + else: + return _repr(o) + + if not allow_nan: + raise ValueError( + "Out of range float values are not JSON compliant: " + + repr(o)) + + return text + + + key_memo = {} + if (_one_shot and c_make_encoder is not None + and not self.indent and not self.sort_keys): + _iterencode = c_make_encoder( + markers, self.default, _encoder, self.indent, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, self.allow_nan, key_memo, self.use_decimal) + else: + _iterencode = _make_iterencode( + markers, self.default, _encoder, self.indent, floatstr, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, _one_shot, self.use_decimal) + try: + return _iterencode(o, 0) + finally: + key_memo.clear() + + +class JSONEncoderForHTML(JSONEncoder): + """An encoder that produces JSON safe to embed in HTML. + + To embed JSON content in, say, a script tag on a web page, the + characters &, < and > should be escaped. They cannot be escaped + with the usual entities (e.g. &) because they are not expanded + within + {{end}} + {{end}} + + {{define "messages"}} +
    +

    Camlistore on Google Cloud

    + + {{if .InstanceIP}} +

    Success. Your Camlistore instance should be up at https://{{.InstanceIP}}. It can take a couple of minutes to be ready.

    +

    Please save the information on this page in case you need to come back for the instruction.

    + +

    First connection

    +

    + A self-signed HTTPS certificate was automatically generated with "{{.Conf.Hostname}}" as the common name.
    + You will need to add an exception for it in your browser when you get a security warning the first time you connect. At which point you should check that the certificate fingerprint matches one of: + + + +
    SHA-1{{.CertFingerprintSHA1}}
    SHA-256{{.CertFingerprintSHA256}}
    +

    + +

    Further configuration

    +

    + Manage your instance at {{.ProjectConsoleURL}}. +

    + +

    + To change your login and password, go to the camlistore-server instance page. Set camlistore-username and/or camlistore-password in the custom metadata section. Then restart Camlistore. +

    + +

    + If you want to use your own HTTPS certificate and key, go to the storage browser. Delete "` + certFilename + `", "` + keyFilename + `", and replace them by uploading your own files (with the same names). Then restart Camlistore. +

    + +

    + To manage/add SSH keys, go to the camlistore-server instance page. Scroll down to the SSH Keys section. +

    + {{end}} + {{if .Err}} +

    {{.Err}}

    + {{range $hint := .Hints}} +

    {{$hint}}

    + {{end}} + {{end}} + {{end}} + +{{define "withform"}} + +{{template "header" .}} + + {{if .InstanceKey}} +
    + {{end}} + {{template "banner" .}} + {{template "toplinks" .}} + {{template "progress" .}} + {{template "messages" .}} +
    + + +

    Deploy Camlistore on Google Cloud

    + +

    +This tool helps you create your own private Camlistore instance running on Google's cloud. Be sure to understand Google Compute Engine's pricing before proceeding. To delete your instance and stop paying Google for the virtual machine, visit the Google Cloud console. +

    + + + + + + + + +
    Project ID
    +
      +
    • Select a Google Project in which to create the VM. If it doesn't already exist, create it first before using this Camlistore creation tool.
    • +
    • Requirements:
    • +
        +
      • Enable billing. (Billing & settings)
      • +
      • APIs and auth > APIs > Google Cloud Storage
      • +
      • APIs and auth > APIs > Google Cloud Storage JSON API
      • +
      • APIs and auth > APIs > Google Compute Engine
      • +
      • APIs and auth > APIs > Google Cloud Logging API
      • +
      +
    +
    New password
    New password for your Camlistore server.
    Zone + + + {{range $k, $v := .ZoneValues}} + + {{end}} + +
    Machine type + + + {{range $k, $v := .MachineValues}} + + {{end}} + +

    (it will ask for permissions)
    +
    +
    + {{template "footer" .}} + {{if .InstanceKey}} +
    + {{end}} + + +{{end}} + +{{define "noform"}} + +{{template "header" .}} + + {{if .InstanceKey}} +
    + {{end}} + {{template "banner" .}} + {{template "toplinks" .}} + {{template "progress" .}} + {{template "messages" .}} + {{template "footer" .}} + {{if .InstanceKey}} +
    + {{end}} + + +{{end}} +` diff --git a/vendor/github.com/camlistore/camlistore/pkg/deploy/gce/notes.txt b/vendor/github.com/camlistore/camlistore/pkg/deploy/gce/notes.txt new file mode 100644 index 00000000..f62b16e0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/deploy/gce/notes.txt @@ -0,0 +1,31 @@ +non-core dev: +gcutil --service_version="v1" --project="camanaged" addinstance "camlistore" --zone="us-central1-b" --machine_type="n1-standard-1" --network="default" --external_ip_address="107.178.214.163" --metadata="cam-key-1:cam-value-1" --metadata="cam-key-2:cam-value-2" --metadata="sshKeys:bradfitz:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCw6Dk3iskKylP2zginCOAzIunMA38vGL9b/i18UG/Iuq+jKczZXB/1dlcZGSOs3+LtGh/C341TXTioydxTw+ux1AbmUk4c6L404skl85XFOys/GLxA4sHxBSb5we0Q57yohSgeZNlQd+Scmu5v7WC0N7I3hOK0lJgtxRNyC2nncGC0UOm+IGPTWcqPJERTauH/OhoAddWQehf1ugxTJYFU9atl3Op/mDXfyGBSLweWAQ84fhVKRZnl4i9Yhk1b357Q8cVKH6UQUADVamo7CQOsenzx99UL0thFRTSbuKALyf9e+SPwJrtIxZaX+skVSR+CzooRbypIamLbNXhfbxNz bradfitz@Bradleys-MacBook-Air.local" --service_account_scopes="https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/compute.readonly,https://www.googleapis.com/auth/devstorage.full_control,https://www.googleapis.com/auth/sqlservice,https://www.googleapis.com/auth/sqlservice.admin,https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/datastore,https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/compute,https://www.googleapis.com/auth/devstorage.full_control,https://www.googleapis.com/auth/taskqueue,https://www.googleapis.com/auth/bigquery,https://www.googleapis.com/auth/sqlservice,https://www.googleapis.com/auth/datastore" --tags="tag,tag2,http-server,https-server" --persistent_boot_disk="true" --auto_delete_boot_disk="false" --image=projects/debian-cloud/global/images/backports-debian-7-wheezy-v20140718 + +$ curl -H "Metadata-Flavor:Google" http://metadata/computeMetadata/v1/instance/service-accounts/default/scopes +https://www.googleapis.com/auth/bigquery +https://www.googleapis.com/auth/cloud-platform +https://www.googleapis.com/auth/compute +https://www.googleapis.com/auth/compute.readonly +https://www.googleapis.com/auth/datastore +https://www.googleapis.com/auth/devstorage.full_control +https://www.googleapis.com/auth/sqlservice +https://www.googleapis.com/auth/sqlservice.admin +https://www.googleapis.com/auth/taskqueue +https://www.googleapis.com/auth/userinfo.email + +gcutil --project=camanaged addinstance \ + --image=projects/coreos-cloud/global/images/coreos-alpha-394-0-0-v20140801 \ + --persistent_boot_disk \ + --zone=us-central1-a --machine_type=n1-standard-1 \ + --external_ip_address=107.178.208.16 \ + --auto_delete_boot_disk \ + --tags=http-server,https-server \ + --metadata_from_file=user-data:cloud-config.yaml core1 + +TODO: +- allow config from /gcs/bucket/key; add pkg for os.Stat/os.Open wrappers checking + prefix +- use that package for: + "httpsCert": "/home/bradfitz/keys/camlihouse/ssl.crt", + "httpsKey": "/home/bradfitz/keys/camlihouse/ssl.key", + "identitySecretRing": "/home/bradfitz/.config/camlistore/identity-secring.gpg", diff --git a/vendor/github.com/camlistore/camlistore/pkg/env/env.go b/vendor/github.com/camlistore/camlistore/pkg/env/env.go new file mode 100644 index 00000000..79befcaf --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/env/env.go @@ -0,0 +1,69 @@ +/* +Copyright 2015 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package env detects what sort of environment Camlistore is running in. +package env + +import ( + "os" + "strconv" + "sync" + + "google.golang.org/cloud/compute/metadata" +) + +// IsDebug reports whether this is a debug environment. +func IsDebug() bool { + return isDebug +} + +// DebugUploads reports whether this is a debug environment for uploads. +func DebugUploads() bool { + return isDebugUploads +} + +// IsDev reports whether this is a development server environment (devcam server). +func IsDev() bool { + return isDev +} + +// OsGCE reports whether this process is running in a Google Compute +// Engine (GCE) environment. This only returns true if the +// "camlistore-config-dir" instance metadata value is defined. +// Instances running in custom configs on GCE will be unaffected. +func OnGCE() bool { + gceOnce.Do(detectGCE) + return isGCE +} + +var ( + gceOnce sync.Once + isGCE bool +) + +func detectGCE() { + if !metadata.OnGCE() { + return + } + v, _ := metadata.InstanceAttributeValue("camlistore-config-dir") + isGCE = v != "" +} + +var ( + isDev = os.Getenv("CAMLI_DEV_CAMLI_ROOT") != "" + isDebug, _ = strconv.ParseBool(os.Getenv("CAMLI_DEBUG")) + isDebugUploads, _ = strconv.ParseBool(os.Getenv("CAMLI_DEBUG_UPLOADS")) +) diff --git a/vendor/github.com/camlistore/camlistore/pkg/fault/fault.go b/vendor/github.com/camlistore/camlistore/pkg/fault/fault.go new file mode 100644 index 00000000..e3ac452f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fault/fault.go @@ -0,0 +1,59 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package fault handles fault injection for testing. +package fault + +import ( + "errors" + "math/rand" + "os" + "strconv" + "strings" +) + +var fakeErr = errors.New("fake injected error for testing") + +// An Injector reports whether fake errors should be returned. +type Injector struct { + failPercent int +} + +// NewInjector returns a new fault injector with the given name. The +// environment variable "FAULT_" + capital(name) + "_FAIL_PERCENT" +// controls the percentage of requests that fail. If undefined or +// zero, no requests fail. +func NewInjector(name string) *Injector { + var failPercent, _ = strconv.Atoi(os.Getenv("FAULT_" + strings.ToUpper(name) + "_FAIL_PERCENT")) + return &Injector{ + failPercent: failPercent, + } +} + +// ShouldFail reports whether a fake error should be returned. +func (in *Injector) ShouldFail() bool { + return in.failPercent > 0 && in.failPercent > rand.Intn(100) +} + +// FailErr checks ShouldFail and, if true, assigns a fake error to err +// and returns true. +func (in *Injector) FailErr(err *error) bool { + if !in.ShouldFail() { + return false + } + *err = fakeErr + return true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fileembed/fileembed.go b/vendor/github.com/camlistore/camlistore/pkg/fileembed/fileembed.go new file mode 100644 index 00000000..c3e5d324 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fileembed/fileembed.go @@ -0,0 +1,331 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package fileembed provides access to static data resources (images, +// HTML, css, etc) embedded into the binary with genfileembed. +// +// Most of the package contains internal details used by genfileembed. +// Normal applications will simply make a global Files variable. +package fileembed + +import ( + "compress/zlib" + "encoding/base64" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +// Files contains all the embedded resources. +type Files struct { + // Optional environment variable key to override + OverrideEnv string + + // Optional fallback directory to check, if not in memory. + DirFallback string + + // SlurpToMemory controls whether on first access the file is + // slurped into memory. It's intended for use with DirFallback. + SlurpToMemory bool + + // Listable controls whether requests for the http file "/" return + // a directory of available files. Must be set to true for + // http.FileServer to correctly handle requests for index.html. + Listable bool + + lk sync.Mutex + file map[string]*staticFile +} + +type staticFile struct { + name string + contents []byte + modtime time.Time +} + +type Opener interface { + Open() (io.Reader, error) +} + +type String string + +func (s String) Open() (io.Reader, error) { + return strings.NewReader(string(s)), nil +} + +// ZlibCompressed is used to store a compressed file. +type ZlibCompressed string + +func (zb ZlibCompressed) Open() (io.Reader, error) { + rz, err := zlib.NewReader(strings.NewReader(string(zb))) + if err != nil { + return nil, fmt.Errorf("Could not open ZlibCompressed: %v", err) + } + return rz, nil +} + +// ZlibCompressedBase64 is used to store a compressed file. +// Unlike ZlibCompressed, the string is base64 encoded, +// in standard base64 encoding. +type ZlibCompressedBase64 string + +func (zb ZlibCompressedBase64) Open() (io.Reader, error) { + rz, err := zlib.NewReader(base64.NewDecoder(base64.StdEncoding, strings.NewReader(string(zb)))) + if err != nil { + return nil, fmt.Errorf("Could not open ZlibCompressedBase64: %v", err) + } + return rz, nil +} + +// Multi concatenates multiple Openers into one, like io.MultiReader. +func Multi(openers ...Opener) Opener { + return multi(openers) +} + +type multi []Opener + +func (m multi) Open() (io.Reader, error) { + rs := make([]io.Reader, 0, len(m)) + for _, o := range m { + r, err := o.Open() + if err != nil { + return nil, err + } + rs = append(rs, r) + } + return io.MultiReader(rs...), nil +} + +// Add adds a file to the file set. +func (f *Files) Add(filename string, size int64, modtime time.Time, o Opener) { + f.lk.Lock() + defer f.lk.Unlock() + + r, err := o.Open() + if err != nil { + log.Printf("Could not add file %v: %v", filename, err) + return + } + contents, err := ioutil.ReadAll(r) + if err != nil { + log.Printf("Could not read contents of file %v: %v", filename, err) + return + } + + f.add(filename, &staticFile{ + name: filename, + contents: contents, + modtime: modtime, + }) +} + +// f.lk must be locked +func (f *Files) add(filename string, sf *staticFile) { + if f.file == nil { + f.file = make(map[string]*staticFile) + } + f.file[filename] = sf +} + +var _ http.FileSystem = (*Files)(nil) + +func (f *Files) Open(filename string) (hf http.File, err error) { + // don't bother locking f.lk here, because Listable will normally be set on initialization + if filename == "/" && f.Listable { + return openDir(f) + } + filename = strings.TrimLeft(filename, "/") + if e := f.OverrideEnv; e != "" && os.Getenv(e) != "" { + diskPath := filepath.Join(os.Getenv(e), filename) + return os.Open(diskPath) + } + f.lk.Lock() + defer f.lk.Unlock() + sf, ok := f.file[filename] + if !ok { + return f.openFallback(filename) + } + return &fileHandle{sf: sf}, nil +} + +// f.lk is held +func (f *Files) openFallback(filename string) (http.File, error) { + if f.DirFallback == "" { + return nil, os.ErrNotExist + } + of, err := os.Open(filepath.Join(f.DirFallback, filename)) + switch { + case err != nil: + return nil, err + case f.SlurpToMemory: + defer of.Close() + bs, err := ioutil.ReadAll(of) + if err != nil { + return nil, err + } + fi, err := of.Stat() + + sf := &staticFile{ + name: filename, + contents: bs, + modtime: fi.ModTime(), + } + f.add(filename, sf) + return &fileHandle{sf: sf}, nil + } + return of, nil +} + +type fileHandle struct { + sf *staticFile + off int64 + closed bool +} + +var _ http.File = (*fileHandle)(nil) + +func (f *fileHandle) Close() error { + if f.closed { + return os.ErrInvalid + } + f.closed = true + return nil +} + +func (f *fileHandle) Read(p []byte) (n int, err error) { + if f.off >= int64(len(f.sf.contents)) { + return 0, io.EOF + } + n = copy(p, f.sf.contents[f.off:]) + f.off += int64(n) + return +} + +func (f *fileHandle) Readdir(int) ([]os.FileInfo, error) { + return nil, errors.New("not directory") +} + +func (f *fileHandle) Seek(offset int64, whence int) (int64, error) { + switch whence { + case os.SEEK_SET: + f.off = offset + case os.SEEK_CUR: + f.off += offset + case os.SEEK_END: + f.off = f.sf.Size() + offset + default: + return 0, os.ErrInvalid + } + if f.off < 0 { + f.off = 0 + } + return f.off, nil +} + +func (f *fileHandle) Stat() (os.FileInfo, error) { + return f.sf, nil +} + +var _ os.FileInfo = (*staticFile)(nil) + +func (f *staticFile) Name() string { return f.name } +func (f *staticFile) Size() int64 { return int64(len(f.contents)) } +func (f *staticFile) Mode() os.FileMode { return 0444 } +func (f *staticFile) ModTime() time.Time { return f.modtime } +func (f *staticFile) IsDir() bool { return false } +func (f *staticFile) Sys() interface{} { return nil } + +func openDir(f *Files) (hf http.File, err error) { + f.lk.Lock() + defer f.lk.Unlock() + + allFiles := make([]os.FileInfo, 0, len(f.file)) + var dirModtime time.Time + + for filename, sfile := range f.file { + if strings.Contains(filename, "/") { + continue // skip child directories; we only support readdir on the rootdir for now + } + allFiles = append(allFiles, sfile) + // a directory's modtime is the maximum contained modtime + if sfile.modtime.After(dirModtime) { + dirModtime = sfile.modtime + } + } + + return &dirHandle{ + sd: &staticDir{name: "/", modtime: dirModtime}, + files: allFiles, + }, nil +} + +type dirHandle struct { + sd *staticDir + files []os.FileInfo + off int +} + +func (d *dirHandle) Readdir(n int) ([]os.FileInfo, error) { + if n <= 0 { + return d.files, nil + } + if d.off >= len(d.files) { + return []os.FileInfo{}, io.EOF + } + + if d.off+n > len(d.files) { + n = len(d.files) - d.off + } + matches := d.files[d.off : d.off+n] + d.off += n + + var err error + if d.off > len(d.files) { + err = io.EOF + } + + return matches, err +} + +func (d *dirHandle) Close() error { return nil } +func (d *dirHandle) Read(p []byte) (int, error) { return 0, errors.New("not file") } +func (d *dirHandle) Seek(int64, int) (int64, error) { return 0, os.ErrInvalid } +func (d *dirHandle) Stat() (os.FileInfo, error) { return d.sd, nil } + +type staticDir struct { + name string + modtime time.Time +} + +func (d *staticDir) Name() string { return d.name } +func (d *staticDir) Size() int64 { return 0 } +func (d *staticDir) Mode() os.FileMode { return 0444 | os.ModeDir } +func (d *staticDir) ModTime() time.Time { return d.modtime } +func (d *staticDir) IsDir() bool { return true } +func (d *staticDir) Sys() interface{} { return nil } + +// JoinStrings joins returns the concatentation of ss. +func JoinStrings(ss ...string) string { + return strings.Join(ss, "") +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fileembed/genfileembed/genfileembed.go b/vendor/github.com/camlistore/camlistore/pkg/fileembed/genfileembed/genfileembed.go new file mode 100644 index 00000000..a19fb3d0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fileembed/genfileembed/genfileembed.go @@ -0,0 +1,372 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// The genfileembed command embeds resources into Go files, to eliminate run-time +// dependencies on files on the filesystem. +package main + +import ( + "bytes" + "compress/zlib" + "crypto/sha1" + "encoding/base64" + "flag" + "fmt" + "go/parser" + "go/printer" + "go/token" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "camlistore.org/pkg/rollsum" +) + +var ( + processAll = flag.Bool("all", false, "process all files (if false, only process modified files)") + + fileEmbedPkgPath = flag.String("fileembed-package", "camlistore.org/pkg/fileembed", "the Go package name for fileembed. If you have vendored fileembed (e.g. with goven), you can use this flag to ensure that generated code imports the vendored package.") + + chunkThreshold = flag.Int64("chunk-threshold", 0, "If non-zero, the maximum size of a file before it's cut up into content-addressable chunks with a rolling checksum") + chunkPackage = flag.String("chunk-package", "", "Package to hold chunks") + + destFilesStderr = flag.Bool("output-files-stderr", false, "Write the absolute path of all output files to stderr prefixed with OUTPUT:") + + patternFilename = flag.String("pattern-file", "fileembed.go", "Filepath relative to from which to read the #fileembed pattern") + + buildTags = flag.String("build-tags", "", "Add these tags as +build constraints to the resulting zembed_*.go files") +) + +const ( + maxUncompressed = 50 << 10 // 50KB + // Threshold ratio for compression. + // Files which don't compress at least as well are kept uncompressed. + zRatio = 0.5 +) + +func usage() { + fmt.Fprintf(os.Stderr, "usage: genfileembed [flags] []\n") + flag.PrintDefaults() + os.Exit(2) +} + +func main() { + flag.Usage = usage + flag.Parse() + + absPath, err := os.Getwd() // absolute path of output directory + if err != nil { + log.Fatal(err) + } + dir := "." + switch flag.NArg() { + case 0: + case 1: + dir = flag.Arg(0) + if err := os.Chdir(dir); err != nil { + log.Fatalf("chdir(%q) = %v", dir, err) + } + if filepath.IsAbs(dir) { + absPath = dir + } else { + absPath = filepath.Join(absPath, dir) + } + default: + flag.Usage() + } + + pkgName, filePattern, fileEmbedModTime, err := parseFileEmbed() + if err != nil { + log.Fatalf("Error parsing %s/%s: %v", dir, *patternFilename, err) + } + + for _, fileName := range matchingFiles(filePattern) { + fi, err := os.Stat(fileName) + if err != nil { + log.Fatal(err) + } + + embedName := "zembed_" + strings.Replace(fileName, string(filepath.Separator), "_", -1) + ".go" + if *destFilesStderr { + fmt.Fprintf(os.Stderr, "OUTPUT:%s\n", filepath.Join(absPath, embedName)) + } + zfi, zerr := os.Stat(embedName) + genFile := func() bool { + if *processAll || zerr != nil { + return true + } + if zfi.ModTime().Before(fi.ModTime()) { + return true + } + if zfi.ModTime().Before(fileEmbedModTime) { + return true + } + return false + } + if !genFile() { + continue + } + log.Printf("Updating %s (package %s)", embedName, pkgName) + + bs, err := ioutil.ReadFile(fileName) + if err != nil { + log.Fatal(err) + } + + zb, fileSize := compressFile(bytes.NewReader(bs)) + ratio := float64(len(zb)) / float64(fileSize) + byteStreamType := "" + var qb []byte // quoted string, or Go expression evaluating to a string + var imports string + if *chunkThreshold > 0 && int64(len(bs)) > *chunkThreshold { + byteStreamType = "fileembed.Multi" + qb = chunksOf(bs) + if *chunkPackage == "" { + log.Fatalf("Must provide a --chunk-package value with --chunk-threshold") + } + imports = fmt.Sprintf("import chunkpkg \"%s\"\n", *chunkPackage) + } else if fileSize < maxUncompressed || ratio > zRatio { + byteStreamType = "fileembed.String" + qb = quote(bs) + } else { + byteStreamType = "fileembed.ZlibCompressedBase64" + qb = quote([]byte(base64.StdEncoding.EncodeToString(zb))) + } + + var b bytes.Buffer + fmt.Fprintf(&b, "// THIS FILE IS AUTO-GENERATED FROM %s\n", fileName) + fmt.Fprintf(&b, "// DO NOT EDIT.\n") + if *buildTags != "" { + fmt.Fprintf(&b, "// +build %s\n", *buildTags) + } + fmt.Fprintf(&b, "\n") + fmt.Fprintf(&b, "package %s\n\n", pkgName) + fmt.Fprintf(&b, "import \"time\"\n\n") + fmt.Fprintf(&b, "import \""+*fileEmbedPkgPath+"\"\n\n") + b.WriteString(imports) + fmt.Fprintf(&b, "func init() {\n\tFiles.Add(%q, %d, time.Unix(0, %d), %s(%s));\n}\n", + fileName, fileSize, fi.ModTime().UnixNano(), byteStreamType, qb) + + // gofmt it + fset := token.NewFileSet() + ast, err := parser.ParseFile(fset, "", b.Bytes(), parser.ParseComments) + if err != nil { + log.Fatal(err) + } + + var clean bytes.Buffer + config := &printer.Config{ + Mode: printer.TabIndent | printer.UseSpaces, + Tabwidth: 8, + } + err = config.Fprint(&clean, fset, ast) + if err != nil { + log.Fatal(err) + } + + if err := writeFileIfDifferent(embedName, clean.Bytes()); err != nil { + log.Fatal(err) + } + } +} + +func writeFileIfDifferent(filename string, contents []byte) error { + fi, err := os.Stat(filename) + if err == nil && fi.Size() == int64(len(contents)) && contentsEqual(filename, contents) { + os.Chtimes(filename, time.Now(), time.Now()) + return nil + } + return ioutil.WriteFile(filename, contents, 0644) +} + +func contentsEqual(filename string, contents []byte) bool { + got, err := ioutil.ReadFile(filename) + if err != nil { + return false + } + return bytes.Equal(got, contents) +} + +func compressFile(r io.Reader) ([]byte, int64) { + var zb bytes.Buffer + w := zlib.NewWriter(&zb) + n, err := io.Copy(w, r) + if err != nil { + log.Fatal(err) + } + w.Close() + return zb.Bytes(), n +} + +func quote(bs []byte) []byte { + var qb bytes.Buffer + qb.WriteString(`fileembed.JoinStrings("`) + run := 0 + concatCount := 0 + for _, b := range bs { + if b == '\n' { + qb.WriteString(`\n`) + } + if b == '\n' || run > 80 { + // Prevent too many strings from being concatenated together. + // See https://code.google.com/p/go/issues/detail?id=8240 + concatCount++ + if concatCount < 50 { + qb.WriteString("\" +\n\t\"") + } else { + concatCount = 0 + qb.WriteString("\",\n\t\"") + } + run = 0 + } + if b == '\n' { + continue + } + run++ + if b == '\\' { + qb.WriteString(`\\`) + continue + } + if b == '"' { + qb.WriteString(`\"`) + continue + } + if (b >= 32 && b <= 126) || b == '\t' { + qb.WriteByte(b) + continue + } + fmt.Fprintf(&qb, "\\x%02x", b) + } + qb.WriteString(`")`) + return qb.Bytes() +} + +// matchingFiles finds all files matching a regex that should be embedded. This +// skips files prefixed with "zembed_", since those are an implementation +// detail of the embedding process itself. +func matchingFiles(p *regexp.Regexp) []string { + var f []string + err := filepath.Walk(".", func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + n := filepath.Base(path) + if !fi.IsDir() && !strings.HasPrefix(n, "zembed_") && p.MatchString(n) { + f = append(f, path) + } + return nil + }) + if err != nil { + log.Fatalf("Error walking directory tree: %s", err) + return nil + } + return f +} + +func parseFileEmbed() (pkgName string, filePattern *regexp.Regexp, modTime time.Time, err error) { + fe, err := os.Open(*patternFilename) + if err != nil { + return + } + defer fe.Close() + + fi, err := fe.Stat() + if err != nil { + return + } + modTime = fi.ModTime() + + fs := token.NewFileSet() + astf, err := parser.ParseFile(fs, *patternFilename, fe, parser.PackageClauseOnly|parser.ParseComments) + if err != nil { + return + } + pkgName = astf.Name.Name + + if astf.Doc == nil { + err = fmt.Errorf("no package comment before the %q line", "package "+pkgName) + return + } + + pkgComment := astf.Doc.Text() + findPattern := regexp.MustCompile(`(?m)^#fileembed\s+pattern\s+(\S+)\s*$`) + m := findPattern.FindStringSubmatch(pkgComment) + if m == nil { + err = fmt.Errorf("package comment lacks line of form: #fileembed pattern ") + return + } + pattern := m[1] + filePattern, err = regexp.Compile(pattern) + if err != nil { + err = fmt.Errorf("bad regexp %q: %v", pattern, err) + return + } + return +} + +// chunksOf takes a (presumably large) file's uncompressed input, +// rolling-checksum splits it into ~514 byte chunks, compresses each, +// base64s each, and writes chunk files out, with each file just +// defining an exported fileembed.Opener variable named C where +// xxxx is the first 8 lowercase hex digits of the SHA-1 of the chunk +// value pre-compression. The return value is a Go expression +// referencing each of those chunks concatenated together. +func chunksOf(in []byte) (stringExpression []byte) { + var multiParts [][]byte + rs := rollsum.New() + const nBits = 9 // ~512 byte chunks + last := 0 + for i, b := range in { + rs.Roll(b) + if rs.OnSplitWithBits(nBits) || i == len(in)-1 { + raw := in[last : i+1] // inclusive + last = i + 1 + s1 := sha1.New() + s1.Write(raw) + sha1hex := fmt.Sprintf("%x", s1.Sum(nil))[:8] + writeChunkFile(sha1hex, raw) + multiParts = append(multiParts, []byte(fmt.Sprintf("chunkpkg.C%s", sha1hex))) + } + } + return bytes.Join(multiParts, []byte(",\n\t")) +} + +func writeChunkFile(hex string, raw []byte) { + path := os.Getenv("GOPATH") + if path == "" { + log.Fatalf("No GOPATH set") + } + path = filepath.SplitList(path)[0] + file := filepath.Join(path, "src", filepath.FromSlash(*chunkPackage), "chunk_"+hex+".go") + zb, _ := compressFile(bytes.NewReader(raw)) + var buf bytes.Buffer + buf.WriteString("// THIS FILE IS AUTO-GENERATED. SEE README.\n\n") + buf.WriteString("package chunkpkg\n") + buf.WriteString("import \"" + *fileEmbedPkgPath + "\"\n\n") + fmt.Fprintf(&buf, "var C%s fileembed.Opener\n\nfunc init() { C%s = fileembed.ZlibCompressedBase64(%s)\n }\n", + hex, + hex, + quote([]byte(base64.StdEncoding.EncodeToString(zb)))) + err := writeFileIfDifferent(file, buf.Bytes()) + if err != nil { + log.Fatalf("Error writing chunk %s to %v: %v", hex, file, err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/at.go b/vendor/github.com/camlistore/camlistore/pkg/fs/at.go new file mode 100644 index 00000000..3ae3ff0f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/at.go @@ -0,0 +1,113 @@ +// +build linux darwin + +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "log" + "os" + + "camlistore.org/third_party/bazil.org/fuse" + fusefs "camlistore.org/third_party/bazil.org/fuse/fs" +) + +type atDir struct { + noXattr + fs *CamliFileSystem +} + +func (n *atDir) Attr() fuse.Attr { + return fuse.Attr{ + Mode: os.ModeDir | 0500, + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + } +} + +func (n *atDir) ReadDir(intr fusefs.Intr) ([]fuse.Dirent, fuse.Error) { + return []fuse.Dirent{ + {Name: "README.txt"}, + }, nil +} + +const atReadme = `You are now in the "at" filesystem, where you can look into the past. + +Locations in the top-level of this directory are dynamically created +as you request them. A dynamic directory is designated by a +timestamp. Once you enter a directory, you'll have a read-only view +of all of the roots that existed as of the specified time. + +Example: + +If you had a root called "importantstuff" and a file in it called +"todo.txt", you can look at the contents of that file as it existed +back before Christmas like this (from the location you mounted +camlistore): + + cat at/2013-12-24/importantstuff/todo.txt + +If you cd into "at/2013-12-24/importantstuff" you can also see all the +files that you deleted since (but none that were created after). + +Timestamps are specified in UTC unless otherwise specified, and may be +in any of the following forms: + +With Nanosecond Granularity + +* 2012-08-28T21:24:35.37465188Z - RFC3339 (this is the canonical format) +* 1346189075374651880 - nanoseconds since 1970-1-1 + +With Millisecond Granularity + +* 1346189075374 - milliseconds since 1970-1-1, common in java + +With Second Granularity + +* 1346189075 - seconds since 1970-1-1, common in unix +* 2012-08-28T21:24:35Z - RFC3339 +* 2012-08-28T21:24:35-08:00 - RFC3339 with numeric timezone +* Tue, 28 Aug 2012 21:24:35 +0000 - RFC1123 + numeric timezone +* Tue, 28 Aug 2012 21:24:35 UTC RFC1123 +* Tue Aug 28 21:24:35 UTC 2012 - Unix date +* Tue Aug 28 21:24:35 2012 - ansi C timestamp +* Tue Aug 28 21:24:35 +0000 2012 - ruby datestamp + +With More Coarse Granularities + +* 2012-08-28T21:24 (This will be considered the same as 2012-08-28T21:24:00Z) +* 2012-08-28T21 (This will be considered the same as 2012-08-28T21:00:00Z) +* 2012-08-28 (This will be considered the same as 2012-08-28T00:00:00Z) +* 2012-08 (This will be considered the same as 2012-08-01T00:00:00Z) +* 2012 (This will be considered the same as 2012-01-01T00:00:00Z) +` + +func (n *atDir) Lookup(name string, intr fusefs.Intr) (fusefs.Node, fuse.Error) { + log.Printf("fs.atDir: Lookup(%q)", name) + + if name == "README.txt" { + return staticFileNode(atReadme), nil + } + + asOf, err := parseTime(name) + if err != nil { + log.Printf("Can't parse time: %v", err) + return nil, fuse.ENOENT + } + + return &rootsDir{fs: n.fs, at: asOf}, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/debug.go b/vendor/github.com/camlistore/camlistore/pkg/fs/debug.go new file mode 100644 index 00000000..d3877437 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/debug.go @@ -0,0 +1,139 @@ +// +build linux darwin + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "bytes" + "fmt" + "os" + "strconv" + + "camlistore.org/pkg/types" + + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs" +) + +// If TrackStats is true, statistics are kept on operations. +var TrackStats bool + +func init() { + TrackStats, _ = strconv.ParseBool(os.Getenv("CAMLI_TRACK_FS_STATS")) +} + +var ( + mutFileOpen = newStat("mutfile-open") + mutFileOpenError = newStat("mutfile-open-error") + mutFileOpenRO = newStat("mutfile-open-ro") + mutFileOpenRW = newStat("mutfile-open-rw") + roFileOpen = newStat("rofile-open") + roFileOpenError = newStat("rofile-open-error") +) + +var statByName = map[string]*stat{} + +func newStat(name string) *stat { + if statByName[name] != nil { + panic("duplicate registraton of " + name) + } + s := &stat{name: name} + statByName[name] = s + return s +} + +// A stat is a wrapper around an atomic int64, as is a fuse.Node +// exporting that data as a decimal. +type stat struct { + n types.AtomicInt64 + name string +} + +func (s *stat) Incr() { + if TrackStats { + s.n.Add(1) + } +} + +func (s *stat) content() []byte { + var buf bytes.Buffer + fmt.Fprintf(&buf, "%d", s.n.Get()) + buf.WriteByte('\n') + return buf.Bytes() +} + +func (s *stat) Attr() fuse.Attr { + return fuse.Attr{ + Mode: 0400, + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + Size: uint64(len(s.content())), + Mtime: serverStart, + Ctime: serverStart, + Crtime: serverStart, + } +} + +func (s *stat) Open(req *fuse.OpenRequest, res *fuse.OpenResponse, intr fs.Intr) (fs.Handle, fuse.Error) { + // Set DirectIO to keep this file from being cached in OS X's kernel. + res.Flags |= fuse.OpenDirectIO + return s, nil +} + +func (s *stat) Read(req *fuse.ReadRequest, res *fuse.ReadResponse, intr fs.Intr) fuse.Error { + c := s.content() + if req.Offset > int64(len(c)) { + return nil + } + c = c[req.Offset:] + size := req.Size + if size > len(c) { + size = len(c) + } + res.Data = make([]byte, size) + copy(res.Data, c) + return nil +} + +// A statsDir FUSE directory node is returned by root.go, by opening +// ".camli_fs_stats" in the root directory. +type statsDir struct{} + +func (statsDir) Attr() fuse.Attr { + return fuse.Attr{ + Mode: os.ModeDir | 0700, + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + } +} + +func (statsDir) ReadDir(intr fs.Intr) (ents []fuse.Dirent, err fuse.Error) { + for k := range statByName { + ents = append(ents, fuse.Dirent{Name: k}) + } + return +} + +func (statsDir) Lookup(req *fuse.LookupRequest, res *fuse.LookupResponse, intr fs.Intr) (fs.Node, fuse.Error) { + name := req.Name + s, ok := statByName[name] + if !ok { + return nil, fuse.ENOENT + } + return s, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/fs.go b/vendor/github.com/camlistore/camlistore/pkg/fs/fs.go new file mode 100644 index 00000000..82453b88 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/fs.go @@ -0,0 +1,424 @@ +// +build linux darwin + +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package fs implements a FUSE filesystem for Camlistore and is +// used by the cammount binary. +package fs + +import ( + "fmt" + "io" + "log" + "os" + "sync" + "syscall" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/client" + "camlistore.org/pkg/lru" + "camlistore.org/pkg/schema" + + "camlistore.org/third_party/bazil.org/fuse" + fusefs "camlistore.org/third_party/bazil.org/fuse/fs" +) + +var serverStart = time.Now() + +var errNotDir = fuse.Errno(syscall.ENOTDIR) + +type CamliFileSystem struct { + fetcher blob.Fetcher + client *client.Client // or nil, if not doing search queries + root fusefs.Node + + // IgnoreOwners, if true, collapses all file ownership to the + // uid/gid running the fuse filesystem, and sets all the + // permissions to 0600/0700. + IgnoreOwners bool + + blobToSchema *lru.Cache // ~map[blobstring]*schema.Blob + nameToBlob *lru.Cache // ~map[string]blob.Ref + nameToAttr *lru.Cache // ~map[string]*fuse.Attr +} + +var _ fusefs.FS = (*CamliFileSystem)(nil) + +func newCamliFileSystem(fetcher blob.Fetcher) *CamliFileSystem { + return &CamliFileSystem{ + fetcher: fetcher, + blobToSchema: lru.New(1024), // arbitrary; TODO: tunable/smarter? + nameToBlob: lru.New(1024), // arbitrary: TODO: tunable/smarter? + nameToAttr: lru.New(1024), // arbitrary: TODO: tunable/smarter? + } +} + +// NewDefaultCamliFileSystem returns a filesystem with a generic base, from which +// users can navigate by blobref, tag, date, etc. +func NewDefaultCamliFileSystem(client *client.Client, fetcher blob.Fetcher) *CamliFileSystem { + if client == nil || fetcher == nil { + panic("nil argument") + } + fs := newCamliFileSystem(fetcher) + fs.root = &root{fs: fs} // root.go + fs.client = client + return fs +} + +// NewRootedCamliFileSystem returns a CamliFileSystem with a node based on a blobref +// as its base. +func NewRootedCamliFileSystem(cli *client.Client, fetcher blob.Fetcher, root blob.Ref) (*CamliFileSystem, error) { + fs := newCamliFileSystem(fetcher) + fs.client = cli + + n, err := fs.newNodeFromBlobRef(root) + + if err != nil { + return nil, err + } + + fs.root = n + + return fs, nil +} + +// node implements fuse.Node with a read-only Camli "file" or +// "directory" blob. +type node struct { + noXattr + fs *CamliFileSystem + blobref blob.Ref + + pnodeModTime time.Time // optionally set by recent.go; modtime of permanode + + dmu sync.Mutex // guards dirents. acquire before mu. + dirents []fuse.Dirent // nil until populated once + + mu sync.Mutex // guards rest + attr fuse.Attr + meta *schema.Blob + lookMap map[string]blob.Ref +} + +func (n *node) Attr() (attr fuse.Attr) { + _, err := n.schema() + if err != nil { + // Hm, can't return it. Just log it I guess. + log.Printf("error fetching schema superset for %v: %v", n.blobref, err) + } + return n.attr +} + +func (n *node) addLookupEntry(name string, ref blob.Ref) { + n.mu.Lock() + defer n.mu.Unlock() + if n.lookMap == nil { + n.lookMap = make(map[string]blob.Ref) + } + n.lookMap[name] = ref +} + +func (n *node) Lookup(name string, intr fusefs.Intr) (fusefs.Node, fuse.Error) { + if name == ".quitquitquit" { + // TODO: only in dev mode + log.Fatalf("Shutting down due to .quitquitquit lookup.") + } + + // If we haven't done Readdir yet (dirents isn't set), then force a Readdir + // call to populate lookMap. + n.dmu.Lock() + loaded := n.dirents != nil + n.dmu.Unlock() + if !loaded { + n.ReadDir(nil) + } + + n.mu.Lock() + defer n.mu.Unlock() + ref, ok := n.lookMap[name] + if !ok { + return nil, fuse.ENOENT + } + return &node{fs: n.fs, blobref: ref}, nil +} + +func (n *node) schema() (*schema.Blob, error) { + // TODO: use singleflight library here instead of a lock? + n.mu.Lock() + defer n.mu.Unlock() + if n.meta != nil { + return n.meta, nil + } + blob, err := n.fs.fetchSchemaMeta(n.blobref) + if err == nil { + n.meta = blob + n.populateAttr() + } + return blob, err +} + +func isWriteFlags(flags fuse.OpenFlags) bool { + // TODO read/writeness are not flags, use O_ACCMODE + return flags&fuse.OpenFlags(os.O_WRONLY|os.O_RDWR|os.O_APPEND|os.O_CREATE) != 0 +} + +func (n *node) Open(req *fuse.OpenRequest, res *fuse.OpenResponse, intr fusefs.Intr) (fusefs.Handle, fuse.Error) { + log.Printf("CAMLI Open on %v: %#v", n.blobref, req) + if isWriteFlags(req.Flags) { + return nil, fuse.EPERM + } + ss, err := n.schema() + if err != nil { + log.Printf("open of %v: %v", n.blobref, err) + return nil, fuse.EIO + } + if ss.Type() == "directory" { + return n, nil + } + fr, err := ss.NewFileReader(n.fs.fetcher) + if err != nil { + // Will only happen if ss.Type != "file" or "bytes" + log.Printf("NewFileReader(%s) = %v", n.blobref, err) + return nil, fuse.EIO + } + return &nodeReader{n: n, fr: fr}, nil +} + +type nodeReader struct { + n *node + fr *schema.FileReader +} + +func (nr *nodeReader) Read(req *fuse.ReadRequest, res *fuse.ReadResponse, intr fusefs.Intr) fuse.Error { + log.Printf("CAMLI nodeReader READ on %v: %#v", nr.n.blobref, req) + if req.Offset >= nr.fr.Size() { + return nil + } + size := req.Size + if int64(size)+req.Offset >= nr.fr.Size() { + size -= int((int64(size) + req.Offset) - nr.fr.Size()) + } + buf := make([]byte, size) + n, err := nr.fr.ReadAt(buf, req.Offset) + if err == io.EOF { + err = nil + } + if err != nil { + log.Printf("camli read on %v at %d: %v", nr.n.blobref, req.Offset, err) + return fuse.EIO + } + res.Data = buf[:n] + return nil +} + +func (nr *nodeReader) Release(req *fuse.ReleaseRequest, intr fusefs.Intr) fuse.Error { + log.Printf("CAMLI nodeReader RELEASE on %v", nr.n.blobref) + nr.fr.Close() + return nil +} + +func (n *node) ReadDir(intr fusefs.Intr) ([]fuse.Dirent, fuse.Error) { + log.Printf("CAMLI ReadDir on %v", n.blobref) + n.dmu.Lock() + defer n.dmu.Unlock() + if n.dirents != nil { + return n.dirents, nil + } + + ss, err := n.schema() + if err != nil { + log.Printf("camli.ReadDir error on %v: %v", n.blobref, err) + return nil, fuse.EIO + } + dr, err := schema.NewDirReader(n.fs.fetcher, ss.BlobRef()) + if err != nil { + log.Printf("camli.ReadDir error on %v: %v", n.blobref, err) + return nil, fuse.EIO + } + schemaEnts, err := dr.Readdir(-1) + if err != nil { + log.Printf("camli.ReadDir error on %v: %v", n.blobref, err) + return nil, fuse.EIO + } + n.dirents = make([]fuse.Dirent, 0) + for _, sent := range schemaEnts { + if name := sent.FileName(); name != "" { + n.addLookupEntry(name, sent.BlobRef()) + n.dirents = append(n.dirents, fuse.Dirent{Name: name}) + } + } + return n.dirents, nil +} + +// populateAttr should only be called once n.ss is known to be set and +// non-nil +func (n *node) populateAttr() error { + meta := n.meta + + n.attr.Mode = meta.FileMode() + + if n.fs.IgnoreOwners { + n.attr.Uid = uint32(os.Getuid()) + n.attr.Gid = uint32(os.Getgid()) + executeBit := n.attr.Mode & 0100 + n.attr.Mode = (n.attr.Mode ^ n.attr.Mode.Perm()) | 0400 | executeBit + } else { + n.attr.Uid = uint32(meta.MapUid()) + n.attr.Gid = uint32(meta.MapGid()) + } + + // TODO: inode? + + if mt := meta.ModTime(); !mt.IsZero() { + n.attr.Mtime = mt + } else { + n.attr.Mtime = n.pnodeModTime + } + + switch meta.Type() { + case "file": + n.attr.Size = uint64(meta.PartsSize()) + n.attr.Blocks = 0 // TODO: set? + n.attr.Mode |= 0400 + case "directory": + n.attr.Mode |= 0500 + case "symlink": + n.attr.Mode |= 0400 + default: + log.Printf("unknown attr ss.Type %q in populateAttr", meta.Type()) + } + return nil +} + +func (fs *CamliFileSystem) Root() (fusefs.Node, fuse.Error) { + return fs.root, nil +} + +func (fs *CamliFileSystem) Statfs(req *fuse.StatfsRequest, res *fuse.StatfsResponse, intr fusefs.Intr) fuse.Error { + // Make some stuff up, just to see if it makes "lsof" happy. + res.Blocks = 1 << 35 + res.Bfree = 1 << 34 + res.Bavail = 1 << 34 + res.Files = 1 << 29 + res.Ffree = 1 << 28 + res.Namelen = 2048 + res.Bsize = 1024 + return nil +} + +// Errors returned are: +// os.ErrNotExist -- blob not found +// os.ErrInvalid -- not JSON or a camli schema blob +func (fs *CamliFileSystem) fetchSchemaMeta(br blob.Ref) (*schema.Blob, error) { + blobStr := br.String() + if blob, ok := fs.blobToSchema.Get(blobStr); ok { + return blob.(*schema.Blob), nil + } + + rc, _, err := fs.fetcher.Fetch(br) + if err != nil { + return nil, err + } + defer rc.Close() + blob, err := schema.BlobFromReader(br, rc) + if err != nil { + log.Printf("Error parsing %s as schema blob: %v", br, err) + return nil, os.ErrInvalid + } + if blob.Type() == "" { + log.Printf("blob %s is JSON but lacks camliType", br) + return nil, os.ErrInvalid + } + fs.blobToSchema.Add(blobStr, blob) + return blob, nil +} + +// consolated logic for determining a node to mount based on an arbitrary blobref +func (fs *CamliFileSystem) newNodeFromBlobRef(root blob.Ref) (fusefs.Node, error) { + blob, err := fs.fetchSchemaMeta(root) + if err != nil { + return nil, err + } + + switch blob.Type() { + case "directory": + n := &node{fs: fs, blobref: root, meta: blob} + n.populateAttr() + return n, nil + + case "permanode": + // other mutDirs listed in the default fileystem have names and are displayed + return &mutDir{fs: fs, permanode: root, name: "-"}, nil + } + + return nil, fmt.Errorf("Blobref must be of a directory or permanode got a %v", blob.Type()) +} + +type notImplementDirNode struct{ noXattr } + +func (notImplementDirNode) Attr() fuse.Attr { + return fuse.Attr{ + Mode: os.ModeDir | 0000, + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + } +} + +type staticFileNode string + +func (s staticFileNode) Attr() fuse.Attr { + return fuse.Attr{ + Mode: 0400, + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + Size: uint64(len(s)), + Mtime: serverStart, + Ctime: serverStart, + Crtime: serverStart, + } +} + +func (s staticFileNode) Read(req *fuse.ReadRequest, res *fuse.ReadResponse, intr fusefs.Intr) fuse.Error { + if req.Offset > int64(len(s)) { + return nil + } + s = s[req.Offset:] + size := req.Size + if size > len(s) { + size = len(s) + } + res.Data = make([]byte, size) + copy(res.Data, s) + return nil +} + +func (n staticFileNode) Getxattr(*fuse.GetxattrRequest, *fuse.GetxattrResponse, fusefs.Intr) fuse.Error { + return fuse.ENODATA +} + +func (n staticFileNode) Listxattr(*fuse.ListxattrRequest, *fuse.ListxattrResponse, fusefs.Intr) fuse.Error { + return nil +} + +func (n staticFileNode) Setxattr(*fuse.SetxattrRequest, fusefs.Intr) fuse.Error { + return fuse.EPERM +} + +func (n staticFileNode) Removexattr(*fuse.RemovexattrRequest, fusefs.Intr) fuse.Error { + return fuse.EPERM +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/fs_test.go b/vendor/github.com/camlistore/camlistore/pkg/fs/fs_test.go new file mode 100644 index 00000000..cb9450fd --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/fs_test.go @@ -0,0 +1,710 @@ +// +build linux darwin + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "reflect" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "testing" + "time" + + "camlistore.org/pkg/test" + "camlistore.org/third_party/bazil.org/fuse/syscallx" +) + +var ( + errmu sync.Mutex + lasterr error +) + +func condSkip(t *testing.T) { + errmu.Lock() + defer errmu.Unlock() + if lasterr != nil { + t.Skipf("Skipping test; some other test already failed.") + } + if !(runtime.GOOS == "darwin" || runtime.GOOS == "linux") { + t.Skipf("Skipping test on OS %q", runtime.GOOS) + } + if runtime.GOOS == "darwin" { + _, err := os.Stat("/Library/Filesystems/osxfusefs.fs/Support/mount_osxfusefs") + if os.IsNotExist(err) { + test.DependencyErrorOrSkip(t) + } else if err != nil { + t.Fatal(err) + } + } +} + +func brokenTest(t *testing.T) { + if v, _ := strconv.ParseBool(os.Getenv("RUN_BROKEN_TESTS")); !v { + t.Skipf("Skipping broken tests without RUN_BROKEN_TESTS=1") + } +} + +type mountEnv struct { + t *testing.T + mountPoint string + process *os.Process +} + +func (e *mountEnv) Stat(s *stat) int64 { + file := filepath.Join(e.mountPoint, ".camli_fs_stats", s.name) + slurp, err := ioutil.ReadFile(file) + if err != nil { + e.t.Fatal(err) + } + slurp = bytes.TrimSpace(slurp) + v, err := strconv.ParseInt(string(slurp), 10, 64) + if err != nil { + e.t.Fatalf("unexpected value %q in file %s", slurp, file) + } + return v +} + +func testName() string { + skip := 0 + for { + pc, _, _, ok := runtime.Caller(skip) + skip++ + if !ok { + panic("Failed to find test name") + } + name := strings.TrimPrefix(runtime.FuncForPC(pc).Name(), "camlistore.org/pkg/fs.") + if strings.HasPrefix(name, "Test") { + return name + } + } +} + +func inEmptyMutDir(t *testing.T, fn func(env *mountEnv, dir string)) { + cammountTest(t, func(env *mountEnv) { + dir := filepath.Join(env.mountPoint, "roots", testName()) + if err := os.Mkdir(dir, 0755); err != nil { + t.Fatalf("Failed to make roots/r dir: %v", err) + } + fi, err := os.Stat(dir) + if err != nil || !fi.IsDir() { + t.Fatalf("Stat of %s dir = %v, %v; want a directory", dir, fi, err) + } + fn(env, dir) + }) +} + +func cammountTest(t *testing.T, fn func(env *mountEnv)) { + dupLog := io.MultiWriter(os.Stderr, testLog{t}) + log.SetOutput(dupLog) + defer log.SetOutput(os.Stderr) + + w := test.GetWorld(t) + mountPoint, err := ioutil.TempDir("", "fs-test-mount") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.RemoveAll(mountPoint); err != nil { + t.Fatal(err) + } + }() + verbose := "false" + var stderrDest io.Writer = ioutil.Discard + if v, _ := strconv.ParseBool(os.Getenv("VERBOSE_FUSE")); v { + verbose = "true" + stderrDest = testLog{t} + } + if v, _ := strconv.ParseBool(os.Getenv("VERBOSE_FUSE_STDERR")); v { + stderrDest = io.MultiWriter(stderrDest, os.Stderr) + } + + mount := w.Cmd("cammount", "--debug="+verbose, mountPoint) + mount.Stderr = stderrDest + mount.Env = append(mount.Env, "CAMLI_TRACK_FS_STATS=1") + + stdin, err := mount.StdinPipe() + if err != nil { + t.Fatal(err) + } + if err := w.Ping(); err != nil { + t.Fatal(err) + } + if err := mount.Start(); err != nil { + t.Fatal(err) + } + waitc := make(chan error, 1) + go func() { waitc <- mount.Wait() }() + defer func() { + log.Printf("Sending quit") + stdin.Write([]byte("q\n")) + select { + case <-time.After(5 * time.Second): + log.Printf("timeout waiting for cammount to finish") + mount.Process.Kill() + Unmount(mountPoint) + case err := <-waitc: + log.Printf("cammount exited: %v", err) + } + if !test.WaitFor(not(dirToBeFUSE(mountPoint)), 5*time.Second, 1*time.Second) { + // It didn't unmount. Try again. + Unmount(mountPoint) + } + }() + + if !test.WaitFor(dirToBeFUSE(mountPoint), 5*time.Second, 100*time.Millisecond) { + t.Fatalf("error waiting for %s to be mounted", mountPoint) + } + fn(&mountEnv{ + t: t, + mountPoint: mountPoint, + process: mount.Process, + }) + +} + +func TestRoot(t *testing.T) { + condSkip(t) + cammountTest(t, func(env *mountEnv) { + f, err := os.Open(env.mountPoint) + if err != nil { + t.Fatal(err) + } + defer f.Close() + names, err := f.Readdirnames(-1) + if err != nil { + t.Fatal(err) + } + sort.Strings(names) + want := []string{"WELCOME.txt", "at", "date", "recent", "roots", "sha1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "tag"} + if !reflect.DeepEqual(names, want) { + t.Errorf("root directory = %q; want %q", names, want) + } + }) +} + +type testLog struct { + t *testing.T +} + +func (tl testLog) Write(p []byte) (n int, err error) { + tl.t.Log(strings.TrimSpace(string(p))) + return len(p), nil +} + +func TestMutable(t *testing.T) { + condSkip(t) + inEmptyMutDir(t, func(env *mountEnv, rootDir string) { + filename := filepath.Join(rootDir, "x") + f, err := os.Create(filename) + if err != nil { + t.Fatalf("Create: %v", err) + } + if err := f.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + fi, err := os.Stat(filename) + if err != nil { + t.Errorf("Stat error: %v", err) + } else if !fi.Mode().IsRegular() || fi.Size() != 0 { + t.Errorf("Stat of roots/r/x = %v size %d; want a %d byte regular file", fi.Mode(), fi.Size(), 0) + } + + for _, str := range []string{"foo, ", "bar\n", "another line.\n"} { + f, err = os.OpenFile(filename, os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + t.Fatalf("OpenFile: %v", err) + } + if _, err := f.Write([]byte(str)); err != nil { + t.Logf("Error with append: %v", err) + t.Fatalf("Error appending %q to %s: %v", str, filename, err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + } + ro0 := env.Stat(mutFileOpenRO) + slurp, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + if env.Stat(mutFileOpenRO)-ro0 != 1 { + t.Error("Read didn't trigger read-only path optimization.") + } + + const want = "foo, bar\nanother line.\n" + fi, err = os.Stat(filename) + if err != nil { + t.Errorf("Stat error: %v", err) + } else if !fi.Mode().IsRegular() || fi.Size() != int64(len(want)) { + t.Errorf("Stat of roots/r/x = %v size %d; want a %d byte regular file", fi.Mode(), fi.Size(), len(want)) + } + if got := string(slurp); got != want { + t.Fatalf("contents = %q; want %q", got, want) + } + + // Delete it. + if err := os.Remove(filename); err != nil { + t.Fatal(err) + } + + // Gone? + if _, err := os.Stat(filename); !os.IsNotExist(err) { + t.Fatalf("expected file to be gone; got stat err = %v instead", err) + } + }) +} + +func TestDifferentWriteTypes(t *testing.T) { + condSkip(t) + inEmptyMutDir(t, func(env *mountEnv, rootDir string) { + filename := filepath.Join(rootDir, "big") + + writes := []struct { + name string + flag int + write []byte // if non-nil, Write is called + writeAt []byte // if non-nil, WriteAt is used + writePos int64 // writeAt position + want string // shortenString of remaining file + }{ + { + name: "write 8k of a", + flag: os.O_RDWR | os.O_CREATE | os.O_TRUNC, + write: bytes.Repeat([]byte("a"), 8<<10), + want: "a{8192}", + }, + { + name: "writeAt HI at offset 10", + flag: os.O_RDWR, + writeAt: []byte("HI"), + writePos: 10, + want: "a{10}HIa{8180}", + }, + { + name: "append single C", + flag: os.O_WRONLY | os.O_APPEND, + write: []byte("C"), + want: "a{10}HIa{8180}C", + }, + { + name: "append 8k of b", + flag: os.O_WRONLY | os.O_APPEND, + write: bytes.Repeat([]byte("b"), 8<<10), + want: "a{10}HIa{8180}Cb{8192}", + }, + } + + for _, wr := range writes { + f, err := os.OpenFile(filename, wr.flag, 0644) + if err != nil { + t.Fatalf("%s: OpenFile: %v", wr.name, err) + } + if wr.write != nil { + if n, err := f.Write(wr.write); err != nil || n != len(wr.write) { + t.Fatalf("%s: Write = (%v, %v); want (%d, nil)", wr.name, n, err, len(wr.write)) + } + } + if wr.writeAt != nil { + if n, err := f.WriteAt(wr.writeAt, wr.writePos); err != nil || n != len(wr.writeAt) { + t.Fatalf("%s: WriteAt = (%v, %v); want (%d, nil)", wr.name, n, err, len(wr.writeAt)) + } + } + if err := f.Close(); err != nil { + t.Fatalf("%s: Close: %v", wr.name, err) + } + + slurp, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatalf("%s: Slurp: %v", wr.name, err) + } + if got := shortenString(string(slurp)); got != wr.want { + t.Fatalf("%s: afterwards, file = %q; want %q", wr.name, got, wr.want) + } + + } + + // Delete it. + if err := os.Remove(filename); err != nil { + t.Fatal(err) + } + }) +} + +func statStr(name string) string { + fi, err := os.Stat(name) + if os.IsNotExist(err) { + return "ENOENT" + } + if err != nil { + return "err=" + err.Error() + } + return fmt.Sprintf("file %v, size %d", fi.Mode(), fi.Size()) +} + +func TestRename(t *testing.T) { + condSkip(t) + inEmptyMutDir(t, func(env *mountEnv, rootDir string) { + name1 := filepath.Join(rootDir, "1") + name2 := filepath.Join(rootDir, "2") + subdir := filepath.Join(rootDir, "dir") + name3 := filepath.Join(subdir, "3") + + contents := []byte("Some file contents") + const gone = "ENOENT" + const reg = "file -rw-------, size 18" + + if err := ioutil.WriteFile(name1, contents, 0644); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(subdir, 0755); err != nil { + t.Fatal(err) + } + + if got, want := statStr(name1), reg; got != want { + t.Errorf("name1 = %q; want %q", got, want) + } + if err := os.Rename(name1, name2); err != nil { + t.Fatal(err) + } + if got, want := statStr(name1), gone; got != want { + t.Errorf("name1 = %q; want %q", got, want) + } + if got, want := statStr(name2), reg; got != want { + t.Errorf("name2 = %q; want %q", got, want) + } + + // Moving to a different directory. + if err := os.Rename(name2, name3); err != nil { + t.Fatal(err) + } + if got, want := statStr(name2), gone; got != want { + t.Errorf("name2 = %q; want %q", got, want) + } + if got, want := statStr(name3), reg; got != want { + t.Errorf("name3 = %q; want %q", got, want) + } + }) +} + +func parseXattrList(from []byte) map[string]bool { + attrNames := bytes.Split(from, []byte{0}) + m := map[string]bool{} + for _, nm := range attrNames { + if len(nm) == 0 { + continue + } + m[string(nm)] = true + } + return m +} + +func TestXattr(t *testing.T) { + condSkip(t) + inEmptyMutDir(t, func(env *mountEnv, rootDir string) { + name1 := filepath.Join(rootDir, "1") + attr1 := "attr1" + attr2 := "attr2" + + contents := []byte("Some file contents") + + if err := ioutil.WriteFile(name1, contents, 0644); err != nil { + t.Fatal(err) + } + + buf := make([]byte, 8192) + // list empty + n, err := syscallx.Listxattr(name1, buf) + if err != nil { + t.Errorf("Error in initial listxattr: %v", err) + } + if n != 0 { + t.Errorf("Expected zero-length xattr list, got %q", buf[:n]) + } + + // get missing + n, err = syscallx.Getxattr(name1, attr1, buf) + if err == nil { + t.Errorf("Expected error getting non-existent xattr, got %q", buf[:n]) + } + + // Set (two different attributes) + err = syscallx.Setxattr(name1, attr1, []byte("hello1"), 0) + if err != nil { + t.Fatalf("Error setting xattr: %v", err) + } + err = syscallx.Setxattr(name1, attr2, []byte("hello2"), 0) + if err != nil { + t.Fatalf("Error setting xattr: %v", err) + } + // Alternate value for first attribute + err = syscallx.Setxattr(name1, attr1, []byte("hello1a"), 0) + if err != nil { + t.Fatalf("Error setting xattr: %v", err) + } + + // list attrs + n, err = syscallx.Listxattr(name1, buf) + if err != nil { + t.Errorf("Error in initial listxattr: %v", err) + } + m := parseXattrList(buf[:n]) + if !(len(m) == 2 && m[attr1] && m[attr2]) { + t.Errorf("Missing an attribute: %q", buf[:n]) + } + + // Remove attr + err = syscallx.Removexattr(name1, attr2) + if err != nil { + t.Errorf("Failed to remove attr: %v", err) + } + + // List attrs + n, err = syscallx.Listxattr(name1, buf) + if err != nil { + t.Errorf("Error in initial listxattr: %v", err) + } + m = parseXattrList(buf[:n]) + if !(len(m) == 1 && m[attr1]) { + t.Errorf("Missing an attribute: %q", buf[:n]) + } + + // Get remaining attr + n, err = syscallx.Getxattr(name1, attr1, buf) + if err != nil { + t.Errorf("Error getting attr1: %v", err) + } + if string(buf[:n]) != "hello1a" { + t.Logf("Expected hello1a, got %q", buf[:n]) + } + }) +} + +func TestSymlink(t *testing.T) { + condSkip(t) + // Do it all once, unmount, re-mount and then check again. + // TODO(bradfitz): do this same pattern (unmount and remount) in the other tests. + var suffix string + var link string + const target = "../../some-target" // arbitrary string. some-target is fake. + check := func() { + fi, err := os.Lstat(link) + if err != nil { + t.Fatalf("Stat: %v", err) + } + if fi.Mode()&os.ModeSymlink == 0 { + t.Errorf("Mode = %v; want Symlink bit set", fi.Mode()) + } + got, err := os.Readlink(link) + if err != nil { + t.Fatalf("Readlink: %v", err) + } + if got != target { + t.Errorf("ReadLink = %q; want %q", got, target) + } + } + inEmptyMutDir(t, func(env *mountEnv, rootDir string) { + // Save for second test: + link = filepath.Join(rootDir, "some-link") + suffix = strings.TrimPrefix(link, env.mountPoint) + + if err := os.Symlink(target, link); err != nil { + t.Fatalf("Symlink: %v", err) + } + t.Logf("Checking in first process...") + check() + }) + cammountTest(t, func(env *mountEnv) { + t.Logf("Checking in second process...") + link = env.mountPoint + suffix + check() + }) +} + +func TestFinderCopy(t *testing.T) { + if runtime.GOOS != "darwin" { + t.Skipf("Skipping Darwin-specific test.") + } + condSkip(t) + inEmptyMutDir(t, func(env *mountEnv, destDir string) { + f, err := ioutil.TempFile("", "finder-copy-file") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + want := []byte("Some data for Finder to copy.") + if _, err := f.Write(want); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + + cmd := exec.Command("osascript") + script := fmt.Sprintf(` +tell application "Finder" + copy file POSIX file %q to folder POSIX file %q +end tell +`, f.Name(), destDir) + cmd.Stdin = strings.NewReader(script) + + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("Error running AppleScript: %v, %s", err, out) + } else { + t.Logf("AppleScript said: %q", out) + } + + destFile := filepath.Join(destDir, filepath.Base(f.Name())) + fi, err := os.Stat(destFile) + if err != nil { + t.Errorf("Stat = %v, %v", fi, err) + } + if fi.Size() != int64(len(want)) { + t.Errorf("Dest stat size = %d; want %d", fi.Size(), len(want)) + } + slurp, err := ioutil.ReadFile(destFile) + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + if !bytes.Equal(slurp, want) { + t.Errorf("Dest file = %q; want %q", slurp, want) + } + }) +} + +func TestTextEdit(t *testing.T) { + if testing.Short() { + t.Skipf("Skipping in short mode") + } + if runtime.GOOS != "darwin" { + t.Skipf("Skipping Darwin-specific test.") + } + condSkip(t) + inEmptyMutDir(t, func(env *mountEnv, testDir string) { + var ( + testFile = filepath.Join(testDir, "some-text-file.txt") + content1 = []byte("Some text content.") + content2 = []byte("Some replacement content.") + ) + if err := ioutil.WriteFile(testFile, content1, 0644); err != nil { + t.Fatal(err) + } + + cmd := exec.Command("osascript") + script := fmt.Sprintf(` +tell application "TextEdit" + activate + open POSIX file %q + tell front document + set paragraph 1 to %q as text + save + close + end tell +end tell +`, testFile, content2) + cmd.Stdin = strings.NewReader(script) + + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("Error running AppleScript: %v, %s", err, out) + } else { + t.Logf("AppleScript said: %q", out) + } + + fi, err := os.Stat(testFile) + if err != nil { + t.Errorf("Stat = %v, %v", fi, err) + } else if fi.Size() != int64(len(content2)) { + t.Errorf("Stat size = %d; want %d", fi.Size(), len(content2)) + } + slurp, err := ioutil.ReadFile(testFile) + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + if !bytes.Equal(slurp, content2) { + t.Errorf("File = %q; want %q", slurp, content2) + } + }) +} + +func not(cond func() bool) func() bool { + return func() bool { + return !cond() + } +} + +func dirToBeFUSE(dir string) func() bool { + return func() bool { + out, err := exec.Command("df", dir).CombinedOutput() + if err != nil { + return false + } + if runtime.GOOS == "darwin" { + if strings.Contains(string(out), "mount_osxfusefs@") { + return true + } + return false + } + if runtime.GOOS == "linux" { + return strings.Contains(string(out), "/dev/fuse") && + strings.Contains(string(out), dir) + } + return false + } +} + +// shortenString reduces any run of 5 or more identical bytes to "x{17}". +// "hello" => "hello" +// "fooooooooooooooooo" => "fo{17}" +func shortenString(v string) string { + var buf bytes.Buffer + var last byte + var run int + flush := func() { + switch { + case run == 0: + case run < 5: + for i := 0; i < run; i++ { + buf.WriteByte(last) + } + default: + buf.WriteByte(last) + fmt.Fprintf(&buf, "{%d}", run) + } + run = 0 + } + for i := 0; i < len(v); i++ { + b := v[i] + if b != last { + flush() + } + last = b + run++ + } + flush() + return buf.String() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/mut.go b/vendor/github.com/camlistore/camlistore/pkg/fs/mut.go new file mode 100644 index 00000000..97197f0b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/mut.go @@ -0,0 +1,898 @@ +// +build linux darwin + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/readerutil" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/search" + "camlistore.org/pkg/syncutil" + + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs" +) + +// How often to refresh directory nodes by reading from the blobstore. +const populateInterval = 30 * time.Second + +// How long an item that was created locally will be present +// regardless of its presence in the indexing server. +const deletionRefreshWindow = time.Minute + +type nodeType int + +const ( + fileType nodeType = iota + dirType + symlinkType +) + +// mutDir is a mutable directory. +// Its br is the permanode with camliPath:entname attributes. +type mutDir struct { + fs *CamliFileSystem + permanode blob.Ref + parent *mutDir // or nil, if the root within its roots.go root. + name string // ent name (base name within parent) + + localCreateTime time.Time // time this node was created locally (iff it was) + + mu sync.Mutex + lastPop time.Time + children map[string]mutFileOrDir + xattrs map[string][]byte + deleted bool +} + +func (m *mutDir) String() string { + return fmt.Sprintf("&mutDir{%p name=%q perm:%v}", m, m.fullPath(), m.permanode) +} + +// for debugging +func (n *mutDir) fullPath() string { + if n == nil { + return "" + } + return filepath.Join(n.parent.fullPath(), n.name) +} + +func (n *mutDir) Attr() fuse.Attr { + return fuse.Attr{ + Inode: n.permanode.Sum64(), + Mode: os.ModeDir | 0700, + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + } +} + +func (n *mutDir) Access(req *fuse.AccessRequest, intr fs.Intr) fuse.Error { + n.mu.Lock() + defer n.mu.Unlock() + if n.deleted { + return fuse.ENOENT + } + return nil +} + +func (n *mutFile) Access(req *fuse.AccessRequest, intr fs.Intr) fuse.Error { + n.mu.Lock() + defer n.mu.Unlock() + if n.deleted { + return fuse.ENOENT + } + return nil +} + +// populate hits the blobstore to populate map of child nodes. +func (n *mutDir) populate() error { + n.mu.Lock() + defer n.mu.Unlock() + + // Only re-populate if we haven't done so recently. + now := time.Now() + if n.lastPop.Add(populateInterval).After(now) { + return nil + } + n.lastPop = now + + res, err := n.fs.client.Describe(&search.DescribeRequest{ + BlobRef: n.permanode, + Depth: 3, + }) + if err != nil { + log.Println("mutDir.paths:", err) + return nil + } + db := res.Meta[n.permanode.String()] + if db == nil { + return errors.New("dir blobref not described") + } + + // Find all child permanodes and stick them in n.children + if n.children == nil { + n.children = make(map[string]mutFileOrDir) + } + currentChildren := map[string]bool{} + for k, v := range db.Permanode.Attr { + const p = "camliPath:" + if !strings.HasPrefix(k, p) || len(v) < 1 { + continue + } + name := k[len(p):] + childRef := v[0] + child := res.Meta[childRef] + if child == nil { + log.Printf("child not described: %v", childRef) + continue + } + if child.Permanode == nil { + log.Printf("invalid child, not a permanode: %v", childRef) + continue + } + if target := child.Permanode.Attr.Get("camliSymlinkTarget"); target != "" { + // This is a symlink. + n.maybeAddChild(name, child.Permanode, &mutFile{ + fs: n.fs, + permanode: blob.ParseOrZero(childRef), + parent: n, + name: name, + symLink: true, + target: target, + }) + } else if isDir(child.Permanode) { + // This is a directory. + n.maybeAddChild(name, child.Permanode, &mutDir{ + fs: n.fs, + permanode: blob.ParseOrZero(childRef), + parent: n, + name: name, + }) + } else if contentRef := child.Permanode.Attr.Get("camliContent"); contentRef != "" { + // This is a file. + content := res.Meta[contentRef] + if content == nil { + log.Printf("child content not described: %v", childRef) + continue + } + if content.CamliType != "file" { + log.Printf("child not a file: %v", childRef) + continue + } + if content.File == nil { + log.Printf("camlitype \"file\" child %v has no described File member", childRef) + continue + } + n.maybeAddChild(name, child.Permanode, &mutFile{ + fs: n.fs, + permanode: blob.ParseOrZero(childRef), + parent: n, + name: name, + content: blob.ParseOrZero(contentRef), + size: content.File.Size, + }) + } else { + // unhandled type... + continue + } + currentChildren[name] = true + } + // Remove unreferenced children + for name, oldchild := range n.children { + if _, ok := currentChildren[name]; !ok { + if oldchild.eligibleToDelete() { + delete(n.children, name) + } + } + } + return nil +} + +// maybeAddChild adds a child directory to this mutable directory +// unless it already has one with this name and permanode. +func (m *mutDir) maybeAddChild(name string, permanode *search.DescribedPermanode, + child mutFileOrDir) { + if current, ok := m.children[name]; !ok || + current.permanodeString() != child.permanodeString() { + + child.xattr().load(permanode) + m.children[name] = child + } +} + +func isDir(d *search.DescribedPermanode) bool { + // Explicit + if d.Attr.Get("camliNodeType") == "directory" { + return true + } + // Implied + for k := range d.Attr { + if strings.HasPrefix(k, "camliPath:") { + return true + } + } + return false +} + +func (n *mutDir) ReadDir(intr fs.Intr) ([]fuse.Dirent, fuse.Error) { + if err := n.populate(); err != nil { + log.Println("populate:", err) + return nil, fuse.EIO + } + n.mu.Lock() + defer n.mu.Unlock() + var ents []fuse.Dirent + for name, childNode := range n.children { + var ino uint64 + switch v := childNode.(type) { + case *mutDir: + ino = v.permanode.Sum64() + case *mutFile: + ino = v.permanode.Sum64() + default: + log.Printf("mutDir.ReadDir: unknown child type %T", childNode) + } + + // TODO: figure out what Dirent.Type means. + // fuse.go says "Type uint32 // ?" + dirent := fuse.Dirent{ + Name: name, + Inode: ino, + } + log.Printf("mutDir(%q) appending inode %x, %+v", n.fullPath(), dirent.Inode, dirent) + ents = append(ents, dirent) + } + return ents, nil +} + +func (n *mutDir) Lookup(name string, intr fs.Intr) (ret fs.Node, err fuse.Error) { + defer func() { + log.Printf("mutDir(%q).Lookup(%q) = %v, %v", n.fullPath(), name, ret, err) + }() + if err := n.populate(); err != nil { + log.Println("populate:", err) + return nil, fuse.EIO + } + n.mu.Lock() + defer n.mu.Unlock() + if n2 := n.children[name]; n2 != nil { + return n2, nil + } + return nil, fuse.ENOENT +} + +// Create of regular file. (not a dir) +// +// Flags are always 514: O_CREAT is 0x200 | O_RDWR is 0x2. +// From fuse_vnops.c: +// /* XXX: We /always/ creat() like this. Wish we were on Linux. */ +// foi->flags = O_CREAT | O_RDWR; +// +// 2013/07/21 05:26:35 <- &{Create [ID=0x3 Node=0x8 Uid=61652 Gid=5000 Pid=13115] "x" fl=514 mode=-rw-r--r-- fuse.Intr} +// 2013/07/21 05:26:36 -> 0x3 Create {LookupResponse:{Node:23 Generation:0 EntryValid:1m0s AttrValid:1m0s Attr:{Inode:15976986887557313215 Size:0 Blocks:0 Atime:2013-07-21 05:23:51.537251251 +1200 NZST Mtime:2013-07-21 05:23:51.537251251 +1200 NZST Ctime:2013-07-21 05:23:51.537251251 +1200 NZST Crtime:2013-07-21 05:23:51.537251251 +1200 NZST Mode:-rw------- Nlink:1 Uid:61652 Gid:5000 Rdev:0 Flags:0}} OpenResponse:{Handle:1 Flags:0}} +func (n *mutDir) Create(req *fuse.CreateRequest, res *fuse.CreateResponse, intr fs.Intr) (fs.Node, fs.Handle, fuse.Error) { + child, err := n.creat(req.Name, fileType) + if err != nil { + log.Printf("mutDir.Create(%q): %v", req.Name, err) + return nil, nil, fuse.EIO + } + + // Create and return a file handle. + h, ferr := child.(*mutFile).newHandle(nil) + if ferr != nil { + return nil, nil, ferr + } + + return child, h, nil +} + +func (n *mutDir) Mkdir(req *fuse.MkdirRequest, intr fs.Intr) (fs.Node, fuse.Error) { + child, err := n.creat(req.Name, dirType) + if err != nil { + log.Printf("mutDir.Mkdir(%q): %v", req.Name, err) + return nil, fuse.EIO + } + return child, nil +} + +// &fuse.SymlinkRequest{Header:fuse.Header{Conn:(*fuse.Conn)(0xc210047180), ID:0x4, Node:0x8, Uid:0xf0d4, Gid:0x1388, Pid:0x7e88}, NewName:"some-link", Target:"../../some-target"} +func (n *mutDir) Symlink(req *fuse.SymlinkRequest, intr fs.Intr) (fs.Node, fuse.Error) { + node, err := n.creat(req.NewName, symlinkType) + if err != nil { + log.Printf("mutDir.Symlink(%q): %v", req.NewName, err) + return nil, fuse.EIO + } + mf := node.(*mutFile) + mf.symLink = true + mf.target = req.Target + + claim := schema.NewSetAttributeClaim(mf.permanode, "camliSymlinkTarget", req.Target) + _, err = n.fs.client.UploadAndSignBlob(claim) + if err != nil { + log.Printf("mutDir.Symlink(%q) upload error: %v", req.NewName, err) + return nil, fuse.EIO + } + + return node, nil +} + +func (n *mutDir) creat(name string, typ nodeType) (fs.Node, error) { + // Create a Permanode for the file/directory. + pr, err := n.fs.client.UploadNewPermanode() + if err != nil { + return nil, err + } + + var grp syncutil.Group + grp.Go(func() (err error) { + // Add a camliPath:name attribute to the directory permanode. + claim := schema.NewSetAttributeClaim(n.permanode, "camliPath:"+name, pr.BlobRef.String()) + _, err = n.fs.client.UploadAndSignBlob(claim) + return + }) + + // Hide OS X Finder .DS_Store junk. This is distinct from + // extended attributes. + if name == ".DS_Store" { + grp.Go(func() (err error) { + claim := schema.NewSetAttributeClaim(pr.BlobRef, "camliDefVis", "hide") + _, err = n.fs.client.UploadAndSignBlob(claim) + return + }) + } + + if typ == dirType { + grp.Go(func() (err error) { + // Set a directory type on the permanode + claim := schema.NewSetAttributeClaim(pr.BlobRef, "camliNodeType", "directory") + _, err = n.fs.client.UploadAndSignBlob(claim) + return + }) + grp.Go(func() (err error) { + // Set the permanode title to the directory name + claim := schema.NewSetAttributeClaim(pr.BlobRef, "title", name) + _, err = n.fs.client.UploadAndSignBlob(claim) + return + }) + } + if err := grp.Err(); err != nil { + return nil, err + } + + // Add a child node to this node. + var child mutFileOrDir + switch typ { + case dirType: + child = &mutDir{ + fs: n.fs, + permanode: pr.BlobRef, + parent: n, + name: name, + xattrs: map[string][]byte{}, + localCreateTime: time.Now(), + } + case fileType, symlinkType: + child = &mutFile{ + fs: n.fs, + permanode: pr.BlobRef, + parent: n, + name: name, + xattrs: map[string][]byte{}, + localCreateTime: time.Now(), + } + default: + panic("bogus creat type") + } + n.mu.Lock() + if n.children == nil { + n.children = make(map[string]mutFileOrDir) + } + n.children[name] = child + n.mu.Unlock() + + log.Printf("Created %v in %p", child, n) + + return child, nil +} + +func (n *mutDir) Remove(req *fuse.RemoveRequest, intr fs.Intr) fuse.Error { + // Remove the camliPath:name attribute from the directory permanode. + claim := schema.NewDelAttributeClaim(n.permanode, "camliPath:"+req.Name, "") + _, err := n.fs.client.UploadAndSignBlob(claim) + if err != nil { + log.Println("mutDir.Remove:", err) + return fuse.EIO + } + // Remove child from map. + n.mu.Lock() + if n.children != nil { + if removed, ok := n.children[req.Name]; ok { + removed.invalidate() + delete(n.children, req.Name) + log.Printf("Removed %v from %p", removed, n) + } + } + n.mu.Unlock() + return nil +} + +// &RenameRequest{Header:fuse.Header{Conn:(*fuse.Conn)(0xc210048180), ID:0x2, Node:0x8, Uid:0xf0d4, Gid:0x1388, Pid:0x5edb}, NewDir:0x8, OldName:"1", NewName:"2"} +func (n *mutDir) Rename(req *fuse.RenameRequest, newDir fs.Node, intr fs.Intr) fuse.Error { + n2, ok := newDir.(*mutDir) + if !ok { + log.Printf("*mutDir newDir node isn't a *mutDir; is a %T; can't handle. returning EIO.", newDir) + return fuse.EIO + } + + var wg syncutil.Group + wg.Go(n.populate) + wg.Go(n2.populate) + if err := wg.Err(); err != nil { + log.Printf("*mutDir.Rename src dir populate = %v", err) + return fuse.EIO + } + + n.mu.Lock() + target, ok := n.children[req.OldName] + n.mu.Unlock() + if !ok { + log.Printf("*mutDir.Rename src name %q isn't known", req.OldName) + return fuse.ENOENT + } + + now := time.Now() + + // Add a camliPath:name attribute to the dest permanode before unlinking it from + // the source. + claim := schema.NewSetAttributeClaim(n2.permanode, "camliPath:"+req.NewName, target.permanodeString()) + claim.SetClaimDate(now) + _, err := n.fs.client.UploadAndSignBlob(claim) + if err != nil { + log.Printf("Upload rename link error: %v", err) + return fuse.EIO + } + + var grp syncutil.Group + // Unlink the dest permanode from the source. + grp.Go(func() (err error) { + delClaim := schema.NewDelAttributeClaim(n.permanode, "camliPath:"+req.OldName, "") + delClaim.SetClaimDate(now) + _, err = n.fs.client.UploadAndSignBlob(delClaim) + return + }) + // If target is a directory then update its title. + if dir, ok := target.(*mutDir); ok { + grp.Go(func() (err error) { + claim := schema.NewSetAttributeClaim(dir.permanode, "title", req.NewName) + _, err = n.fs.client.UploadAndSignBlob(claim) + return + }) + } + if err := grp.Err(); err != nil { + log.Printf("Upload rename unlink/title error: %v", err) + return fuse.EIO + } + + // TODO(bradfitz): this locking would be racy, if the kernel + // doesn't do it properly. (It should) Let's just trust the + // kernel for now. Later we can verify and remove this + // comment. + n.mu.Lock() + if n.children[req.OldName] != target { + panic("Race.") + } + delete(n.children, req.OldName) + n.mu.Unlock() + n2.mu.Lock() + n2.children[req.NewName] = target + n2.mu.Unlock() + + return nil +} + +// mutFile is a mutable file, or symlink. +type mutFile struct { + fs *CamliFileSystem + permanode blob.Ref + parent *mutDir + name string // ent name (base name within parent) + + localCreateTime time.Time // time this node was created locally (iff it was) + + mu sync.Mutex // protects all following fields + symLink bool // if true, is a symlink + target string // if a symlink + content blob.Ref // if a regular file + size int64 + mtime, atime time.Time // if zero, use serverStart + xattrs map[string][]byte + deleted bool +} + +func (m *mutFile) String() string { + return fmt.Sprintf("&mutFile{%p name=%q perm:%v}", m, m.fullPath(), m.permanode) +} + +// for debugging +func (n *mutFile) fullPath() string { + if n == nil { + return "" + } + return filepath.Join(n.parent.fullPath(), n.name) +} + +func (n *mutFile) xattr() *xattr { + return &xattr{"mutFile", n.fs, n.permanode, &n.mu, &n.xattrs} +} + +func (n *mutDir) xattr() *xattr { + return &xattr{"mutDir", n.fs, n.permanode, &n.mu, &n.xattrs} +} + +func (n *mutDir) Removexattr(req *fuse.RemovexattrRequest, intr fs.Intr) fuse.Error { + return n.xattr().remove(req) +} + +func (n *mutDir) Setxattr(req *fuse.SetxattrRequest, intr fs.Intr) fuse.Error { + return n.xattr().set(req) +} + +func (n *mutDir) Getxattr(req *fuse.GetxattrRequest, res *fuse.GetxattrResponse, intr fs.Intr) fuse.Error { + return n.xattr().get(req, res) +} + +func (n *mutDir) Listxattr(req *fuse.ListxattrRequest, res *fuse.ListxattrResponse, intr fs.Intr) fuse.Error { + return n.xattr().list(req, res) +} + +func (n *mutFile) Getxattr(req *fuse.GetxattrRequest, res *fuse.GetxattrResponse, intr fs.Intr) fuse.Error { + return n.xattr().get(req, res) +} + +func (n *mutFile) Listxattr(req *fuse.ListxattrRequest, res *fuse.ListxattrResponse, intr fs.Intr) fuse.Error { + return n.xattr().list(req, res) +} + +func (n *mutFile) Removexattr(req *fuse.RemovexattrRequest, intr fs.Intr) fuse.Error { + return n.xattr().remove(req) +} + +func (n *mutFile) Setxattr(req *fuse.SetxattrRequest, intr fs.Intr) fuse.Error { + return n.xattr().set(req) +} + +func (n *mutFile) Attr() fuse.Attr { + // TODO: don't grab n.mu three+ times in here. + var mode os.FileMode = 0600 // writable + + n.mu.Lock() + size := n.size + var blocks uint64 + if size > 0 { + blocks = uint64(size)/512 + 1 + } + inode := n.permanode.Sum64() + if n.symLink { + mode |= os.ModeSymlink + } + n.mu.Unlock() + + return fuse.Attr{ + Inode: inode, + Mode: mode, + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + Size: uint64(size), + Blocks: blocks, + Mtime: n.modTime(), + Atime: n.accessTime(), + Ctime: serverStart, + Crtime: serverStart, + } +} + +func (n *mutFile) accessTime() time.Time { + n.mu.Lock() + if !n.atime.IsZero() { + defer n.mu.Unlock() + return n.atime + } + n.mu.Unlock() + return n.modTime() +} + +func (n *mutFile) modTime() time.Time { + n.mu.Lock() + defer n.mu.Unlock() + if !n.mtime.IsZero() { + return n.mtime + } + return serverStart +} + +func (n *mutFile) setContent(br blob.Ref, size int64) error { + n.mu.Lock() + defer n.mu.Unlock() + n.content = br + n.size = size + claim := schema.NewSetAttributeClaim(n.permanode, "camliContent", br.String()) + _, err := n.fs.client.UploadAndSignBlob(claim) + return err +} + +func (n *mutFile) setSizeAtLeast(size int64) { + n.mu.Lock() + defer n.mu.Unlock() + log.Printf("mutFile.setSizeAtLeast(%d). old size = %d", size, n.size) + if size > n.size { + n.size = size + } +} + +// Empirically: +// open for read: req.Flags == 0 +// open for append: req.Flags == 1 +// open for write: req.Flags == 1 +// open for read/write (+<) == 2 (bitmask? of?) +// +// open flags are O_WRONLY (1), O_RDONLY (0), or O_RDWR (2). and also +// bitmaks of O_SYMLINK (0x200000) maybe. (from +// fuse_filehandle_xlate_to_oflags in macosx/kext/fuse_file.h) +func (n *mutFile) Open(req *fuse.OpenRequest, res *fuse.OpenResponse, intr fs.Intr) (fs.Handle, fuse.Error) { + mutFileOpen.Incr() + + log.Printf("mutFile.Open: %v: content: %v dir=%v flags=%v", n.permanode, n.content, req.Dir, req.Flags) + r, err := schema.NewFileReader(n.fs.fetcher, n.content) + if err != nil { + mutFileOpenError.Incr() + log.Printf("mutFile.Open: %v", err) + return nil, fuse.EIO + } + + // Read-only. + if !isWriteFlags(req.Flags) { + mutFileOpenRO.Incr() + log.Printf("mutFile.Open returning read-only file") + n := &node{ + fs: n.fs, + blobref: n.content, + } + return &nodeReader{n: n, fr: r}, nil + } + + mutFileOpenRW.Incr() + log.Printf("mutFile.Open returning read-write filehandle") + + defer r.Close() + return n.newHandle(r) +} + +func (n *mutFile) Fsync(r *fuse.FsyncRequest, intr fs.Intr) fuse.Error { + // TODO(adg): in the fuse package, plumb through fsync to mutFileHandle + // in the same way we did Truncate. + log.Printf("mutFile.Fsync: TODO") + return nil +} + +func (n *mutFile) Readlink(req *fuse.ReadlinkRequest, intr fs.Intr) (string, fuse.Error) { + n.mu.Lock() + defer n.mu.Unlock() + if !n.symLink { + log.Printf("mutFile.Readlink on node that's not a symlink?") + return "", fuse.EIO + } + return n.target, nil +} + +func (n *mutFile) Setattr(req *fuse.SetattrRequest, res *fuse.SetattrResponse, intr fs.Intr) fuse.Error { + log.Printf("mutFile.Setattr on %q: %#v", n.fullPath(), req) + // 2013/07/17 19:43:41 mutFile.Setattr on "foo": &fuse.SetattrRequest{Header:fuse.Header{Conn:(*fuse.Conn)(0xc210047180), ID:0x3, Node:0x3d, Uid:0xf0d4, Gid:0x1388, Pid:0x75e8}, Valid:0x30, Handle:0x0, Size:0x0, Atime:time.Time{sec:63509651021, nsec:0x4aec6b8, loc:(*time.Location)(0x47f7600)}, Mtime:time.Time{sec:63509651021, nsec:0x4aec6b8, loc:(*time.Location)(0x47f7600)}, Mode:0x4000000, Uid:0x0, Gid:0x0, Bkuptime:time.Time{sec:62135596800, nsec:0x0, loc:(*time.Location)(0x47f7600)}, Chgtime:time.Time{sec:62135596800, nsec:0x0, loc:(*time.Location)(0x47f7600)}, Crtime:time.Time{sec:0, nsec:0x0, loc:(*time.Location)(nil)}, Flags:0x0} + + n.mu.Lock() + if req.Valid&fuse.SetattrMtime != 0 { + n.mtime = req.Mtime + } + if req.Valid&fuse.SetattrAtime != 0 { + n.atime = req.Atime + } + if req.Valid&fuse.SetattrSize != 0 { + // TODO(bradfitz): truncate? + n.size = int64(req.Size) + } + n.mu.Unlock() + + res.AttrValid = 1 * time.Minute + res.Attr = n.Attr() + return nil +} + +func (n *mutFile) newHandle(body io.Reader) (fs.Handle, fuse.Error) { + tmp, err := ioutil.TempFile("", "camli-") + if err == nil && body != nil { + _, err = io.Copy(tmp, body) + } + if err != nil { + log.Printf("mutFile.newHandle: %v", err) + if tmp != nil { + tmp.Close() + os.Remove(tmp.Name()) + } + return nil, fuse.EIO + } + return &mutFileHandle{f: n, tmp: tmp}, nil +} + +// mutFileHandle represents an open mutable file. +// It stores the file contents in a temporary file, and +// delegates reads and writes directly to the temporary file. +// When the handle is released, it writes the contents of the +// temporary file to the blobstore, and instructs the parent +// mutFile to update the file permanode. +type mutFileHandle struct { + f *mutFile + tmp *os.File +} + +func (h *mutFileHandle) Read(req *fuse.ReadRequest, res *fuse.ReadResponse, intr fs.Intr) fuse.Error { + if h.tmp == nil { + log.Printf("Read called on camli mutFileHandle without a tempfile set") + return fuse.EIO + } + + buf := make([]byte, req.Size) + n, err := h.tmp.ReadAt(buf, req.Offset) + if err == io.EOF { + err = nil + } + if err != nil { + log.Printf("mutFileHandle.Read: %v", err) + return fuse.EIO + } + res.Data = buf[:n] + return nil +} + +func (h *mutFileHandle) Write(req *fuse.WriteRequest, res *fuse.WriteResponse, intr fs.Intr) fuse.Error { + if h.tmp == nil { + log.Printf("Write called on camli mutFileHandle without a tempfile set") + return fuse.EIO + } + + n, err := h.tmp.WriteAt(req.Data, req.Offset) + log.Printf("mutFileHandle.Write(%q, %d bytes at %d, flags %v) = %d, %v", + h.f.fullPath(), len(req.Data), req.Offset, req.Flags, n, err) + if err != nil { + log.Println("mutFileHandle.Write:", err) + return fuse.EIO + } + res.Size = n + h.f.setSizeAtLeast(req.Offset + int64(n)) + return nil +} + +// Flush is called to let the file system clean up any data buffers +// and to pass any errors in the process of closing a file to the user +// application. +// +// Flush *may* be called more than once in the case where a file is +// opened more than once, but it's not possible to detect from the +// call itself whether this is a final flush. +// +// This is generally the last opportunity to finalize data and the +// return value sets the return value of the Close that led to the +// calling of Flush. +// +// Note that this is distinct from Fsync -- which is a user-requested +// flush (fsync, etc...) +func (h *mutFileHandle) Flush(*fuse.FlushRequest, fs.Intr) fuse.Error { + if h.tmp == nil { + log.Printf("Flush called on camli mutFileHandle without a tempfile set") + return fuse.EIO + } + _, err := h.tmp.Seek(0, 0) + if err != nil { + log.Println("mutFileHandle.Flush:", err) + return fuse.EIO + } + var n int64 + br, err := schema.WriteFileFromReader(h.f.fs.client, h.f.name, readerutil.CountingReader{Reader: h.tmp, N: &n}) + if err != nil { + log.Println("mutFileHandle.Flush:", err) + return fuse.EIO + } + err = h.f.setContent(br, n) + if err != nil { + log.Printf("mutFileHandle.Flush: %v", err) + return fuse.EIO + } + + return nil +} + +// Release is called when a file handle is no longer needed. This is +// called asynchronously after the last handle to a file is closed. +func (h *mutFileHandle) Release(req *fuse.ReleaseRequest, intr fs.Intr) fuse.Error { + h.tmp.Close() + os.Remove(h.tmp.Name()) + h.tmp = nil + + return nil +} + +func (h *mutFileHandle) Truncate(size uint64, intr fs.Intr) fuse.Error { + if h.tmp == nil { + log.Printf("Truncate called on camli mutFileHandle without a tempfile set") + return fuse.EIO + } + + log.Printf("mutFileHandle.Truncate(%q) to size %d", h.f.fullPath(), size) + if err := h.tmp.Truncate(int64(size)); err != nil { + log.Println("mutFileHandle.Truncate:", err) + return fuse.EIO + } + return nil +} + +// mutFileOrDir is a *mutFile or *mutDir +type mutFileOrDir interface { + fs.Node + invalidate() + permanodeString() string + xattr() *xattr + eligibleToDelete() bool +} + +func (n *mutFile) permanodeString() string { + return n.permanode.String() +} + +func (n *mutDir) permanodeString() string { + return n.permanode.String() +} + +func (n *mutFile) invalidate() { + n.mu.Lock() + n.deleted = true + n.mu.Unlock() +} + +func (n *mutDir) invalidate() { + n.mu.Lock() + n.deleted = true + n.mu.Unlock() +} + +func (n *mutFile) eligibleToDelete() bool { + return n.localCreateTime.Before(time.Now().Add(-deletionRefreshWindow)) +} + +func (n *mutDir) eligibleToDelete() bool { + return n.localCreateTime.Before(time.Now().Add(-deletionRefreshWindow)) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/mut_test.go b/vendor/github.com/camlistore/camlistore/pkg/fs/mut_test.go new file mode 100644 index 00000000..b20b5370 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/mut_test.go @@ -0,0 +1,49 @@ +// +build linux darwin + +/* +Copyright 2014 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "testing" + "time" +) + +func TestDeleteEligibility(t *testing.T) { + tests := []struct { + name string + ts time.Time + exp bool + }{ + {"zero", time.Time{}, true}, + {"now", time.Now(), false}, + {"future", time.Now().Add(time.Hour), false}, + {"recent", time.Now().Add(-(deletionRefreshWindow / 2)), false}, + {"past", time.Now().Add(-(deletionRefreshWindow * 2)), true}, + } + + for _, test := range tests { + d := &mutDir{localCreateTime: test.ts} + if d.eligibleToDelete() != test.exp { + t.Errorf("Expected %v %T/%v", test.exp, d, test.name) + } + f := &mutFile{localCreateTime: test.ts} + if f.eligibleToDelete() != test.exp { + t.Errorf("Expected %v for %T/%v", test.exp, f, test.name) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/recent.go b/vendor/github.com/camlistore/camlistore/pkg/fs/recent.go new file mode 100644 index 00000000..ba9149d7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/recent.go @@ -0,0 +1,156 @@ +// +build linux darwin + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "log" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/search" + + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs" +) + +// recentDir implements fuse.Node and is a directory of recent +// permanodes' files, for permanodes with a camliContent pointing to a +// "file". +type recentDir struct { + noXattr + fs *CamliFileSystem + + mu sync.Mutex + ents map[string]*search.DescribedBlob // filename to blob meta + modTime map[string]time.Time // filename to permanode modtime + lastReaddir time.Time + lastNames []string +} + +func (n *recentDir) Attr() fuse.Attr { + return fuse.Attr{ + Mode: os.ModeDir | 0500, + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + } +} + +const recentSearchInterval = 10 * time.Second + +func (n *recentDir) ReadDir(intr fs.Intr) ([]fuse.Dirent, fuse.Error) { + var ents []fuse.Dirent + + n.mu.Lock() + defer n.mu.Unlock() + if n.lastReaddir.After(time.Now().Add(-recentSearchInterval)) { + log.Printf("fs.recent: ReadDir from cache") + for _, name := range n.lastNames { + ents = append(ents, fuse.Dirent{Name: name}) + } + return ents, nil + } + + log.Printf("fs.recent: ReadDir, doing search") + + n.ents = make(map[string]*search.DescribedBlob) + n.modTime = make(map[string]time.Time) + + req := &search.RecentRequest{N: 100} + res, err := n.fs.client.GetRecentPermanodes(req) + if err != nil { + log.Printf("fs.recent: GetRecentPermanodes error in ReadDir: %v", err) + return nil, fuse.EIO + } + + n.lastNames = nil + for _, ri := range res.Recent { + modTime := ri.ModTime.Time() + meta := res.Meta.Get(ri.BlobRef) + if meta == nil || meta.Permanode == nil { + continue + } + cc, ok := blob.Parse(meta.Permanode.Attr.Get("camliContent")) + if !ok { + continue + } + ccMeta := res.Meta.Get(cc) + if ccMeta == nil { + continue + } + var name string + switch { + case ccMeta.File != nil: + name = ccMeta.File.FileName + if mt := ccMeta.File.Time; !mt.IsZero() { + modTime = mt.Time() + } + case ccMeta.Dir != nil: + name = ccMeta.Dir.FileName + default: + continue + } + if name == "" || n.ents[name] != nil { + ext := filepath.Ext(name) + if ext == "" && ccMeta.File != nil && strings.HasSuffix(ccMeta.File.MIMEType, "image/jpeg") { + ext = ".jpg" + } + name = strings.TrimPrefix(ccMeta.BlobRef.String(), "sha1-")[:10] + ext + if n.ents[name] != nil { + continue + } + } + n.ents[name] = ccMeta + n.modTime[name] = modTime + log.Printf("fs.recent: name %q = %v (at %v -> %v)", name, ccMeta.BlobRef, ri.ModTime.Time(), modTime) + n.lastNames = append(n.lastNames, name) + ents = append(ents, fuse.Dirent{ + Name: name, + }) + } + log.Printf("fs.recent returning %d entries", len(ents)) + n.lastReaddir = time.Now() + return ents, nil +} + +func (n *recentDir) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) { + n.mu.Lock() + defer n.mu.Unlock() + if n.ents == nil { + // Odd case: a Lookup before a Readdir. Force a readdir to + // seed our map. Mostly hit just during development. + n.mu.Unlock() // release, since ReadDir will acquire + n.ReadDir(intr) + n.mu.Lock() + } + db := n.ents[name] + log.Printf("fs.recent: Lookup(%q) = %v", name, db) + if db == nil { + return nil, fuse.ENOENT + } + nod := &node{ + fs: n.fs, + blobref: db.BlobRef, + pnodeModTime: n.modTime[name], + } + return nod, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/ro.go b/vendor/github.com/camlistore/camlistore/pkg/fs/ro.go new file mode 100644 index 00000000..4fcdb28e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/ro.go @@ -0,0 +1,383 @@ +// +build linux darwin + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "errors" + "log" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/search" + "camlistore.org/pkg/types" + + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs" +) + +// roDir is a read-only directory. +// Its permanode is the permanode with camliPath:entname attributes. +type roDir struct { + fs *CamliFileSystem + permanode blob.Ref + parent *roDir // or nil, if the root within its roots.go root. + name string // ent name (base name within parent) + at time.Time + + mu sync.Mutex + children map[string]roFileOrDir + xattrs map[string][]byte +} + +func newRODir(fs *CamliFileSystem, permanode blob.Ref, name string, at time.Time) *roDir { + return &roDir{ + fs: fs, + permanode: permanode, + name: name, + at: at, + } +} + +// for debugging +func (n *roDir) fullPath() string { + if n == nil { + return "" + } + return filepath.Join(n.parent.fullPath(), n.name) +} + +func (n *roDir) Attr() fuse.Attr { + return fuse.Attr{ + Inode: n.permanode.Sum64(), + Mode: os.ModeDir | 0500, + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + } +} + +// populate hits the blobstore to populate map of child nodes. +func (n *roDir) populate() error { + n.mu.Lock() + defer n.mu.Unlock() + + // Things never change here, so if we've ever populated, we're + // populated. + if n.children != nil { + return nil + } + + log.Printf("roDir.populate(%q) - Sending request At %v", n.fullPath(), n.at) + + res, err := n.fs.client.Describe(&search.DescribeRequest{ + BlobRef: n.permanode, + Depth: 3, + At: types.Time3339(n.at), + }) + if err != nil { + log.Println("roDir.paths:", err) + return nil + } + db := res.Meta[n.permanode.String()] + if db == nil { + return errors.New("dir blobref not described") + } + + // Find all child permanodes and stick them in n.children + n.children = make(map[string]roFileOrDir) + for k, v := range db.Permanode.Attr { + const p = "camliPath:" + if !strings.HasPrefix(k, p) || len(v) < 1 { + continue + } + name := k[len(p):] + childRef := v[0] + child := res.Meta[childRef] + if child == nil { + log.Printf("child not described: %v", childRef) + continue + } + if target := child.Permanode.Attr.Get("camliSymlinkTarget"); target != "" { + // This is a symlink. + n.children[name] = &roFile{ + fs: n.fs, + permanode: blob.ParseOrZero(childRef), + parent: n, + name: name, + symLink: true, + target: target, + } + } else if isDir(child.Permanode) { + // This is a directory. + n.children[name] = &roDir{ + fs: n.fs, + permanode: blob.ParseOrZero(childRef), + parent: n, + name: name, + } + } else if contentRef := child.Permanode.Attr.Get("camliContent"); contentRef != "" { + // This is a file. + content := res.Meta[contentRef] + if content == nil { + log.Printf("child content not described: %v", childRef) + continue + } + if content.CamliType != "file" { + log.Printf("child not a file: %v", childRef) + continue + } + n.children[name] = &roFile{ + fs: n.fs, + permanode: blob.ParseOrZero(childRef), + parent: n, + name: name, + content: blob.ParseOrZero(contentRef), + size: content.File.Size, + } + } else { + // unknown type + continue + } + n.children[name].xattr().load(child.Permanode) + } + return nil +} + +func (n *roDir) ReadDir(intr fs.Intr) ([]fuse.Dirent, fuse.Error) { + if err := n.populate(); err != nil { + log.Println("populate:", err) + return nil, fuse.EIO + } + n.mu.Lock() + defer n.mu.Unlock() + var ents []fuse.Dirent + for name, childNode := range n.children { + var ino uint64 + switch v := childNode.(type) { + case *roDir: + ino = v.permanode.Sum64() + case *roFile: + ino = v.permanode.Sum64() + default: + log.Printf("roDir.ReadDir: unknown child type %T", childNode) + } + + // TODO: figure out what Dirent.Type means. + // fuse.go says "Type uint32 // ?" + dirent := fuse.Dirent{ + Name: name, + Inode: ino, + } + log.Printf("roDir(%q) appending inode %x, %+v", n.fullPath(), dirent.Inode, dirent) + ents = append(ents, dirent) + } + return ents, nil +} + +func (n *roDir) Lookup(name string, intr fs.Intr) (ret fs.Node, err fuse.Error) { + defer func() { + log.Printf("roDir(%q).Lookup(%q) = %#v, %v", n.fullPath(), name, ret, err) + }() + if err := n.populate(); err != nil { + log.Println("populate:", err) + return nil, fuse.EIO + } + n.mu.Lock() + defer n.mu.Unlock() + if n2 := n.children[name]; n2 != nil { + return n2, nil + } + return nil, fuse.ENOENT +} + +// roFile is a read-only file, or symlink. +type roFile struct { + fs *CamliFileSystem + permanode blob.Ref + parent *roDir + name string // ent name (base name within parent) + + mu sync.Mutex // protects all following fields + symLink bool // if true, is a symlink + target string // if a symlink + content blob.Ref // if a regular file + size int64 + mtime, atime time.Time // if zero, use serverStart + xattrs map[string][]byte +} + +func (n *roDir) Getxattr(req *fuse.GetxattrRequest, res *fuse.GetxattrResponse, intr fs.Intr) fuse.Error { + return n.xattr().get(req, res) +} + +func (n *roDir) Listxattr(req *fuse.ListxattrRequest, res *fuse.ListxattrResponse, intr fs.Intr) fuse.Error { + return n.xattr().list(req, res) +} + +func (n *roFile) Getxattr(req *fuse.GetxattrRequest, res *fuse.GetxattrResponse, intr fs.Intr) fuse.Error { + return n.xattr().get(req, res) +} + +func (n *roFile) Listxattr(req *fuse.ListxattrRequest, res *fuse.ListxattrResponse, intr fs.Intr) fuse.Error { + return n.xattr().list(req, res) +} + +func (n *roFile) Removexattr(req *fuse.RemovexattrRequest, intr fs.Intr) fuse.Error { + return fuse.EPERM +} + +func (n *roFile) Setxattr(req *fuse.SetxattrRequest, intr fs.Intr) fuse.Error { + return fuse.EPERM +} + +// for debugging +func (n *roFile) fullPath() string { + if n == nil { + return "" + } + return filepath.Join(n.parent.fullPath(), n.name) +} + +func (n *roFile) Attr() fuse.Attr { + // TODO: don't grab n.mu three+ times in here. + var mode os.FileMode = 0400 // read-only + + n.mu.Lock() + size := n.size + var blocks uint64 + if size > 0 { + blocks = uint64(size)/512 + 1 + } + inode := n.permanode.Sum64() + if n.symLink { + mode |= os.ModeSymlink + } + n.mu.Unlock() + + return fuse.Attr{ + Inode: inode, + Mode: mode, + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + Size: uint64(size), + Blocks: blocks, + Mtime: n.modTime(), + Atime: n.accessTime(), + Ctime: serverStart, + Crtime: serverStart, + } +} + +func (n *roFile) accessTime() time.Time { + n.mu.Lock() + if !n.atime.IsZero() { + defer n.mu.Unlock() + return n.atime + } + n.mu.Unlock() + return n.modTime() +} + +func (n *roFile) modTime() time.Time { + n.mu.Lock() + defer n.mu.Unlock() + if !n.mtime.IsZero() { + return n.mtime + } + return serverStart +} + +// Empirically: +// open for read: req.Flags == 0 +// open for append: req.Flags == 1 +// open for write: req.Flags == 1 +// open for read/write (+<) == 2 (bitmask? of?) +// +// open flags are O_WRONLY (1), O_RDONLY (0), or O_RDWR (2). and also +// bitmaks of O_SYMLINK (0x200000) maybe. (from +// fuse_filehandle_xlate_to_oflags in macosx/kext/fuse_file.h) +func (n *roFile) Open(req *fuse.OpenRequest, res *fuse.OpenResponse, intr fs.Intr) (fs.Handle, fuse.Error) { + roFileOpen.Incr() + + if isWriteFlags(req.Flags) { + return nil, fuse.EPERM + } + + log.Printf("roFile.Open: %v: content: %v dir=%v flags=%v", n.permanode, n.content, req.Dir, req.Flags) + r, err := schema.NewFileReader(n.fs.fetcher, n.content) + if err != nil { + roFileOpenError.Incr() + log.Printf("roFile.Open: %v", err) + return nil, fuse.EIO + } + + // Turn off the OpenDirectIO bit (on by default in rsc fuse server.go), + // else append operations don't work for some reason. + res.Flags &= ^fuse.OpenDirectIO + + // Read-only. + nod := &node{ + fs: n.fs, + blobref: n.content, + } + return &nodeReader{n: nod, fr: r}, nil +} + +func (n *roFile) Fsync(r *fuse.FsyncRequest, intr fs.Intr) fuse.Error { + // noop + return nil +} + +func (n *roFile) Readlink(req *fuse.ReadlinkRequest, intr fs.Intr) (string, fuse.Error) { + log.Printf("roFile.Readlink(%q)", n.fullPath()) + n.mu.Lock() + defer n.mu.Unlock() + if !n.symLink { + log.Printf("roFile.Readlink on node that's not a symlink?") + return "", fuse.EIO + } + return n.target, nil +} + +// roFileOrDir is a *roFile or *roDir +type roFileOrDir interface { + fs.Node + permanodeString() string + xattr() *xattr +} + +func (n *roFile) permanodeString() string { + return n.permanode.String() +} + +func (n *roDir) permanodeString() string { + return n.permanode.String() +} + +func (n *roFile) xattr() *xattr { + return &xattr{"roFile", n.fs, n.permanode, &n.mu, &n.xattrs} +} + +func (n *roDir) xattr() *xattr { + return &xattr{"roDir", n.fs, n.permanode, &n.mu, &n.xattrs} +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/root.go b/vendor/github.com/camlistore/camlistore/pkg/fs/root.go new file mode 100644 index 00000000..42b891fb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/root.go @@ -0,0 +1,121 @@ +// +build linux darwin + +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "log" + "os" + "sync" + + "camlistore.org/pkg/blob" + + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs" +) + +// root implements fuse.Node and is the typical root of a +// CamliFilesystem with a little hello message and the ability to +// search and browse static snapshots, etc. +type root struct { + noXattr + fs *CamliFileSystem + + mu sync.Mutex // guards recent + recent *recentDir + roots *rootsDir + atDir *atDir +} + +func (n *root) Attr() fuse.Attr { + return fuse.Attr{ + Mode: os.ModeDir | 0700, + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + } +} + +func (n *root) ReadDir(intr fs.Intr) ([]fuse.Dirent, fuse.Error) { + return []fuse.Dirent{ + {Name: "WELCOME.txt"}, + {Name: "tag"}, + {Name: "date"}, + {Name: "recent"}, + {Name: "roots"}, + {Name: "at"}, + {Name: "sha1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}, + }, nil +} + +func (n *root) getRecentDir() *recentDir { + n.mu.Lock() + defer n.mu.Unlock() + if n.recent == nil { + n.recent = &recentDir{fs: n.fs} + } + return n.recent +} + +func (n *root) getRootsDir() *rootsDir { + n.mu.Lock() + defer n.mu.Unlock() + if n.roots == nil { + n.roots = &rootsDir{fs: n.fs} + } + return n.roots +} + +func (n *root) getAtDir() *atDir { + n.mu.Lock() + defer n.mu.Unlock() + if n.atDir == nil { + n.atDir = &atDir{fs: n.fs} + } + return n.atDir +} + +func (n *root) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) { + switch name { + case ".quitquitquit": + log.Fatalf("Shutting down due to root .quitquitquit lookup.") + case "WELCOME.txt": + return staticFileNode("Welcome to CamlistoreFS.\n\nFor now you can only cd into a sha1-xxxx directory, if you know the blobref of a directory or a file.\n"), nil + case "recent": + return n.getRecentDir(), nil + case "tag", "date": + return notImplementDirNode{}, nil + case "at": + return n.getAtDir(), nil + case "roots": + return n.getRootsDir(), nil + case "sha1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": + return notImplementDirNode{}, nil + case ".camli_fs_stats": + return statsDir{}, nil + case "mach_kernel", ".hidden", "._.": + // Just quiet some log noise on OS X. + return nil, fuse.ENOENT + } + + if br, ok := blob.Parse(name); ok { + log.Printf("Root lookup of blobref. %q => %v", name, br) + return &node{fs: n.fs, blobref: br}, nil + } + log.Printf("Bogus root lookup of %q", name) + return nil, fuse.ENOENT +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/roots.go b/vendor/github.com/camlistore/camlistore/pkg/fs/roots.go new file mode 100644 index 00000000..e5c428c7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/roots.go @@ -0,0 +1,337 @@ +// +build linux darwin + +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "log" + "os" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/search" + "camlistore.org/pkg/syncutil" + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs" +) + +const refreshTime = 1 * time.Minute + +type rootsDir struct { + noXattr + fs *CamliFileSystem + at time.Time + + mu sync.Mutex // guards following + lastQuery time.Time + m map[string]blob.Ref // ent name => permanode + children map[string]fs.Node // ent name => child node +} + +func (n *rootsDir) isRO() bool { + return !n.at.IsZero() +} + +func (n *rootsDir) dirMode() os.FileMode { + if n.isRO() { + return 0500 + } + return 0700 +} + +func (n *rootsDir) Attr() fuse.Attr { + return fuse.Attr{ + Mode: os.ModeDir | n.dirMode(), + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + } +} + +func (n *rootsDir) ReadDir(intr fs.Intr) ([]fuse.Dirent, fuse.Error) { + n.mu.Lock() + defer n.mu.Unlock() + if err := n.condRefresh(); err != nil { + return nil, fuse.EIO + } + var ents []fuse.Dirent + for name := range n.m { + ents = append(ents, fuse.Dirent{Name: name}) + } + log.Printf("rootsDir.ReadDir() -> %v", ents) + return ents, nil +} + +func (n *rootsDir) Remove(req *fuse.RemoveRequest, intr fs.Intr) fuse.Error { + if n.isRO() { + return fuse.EPERM + } + n.mu.Lock() + defer n.mu.Unlock() + + if err := n.condRefresh(); err != nil { + return err + } + br := n.m[req.Name] + if !br.Valid() { + return fuse.ENOENT + } + + claim := schema.NewDelAttributeClaim(br, "camliRoot", "") + _, err := n.fs.client.UploadAndSignBlob(claim) + if err != nil { + log.Println("rootsDir.Remove:", err) + return fuse.EIO + } + + delete(n.m, req.Name) + delete(n.children, req.Name) + + return nil +} + +func (n *rootsDir) Rename(req *fuse.RenameRequest, newDir fs.Node, intr fs.Intr) fuse.Error { + log.Printf("rootsDir.Rename %q -> %q", req.OldName, req.NewName) + if n.isRO() { + return fuse.EPERM + } + + n.mu.Lock() + target, exists := n.m[req.OldName] + _, collision := n.m[req.NewName] + n.mu.Unlock() + if !exists { + log.Printf("*rootsDir.Rename src name %q isn't known", req.OldName) + return fuse.ENOENT + } + if collision { + log.Printf("*rootsDir.Rename dest %q already exists", req.NewName) + return fuse.EIO + } + + // Don't allow renames if the root contains content. Rename + // is mostly implemented to make GUIs that create directories + // before asking for the directory name. + res, err := n.fs.client.Describe(&search.DescribeRequest{BlobRef: target}) + if err != nil { + log.Println("rootsDir.Rename:", err) + return fuse.EIO + } + db := res.Meta[target.String()] + if db == nil { + log.Printf("Failed to pull meta for target: %v", target) + return fuse.EIO + } + + for k := range db.Permanode.Attr { + const p = "camliPath:" + if strings.HasPrefix(k, p) { + log.Printf("Found file in %q: %q, disallowing rename", req.OldName, k[len(p):]) + return fuse.EIO + } + } + + claim := schema.NewSetAttributeClaim(target, "camliRoot", req.NewName) + _, err = n.fs.client.UploadAndSignBlob(claim) + if err != nil { + log.Printf("Upload rename link error: %v", err) + return fuse.EIO + } + + // Comment transplanted from mutDir.Rename + // TODO(bradfitz): this locking would be racy, if the kernel + // doesn't do it properly. (It should) Let's just trust the + // kernel for now. Later we can verify and remove this + // comment. + n.mu.Lock() + if n.m[req.OldName] != target { + panic("Race.") + } + delete(n.m, req.OldName) + delete(n.children, req.OldName) + delete(n.children, req.NewName) + n.m[req.NewName] = target + n.mu.Unlock() + + return nil +} + +func (n *rootsDir) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) { + log.Printf("fs.roots: Lookup(%q)", name) + n.mu.Lock() + defer n.mu.Unlock() + if err := n.condRefresh(); err != nil { + return nil, err + } + br := n.m[name] + if !br.Valid() { + return nil, fuse.ENOENT + } + + nod, ok := n.children[name] + if ok { + return nod, nil + } + + if n.isRO() { + nod = newRODir(n.fs, br, name, n.at) + } else { + nod = &mutDir{ + fs: n.fs, + permanode: br, + name: name, + xattrs: map[string][]byte{}, + } + } + n.children[name] = nod + + return nod, nil +} + +// requires n.mu is held +func (n *rootsDir) condRefresh() fuse.Error { + if n.lastQuery.After(time.Now().Add(-refreshTime)) { + return nil + } + log.Printf("fs.roots: querying") + + var rootRes, impRes *search.WithAttrResponse + var grp syncutil.Group + grp.Go(func() (err error) { + rootRes, err = n.fs.client.GetPermanodesWithAttr(&search.WithAttrRequest{N: 100, Attr: "camliRoot"}) + return + }) + grp.Go(func() (err error) { + impRes, err = n.fs.client.GetPermanodesWithAttr(&search.WithAttrRequest{N: 100, Attr: "camliImportRoot"}) + return + }) + if err := grp.Err(); err != nil { + log.Printf("fs.recent: GetRecentPermanodes error in ReadDir: %v", err) + return fuse.EIO + } + + n.m = make(map[string]blob.Ref) + if n.children == nil { + n.children = make(map[string]fs.Node) + } + + dr := &search.DescribeRequest{ + Depth: 1, + } + for _, wi := range rootRes.WithAttr { + dr.BlobRefs = append(dr.BlobRefs, wi.Permanode) + } + for _, wi := range impRes.WithAttr { + dr.BlobRefs = append(dr.BlobRefs, wi.Permanode) + } + if len(dr.BlobRefs) == 0 { + return nil + } + + dres, err := n.fs.client.Describe(dr) + if err != nil { + log.Printf("Describe failure: %v", err) + return fuse.EIO + } + + // Roots + currentRoots := map[string]bool{} + for _, wi := range rootRes.WithAttr { + pn := wi.Permanode + db := dres.Meta[pn.String()] + if db != nil && db.Permanode != nil { + name := db.Permanode.Attr.Get("camliRoot") + if name != "" { + currentRoots[name] = true + n.m[name] = pn + } + } + } + + // Remove any children objects we have mapped that are no + // longer relevant. + for name := range n.children { + if !currentRoots[name] { + delete(n.children, name) + } + } + + // Importers (mapped as roots for now) + for _, wi := range impRes.WithAttr { + pn := wi.Permanode + db := dres.Meta[pn.String()] + if db != nil && db.Permanode != nil { + name := db.Permanode.Attr.Get("camliImportRoot") + if name != "" { + name = strings.Replace(name, ":", "-", -1) + name = strings.Replace(name, "/", "-", -1) + n.m["importer-"+name] = pn + } + } + } + + n.lastQuery = time.Now() + return nil +} + +func (n *rootsDir) Mkdir(req *fuse.MkdirRequest, intr fs.Intr) (fs.Node, fuse.Error) { + if n.isRO() { + return nil, fuse.EPERM + } + + name := req.Name + + // Create a Permanode for the root. + pr, err := n.fs.client.UploadNewPermanode() + if err != nil { + log.Printf("rootsDir.Create(%q): %v", name, err) + return nil, fuse.EIO + } + + var grp syncutil.Group + // Add a camliRoot attribute to the root permanode. + grp.Go(func() (err error) { + claim := schema.NewSetAttributeClaim(pr.BlobRef, "camliRoot", name) + _, err = n.fs.client.UploadAndSignBlob(claim) + return + }) + // Set the title of the root permanode to the root name. + grp.Go(func() (err error) { + claim := schema.NewSetAttributeClaim(pr.BlobRef, "title", name) + _, err = n.fs.client.UploadAndSignBlob(claim) + return + }) + if err := grp.Err(); err != nil { + log.Printf("rootsDir.Create(%q): %v", name, err) + return nil, fuse.EIO + } + + nod := &mutDir{ + fs: n.fs, + permanode: pr.BlobRef, + name: name, + xattrs: map[string][]byte{}, + } + n.mu.Lock() + n.m[name] = pr.BlobRef + n.mu.Unlock() + + return nod, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/time.go b/vendor/github.com/camlistore/camlistore/pkg/fs/time.go new file mode 100644 index 00000000..94512413 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/time.go @@ -0,0 +1,151 @@ +// +build linux darwin + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "errors" + "fmt" + "math" + "strconv" + "time" +) + +var timeFormats = []string{ + time.RFC3339Nano, + time.RFC3339, + time.RFC1123Z, + time.RFC1123, + time.UnixDate, + time.ANSIC, + time.RubyDate, + "2006-01-02T15:04", + "2006-01-02T15", + "2006-01-02", + "2006-01", + "2006", +} + +var errUnparseableTimestamp = errors.New("unparsable timestamp") + +var powTable = []int{ + 10e8, + 10e7, + 10e6, + 10e5, + 10e4, + 10e3, + 10e2, + 10e1, + 10, + 1, +} + +// Hand crafted this parser since it's a really common path. +func parseCanonicalTime(in string) (time.Time, error) { + if len(in) < 20 || in[len(in)-1] != 'Z' { + return time.Time{}, errUnparseableTimestamp + } + + if !(in[4] == '-' && in[7] == '-' && in[10] == 'T' && + in[13] == ':' && in[16] == ':' && (in[19] == '.' || in[19] == 'Z')) { + return time.Time{}, fmt.Errorf("positionally incorrect: %v", in) + } + + // 2012-08-28T21:24:35.37465188Z + // 4 7 10 13 16 19 + // ----------------------------- + // 0-4 5 8 11 14 17 20 + + year, err := strconv.Atoi(in[0:4]) + if err != nil { + return time.Time{}, fmt.Errorf("error parsing year: %v", err) + } + + month, err := strconv.Atoi(in[5:7]) + if err != nil { + return time.Time{}, fmt.Errorf("error parsing month: %v", err) + } + + day, err := strconv.Atoi(in[8:10]) + if err != nil { + return time.Time{}, fmt.Errorf("error parsing day: %v", err) + } + + hour, err := strconv.Atoi(in[11:13]) + if err != nil { + return time.Time{}, fmt.Errorf("error parsing hour: %v", err) + } + + minute, err := strconv.Atoi(in[14:16]) + if err != nil { + return time.Time{}, fmt.Errorf("error parsing minute: %v", err) + } + + second, err := strconv.Atoi(in[17:19]) + if err != nil { + return time.Time{}, fmt.Errorf("error parsing second: %v", err) + } + + var nsecstr string + if in[19] != 'Z' { + nsecstr = in[20 : len(in)-1] + } + var nsec int + + if nsecstr != "" { + nsec, err = strconv.Atoi(nsecstr) + if err != nil { + return time.Time{}, fmt.Errorf("error parsing nanoseconds: %v", err) + } + } + + nsec *= powTable[len(nsecstr)] + + return time.Date(year, time.Month(month), day, + hour, minute, second, nsec, time.UTC), nil +} + +func parseTime(in string) (time.Time, error) { + // First, try a few numerics + n, err := strconv.ParseInt(in, 10, 64) + if err == nil { + switch { + case n > int64(math.MaxInt32)*1000: + // nanosecond timestamps + return time.Unix(n/1e9, n%1e9), nil + case n > int64(math.MaxInt32): + // millisecond timestamps + return time.Unix(n/1000, (n%1000)*1e6), nil + case n > 10000: + // second timestamps + return time.Unix(n, 0), nil + } + } + rv, err := parseCanonicalTime(in) + if err == nil { + return rv, nil + } + for _, f := range timeFormats { + parsed, err := time.Parse(f, in) + if err == nil { + return parsed, nil + } + } + return time.Time{}, errUnparseableTimestamp +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/time_test.go b/vendor/github.com/camlistore/camlistore/pkg/fs/time_test.go new file mode 100644 index 00000000..56fb9fd1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/time_test.go @@ -0,0 +1,165 @@ +// +build linux darwin + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "log" + "testing" + "time" +) + +const exampleTimeString = "2012-08-28T21:24:35.37465188Z" +const milliAccuracy = "2012-08-28T21:24:35.374Z" +const secondAccuracy = "2012-08-28T21:24:35Z" + +var exampleTime time.Time + +func init() { + var err error + exampleTime, err = time.Parse(time.RFC3339, exampleTimeString) + if err != nil { + panic(err) + } + if exampleTimeString != exampleTime.UTC().Format(time.RFC3339Nano) { + log.Panicf("Expected %v, got %v", exampleTimeString, + exampleTime.UTC().Format(time.RFC3339Nano)) + } +} + +func TestTimeParsing(t *testing.T) { + tests := []struct { + input string + exp string + }{ + {"1346189075374651880", exampleTimeString}, + {"1346189075374", milliAccuracy}, + {"1346189075", secondAccuracy}, + {"2012-08-28T21:24:35.37465188Z", exampleTimeString}, + {secondAccuracy, secondAccuracy}, + {"Tue, 28 Aug 2012 21:24:35 +0000", secondAccuracy}, + {"Tue, 28 Aug 2012 21:24:35 UTC", secondAccuracy}, + {"Tue Aug 28 21:24:35 UTC 2012", secondAccuracy}, + {"Tue Aug 28 21:24:35 2012", secondAccuracy}, + {"Tue Aug 28 21:24:35 +0000 2012", secondAccuracy}, + {"2012-08-28T21:24", "2012-08-28T21:24:00Z"}, + {"2012-08-28T21", "2012-08-28T21:00:00Z"}, + {"2012-08-28", "2012-08-28T00:00:00Z"}, + {"2012-08", "2012-08-01T00:00:00Z"}, + {"2012", "2012-01-01T00:00:00Z"}, + } + + for _, x := range tests { + tm, err := parseTime(x.input) + if err != nil { + t.Errorf("Error on %v - %v", x.input, err) + t.Fail() + } + got := tm.UTC().Format(time.RFC3339Nano) + if x.exp != got { + t.Errorf("Expected %v for %v, got %v", x.exp, x.input, got) + t.Fail() + } + } +} + +func TestCanonicalParser(t *testing.T) { + tests := []struct { + input string + exp string + }{ + {"2012-08-28T21:24:35.374651883Z", ""}, + {"2012-08-28T21:24:35.37465188Z", ""}, + {"2012-08-28T21:24:35.3746518Z", ""}, + {"2012-08-28T21:24:35.374651Z", ""}, + {"2012-08-28T21:24:35.37465Z", ""}, + {"2012-08-28T21:24:35.3746Z", ""}, + {"2012-08-28T21:24:35.374Z", ""}, + {"2012-08-28T21:24:35.37Z", ""}, + {"2012-08-28T21:24:35.3Z", ""}, + {"2012-08-28T21:24:35.0Z", "2012-08-28T21:24:35Z"}, + {"2012-08-28T21:24:35.Z", "2012-08-28T21:24:35Z"}, + {"2012-08-28T21:24:35Z", ""}, + } + + for _, x := range tests { + tm, err := parseCanonicalTime(x.input) + if err != nil { + t.Errorf("Error on %v - %v", x.input, err) + t.Fail() + } + got := tm.UTC().Format(time.RFC3339Nano) + exp := x.exp + if exp == "" { + exp = x.input + } + if exp != got { + t.Errorf("Expected %v for %v, got %v", x.exp, x.input, got) + t.Fail() + } + } +} + +func benchTimeParsing(b *testing.B, input string) { + for i := 0; i < b.N; i++ { + _, err := parseTime(input) + if err != nil { + b.Fatalf("Error on %v - %v", input, err) + } + } +} + +func BenchmarkParseTimeCanonicalDirect(b *testing.B) { + input := "2012-08-28T21:24:35.37465188Z" + for i := 0; i < b.N; i++ { + _, err := parseCanonicalTime(input) + if err != nil { + b.Fatalf("Error on %v - %v", input, err) + } + } +} + +func BenchmarkParseTimeCanonicalStdlib(b *testing.B) { + input := "2012-08-28T21:24:35.37465188Z" + for i := 0; i < b.N; i++ { + _, err := time.Parse(time.RFC3339, input) + if err != nil { + b.Fatalf("Error on %v - %v", input, err) + } + } +} + +func BenchmarkParseTimeCanonical(b *testing.B) { + benchTimeParsing(b, "2012-08-28T21:24:35.37465188Z") +} + +func BenchmarkParseTimeMisc(b *testing.B) { + benchTimeParsing(b, "Tue, 28 Aug 2012 21:24:35 +0000") +} + +func BenchmarkParseTimeIntNano(b *testing.B) { + benchTimeParsing(b, "1346189075374651880") +} + +func BenchmarkParseTimeIntMillis(b *testing.B) { + benchTimeParsing(b, "1346189075374") +} + +func BenchmarkParseTimeIntSecs(b *testing.B) { + benchTimeParsing(b, "1346189075") +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/util.go b/vendor/github.com/camlistore/camlistore/pkg/fs/util.go new file mode 100644 index 00000000..c158aa50 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/util.go @@ -0,0 +1,53 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "errors" + "os/exec" + "runtime" + "time" +) + +// Unmount attempts to unmount the provided FUSE mount point, forcibly +// if necessary. +func Unmount(point string) error { + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("/usr/sbin/diskutil", "umount", "force", point) + case "linux": + cmd = exec.Command("fusermount", "-u", point) + default: + return errors.New("unmount: unimplemented") + } + + errc := make(chan error, 1) + go func() { + if err := exec.Command("umount", point).Run(); err == nil { + errc <- err + } + // retry to unmount with the fallback cmd + errc <- cmd.Run() + }() + select { + case <-time.After(1 * time.Second): + return errors.New("umount timeout") + case err := <-errc: + return err + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/xattr.go b/vendor/github.com/camlistore/camlistore/pkg/fs/xattr.go new file mode 100644 index 00000000..b8c29f1a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/xattr.go @@ -0,0 +1,162 @@ +// +build linux darwin + +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "encoding/base64" + "log" + "strings" + "sync" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/search" + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs" +) + +// xattrPrefix is the permanode attribute prefix used for record +// extended attributes. +const xattrPrefix = "xattr:" + +// xattr provides common support for extended attributes for various +// file and directory implementations (fuse.Node) within the FUSE services. +type xattr struct { + typeName string // for logging + fs *CamliFileSystem + permanode blob.Ref + + // mu guards xattrs. Both mu and the xattrs map are provided by the + // fuse.Node when the struct is created. + mu *sync.Mutex + + // This is a pointer to the particular fuse.Node's location of its + // xattr map so that it can be initialized commonly when the fuse.Node + // calls xattr.load(*search.DescribedPermanode) + xattrs *map[string][]byte +} + +// load is invoked after the creation of a fuse.Node that may contain extended +// attributes. This creates the node's xattr map as well as fills it with any +// extended attributes found in the permanode's claims. +func (x *xattr) load(p *search.DescribedPermanode) { + x.mu.Lock() + defer x.mu.Unlock() + + *x.xattrs = map[string][]byte{} + for k, v := range p.Attr { + if strings.HasPrefix(k, xattrPrefix) { + name := k[len(xattrPrefix):] + val, err := base64.StdEncoding.DecodeString(v[0]) + if err != nil { + log.Printf("Base64 decoding error on attribute %v: %v", name, err) + continue + } + (*x.xattrs)[name] = val + } + } +} + +func (x *xattr) set(req *fuse.SetxattrRequest) fuse.Error { + log.Printf("%s.setxattr(%q) -> %q", x.typeName, req.Name, req.Xattr) + + claim := schema.NewSetAttributeClaim(x.permanode, xattrPrefix+req.Name, + base64.StdEncoding.EncodeToString(req.Xattr)) + _, err := x.fs.client.UploadAndSignBlob(claim) + if err != nil { + log.Printf("Error setting xattr: %v", err) + return fuse.EIO + } + + val := make([]byte, len(req.Xattr)) + copy(val, req.Xattr) + x.mu.Lock() + (*x.xattrs)[req.Name] = val + x.mu.Unlock() + + return nil +} + +func (x *xattr) remove(req *fuse.RemovexattrRequest) fuse.Error { + log.Printf("%s.Removexattr(%q)", x.typeName, req.Name) + + claim := schema.NewDelAttributeClaim(x.permanode, xattrPrefix+req.Name, "") + _, err := x.fs.client.UploadAndSignBlob(claim) + + if err != nil { + log.Printf("Error removing xattr: %v", err) + return fuse.EIO + } + + x.mu.Lock() + delete(*x.xattrs, req.Name) + x.mu.Unlock() + + return nil +} + +func (x *xattr) get(req *fuse.GetxattrRequest, res *fuse.GetxattrResponse) fuse.Error { + x.mu.Lock() + defer x.mu.Unlock() + + val, found := (*x.xattrs)[req.Name] + + if !found { + return fuse.ENODATA + } + + res.Xattr = val + + return nil +} + +func (x *xattr) list(req *fuse.ListxattrRequest, res *fuse.ListxattrResponse) fuse.Error { + x.mu.Lock() + defer x.mu.Unlock() + + for k := range *x.xattrs { + res.Xattr = append(res.Xattr, k...) + res.Xattr = append(res.Xattr, '\x00') + } + return nil +} + +// noXattr provides default xattr methods for fuse nodes. The fuse +// package itself defaults to ENOSYS which causes some systems (read: +// MacOSX) to assume that no extended attribute support is available +// anywhere in the filesystem. This different set of defaults just +// returns no values for read requests and permission denied for write +// requests. +type noXattr struct{} + +func (n noXattr) Getxattr(*fuse.GetxattrRequest, *fuse.GetxattrResponse, fs.Intr) fuse.Error { + return fuse.ENODATA +} + +func (n noXattr) Listxattr(*fuse.ListxattrRequest, *fuse.ListxattrResponse, fs.Intr) fuse.Error { + return nil +} + +func (n noXattr) Setxattr(*fuse.SetxattrRequest, fs.Intr) fuse.Error { + return fuse.EPERM +} + +func (n noXattr) Removexattr(*fuse.RemovexattrRequest, fs.Intr) fuse.Error { + return fuse.EPERM +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/fs/z_test.go b/vendor/github.com/camlistore/camlistore/pkg/fs/z_test.go new file mode 100644 index 00000000..bca132e4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/fs/z_test.go @@ -0,0 +1,34 @@ +// +build linux darwin + +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "testing" + + "camlistore.org/pkg/test" +) + +// Make sure that the camlistored process started +// by the World gets terminated when all the tests +// are done. +// This works only as long as TestZLastTest is the +// last test to run in the package. +func TestZLastTest(t *testing.T) { + test.GetWorldMaybe(t).Stop() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/gc/gc.go b/vendor/github.com/camlistore/camlistore/pkg/gc/gc.go new file mode 100644 index 00000000..06a1298a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/gc/gc.go @@ -0,0 +1,198 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package gc defines a generic garbage collector. +package gc + +import ( + "errors" + "fmt" + + "camlistore.org/pkg/context" + "camlistore.org/pkg/syncutil" +) + +const buffered = 32 // arbitrary + +// Item is something that exists that may or may not survive a GC collection. +type Item interface{} + +// A Collector performs a garbage collection. +type Collector struct { + // World specifies a World that should be stopped before a + // collection and started again after. + World World + + Marker Marker + Roots Enumerator + Sweeper Enumerator + ItemEnumerator ItemEnumerator + Deleter Deleter +} + +type Marker interface { + // Mark marks that an item should exist. + // It must be safe for calls from concurrent goroutines. + Mark(Item) error + + // IsMarked returns whether the item is marked. + // It must be safe for calls from concurrent goroutines. + IsMarked(Item) (bool, error) +} + +// World defines the thing that should be stopped before GC and started after. +type World interface { + Stop() error + Start() error +} + +type Deleter interface { + // Delete deletes an item that was deemed unreachable via + // the garbage collector. + // It must be safe for calls from concurrent goroutines. + Delete(Item) error +} + +// Enumerator enumerates items. +type Enumerator interface { + // Enumerate enumerates items (which items depends on usage) + // and sends them to the provided channel. Regardless of return + // value, the channel should be closed. + // + // If the provided context is closed, Enumerate should return + // with an error (typically context.ErrCanceled) + Enumerate(*context.Context, chan<- Item) error +} + +// ItemEnumerator enumerates all the edges out from an item. +type ItemEnumerator interface { + // EnumerateItme is like Enuerator's Enumerate, but specific + // to the provided item. + EnumerateItem(*context.Context, Item, chan<- Item) error +} + +// ctx will be canceled on failure +func (c *Collector) markItem(ctx *context.Context, it Item, isRoot bool) error { + if !isRoot { + marked, err := c.Marker.IsMarked(it) + if err != nil { + return err + } + if marked { + return nil + } + } + if err := c.Marker.Mark(it); err != nil { + return err + } + + ch := make(chan Item, buffered) + var grp syncutil.Group + grp.Go(func() error { + return c.ItemEnumerator.EnumerateItem(ctx, it, ch) + }) + grp.Go(func() error { + for it := range ch { + if err := c.markItem(ctx, it, false); err != nil { + return err + } + } + return nil + }) + if err := grp.Err(); err != nil { + ctx.Cancel() + return err + } + return nil +} + +// Collect performs a garbage collection. +func (c *Collector) Collect(ctx *context.Context) (err error) { + if c.World == nil { + return errors.New("no World") + } + if c.Marker == nil { + return errors.New("no Marker") + } + if c.Roots == nil { + return errors.New("no Roots") + } + if c.Sweeper == nil { + return errors.New("no Sweeper") + } + if c.ItemEnumerator == nil { + return errors.New("no ItemEnumerator") + } + if c.Deleter == nil { + return errors.New("no Deleter") + } + if err := c.World.Stop(); err != nil { + return err + } + defer func() { + startErr := c.World.Start() + if err == nil { + err = startErr + } + }() + + // Mark. + roots := make(chan Item, buffered) + markCtx := ctx.New() + var marker syncutil.Group + marker.Go(func() error { + defer markCtx.Cancel() + for it := range roots { + if err := c.markItem(markCtx, it, true); err != nil { + return err + } + } + return nil + }) + marker.Go(func() error { + return c.Roots.Enumerate(markCtx, roots) + }) + if err := marker.Err(); err != nil { + return fmt.Errorf("Mark failure: %v", err) + } + + // Sweep. + all := make(chan Item, buffered) + sweepCtx := ctx.New() + var sweeper syncutil.Group + sweeper.Go(func() error { + return c.Sweeper.Enumerate(sweepCtx, all) + }) + sweeper.Go(func() error { + defer sweepCtx.Done() + for it := range all { + ok, err := c.Marker.IsMarked(it) + if err != nil { + return err + } + if !ok { + if err := c.Deleter.Delete(it); err != nil { + return err + } + } + } + return nil + }) + if err := sweeper.Err(); err != nil { + return fmt.Errorf("Sweep failure: %v", err) + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/gc/gc_test.go b/vendor/github.com/camlistore/camlistore/pkg/gc/gc_test.go new file mode 100644 index 00000000..82edef3e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/gc/gc_test.go @@ -0,0 +1,183 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gc + +import ( + "reflect" + "sort" + "testing" + + "camlistore.org/pkg/context" +) + +func sl(v ...string) []string { + if len(v) == 0 { + return nil + } + return v +} + +var collectTests = []struct { + name string + world []string + roots []string + graph map[string][]string + wantWorld []string +}{ + { + name: "delete everything", + world: sl("a", "b", "c"), + wantWorld: sl(), + }, + + { + name: "keep everything", + world: sl("a", "b", "c"), + roots: sl("a", "b", "c"), + wantWorld: sl("a", "b", "c"), + }, + + { + name: "keep all via chain", + world: sl("a", "b", "c", "d", "e"), + roots: sl("a"), + graph: map[string][]string{ + "a": sl("b"), + "b": sl("c"), + "c": sl("d"), + "d": sl("e"), + }, + wantWorld: sl("a", "b", "c", "d", "e"), + }, + + { + name: "keep all via fan", + world: sl("a", "b", "c", "d", "e"), + roots: sl("a"), + graph: map[string][]string{ + "a": sl("b", "c", "d", "e"), + }, + wantWorld: sl("a", "b", "c", "d", "e"), + }, + + { + name: "c dies, two roots", + world: sl("a", "b", "c", "d", "e"), + roots: sl("a", "d"), + graph: map[string][]string{ + "a": sl("b"), + "d": sl("e"), + }, + wantWorld: sl("a", "b", "d", "e"), + }, +} + +type worldSet map[string]bool + +func newWorldSet(start []string) worldSet { + s := make(worldSet) + for _, v := range start { + s[v] = true + } + return s +} + +func (s worldSet) Delete(it Item) error { + delete(s, it.(string)) + return nil +} + +func (s worldSet) items() []string { + if len(s) == 0 { + return nil + } + ret := make([]string, 0, len(s)) + for it := range s { + ret = append(ret, it) + } + sort.Strings(ret) + return ret +} + +func TestCollector(t *testing.T) { + for _, tt := range collectTests { + if tt.name == "" { + panic("no name in test") + } + w := newWorldSet(tt.world) + c := &Collector{ + World: testWorld{}, + Marker: testMarker(map[Item]bool{}), + Roots: testEnum(tt.roots), + Sweeper: testEnum(tt.world), + ItemEnumerator: testItemEnum(tt.graph), + Deleter: w, + } + if err := c.Collect(context.New()); err != nil { + t.Errorf("%s: Collect = %v", tt.name, err) + } + got := w.items() + if !reflect.DeepEqual(tt.wantWorld, got) { + t.Errorf("%s: world = %q; want %q", tt.name, got, tt.wantWorld) + } + } +} + +type testEnum []string + +func (s testEnum) Enumerate(ctx *context.Context, dest chan<- Item) error { + defer close(dest) + for _, v := range s { + select { + case dest <- v: + case <-ctx.Done(): + return context.ErrCanceled + } + } + return nil +} + +type testItemEnum map[string][]string + +func (m testItemEnum) EnumerateItem(ctx *context.Context, it Item, dest chan<- Item) error { + defer close(dest) + for _, v := range m[it.(string)] { + select { + case dest <- v: + case <-ctx.Done(): + return context.ErrCanceled + } + } + return nil +} + +type testMarker map[Item]bool + +func (m testMarker) Mark(it Item) error { + m[it] = true + return nil +} + +func (m testMarker) IsMarked(it Item) (v bool, err error) { + v = m[it] + return +} + +type testWorld struct{} + +func (testWorld) Start() error { return nil } +func (testWorld) Stop() error { return nil } diff --git a/vendor/github.com/camlistore/camlistore/pkg/geocode/geocode.go b/vendor/github.com/camlistore/camlistore/pkg/geocode/geocode.go new file mode 100644 index 00000000..15254566 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/geocode/geocode.go @@ -0,0 +1,116 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package geocode handles mapping user-entered locations into lat/long polygons. +package geocode + +import ( + "encoding/json" + "io" + "log" + "net/url" + "sync" + + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/singleflight" +) + +type LatLong struct { + Lat float64 `json:"lat"` + Long float64 `json:"lng"` +} + +type Rect struct { + NorthEast LatLong `json:"northeast"` + SouthWest LatLong `json:"southwest"` +} + +var ( + mu sync.RWMutex + cache = map[string][]Rect{} + + sf singleflight.Group +) + +// Lookup returns rectangles for the given address. Currently the only +// implementation is the Google geocoding service. +func Lookup(ctx *context.Context, address string) ([]Rect, error) { + mu.RLock() + rects, ok := cache[address] + mu.RUnlock() + if ok { + return rects, nil + } + + rectsi, err := sf.Do(address, func() (interface{}, error) { + // TODO: static data files from OpenStreetMap, Wikipedia, etc? + urlStr := "https://maps.googleapis.com/maps/api/geocode/json?address=" + url.QueryEscape(address) + "&sensor=false" + res, err := ctx.HTTPClient().Get(urlStr) + if err != nil { + return nil, err + } + defer httputil.CloseBody(res.Body) + rects, err := decodeGoogleResponse(res.Body) + log.Printf("Google geocode lookup (%q) = %#v, %v", address, rects, err) + if err == nil { + mu.Lock() + cache[address] = rects + mu.Unlock() + } + return rects, err + }) + if err != nil { + return nil, err + } + return rectsi.([]Rect), nil +} + +type googleResTop struct { + Results []*googleResult `json:"results"` +} + +type googleResult struct { + Geometry *googleGeometry `json:"geometry"` +} + +type googleGeometry struct { + Bounds *Rect `json:"bounds"` + Viewport *Rect `json:"viewport"` +} + +func decodeGoogleResponse(r io.Reader) (rects []Rect, err error) { + var resTop googleResTop + if err := json.NewDecoder(r).Decode(&resTop); err != nil { + return nil, err + } + for _, res := range resTop.Results { + if res.Geometry != nil && res.Geometry.Bounds != nil { + r := res.Geometry.Bounds + if r.NorthEast.Lat == 90 && r.NorthEast.Long == 180 && + r.SouthWest.Lat == -90 && r.SouthWest.Long == -180 { + // Google sometimes returns a "whole world" rect for large addresses (like "USA") + // so instead use the viewport in that case. + if res.Geometry.Viewport != nil { + rects = append(rects, *res.Geometry.Viewport) + } + } else { + rects = append(rects, *r) + } + } + } + return +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/geocode/geocode_test.go b/vendor/github.com/camlistore/camlistore/pkg/geocode/geocode_test.go new file mode 100644 index 00000000..660636b9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/geocode/geocode_test.go @@ -0,0 +1,238 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package geocode + +import ( + "reflect" + "strconv" + "strings" + "testing" +) + +func TestDecodeGoogleResponse(t *testing.T) { + tests := []struct { + name string + res string + want []Rect + }{ + { + name: "moscow", + res: googleMoscow, + want: []Rect{ + Rect{ + NorthEast: LatLong{pf("56.009657"), pf("37.945661")}, + SouthWest: LatLong{pf("55.48992699999999"), pf("37.319329")}, + }, + Rect{ + NorthEast: LatLong{pf("46.758882"), pf("-116.962068")}, + SouthWest: LatLong{pf("46.710912"), pf("-117.039698")}, + }, + }, + }, + { + name: "usa", + res: googleUSA, + want: []Rect{ + Rect{ + NorthEast: LatLong{pf("49.38"), pf("-66.94")}, + SouthWest: LatLong{pf("25.82"), pf("-124.39")}, + }, + }, + }, + } + for _, tt := range tests { + rects, err := decodeGoogleResponse(strings.NewReader(tt.res)) + if err != nil { + t.Errorf("Decoding %s: %v", tt.name, err) + continue + } + if !reflect.DeepEqual(rects, tt.want) { + t.Errorf("Test %s: wrong rects\n Got %#v\nWant %#v", tt.name, rects, tt.want) + } + } +} + +// parseFloat64 +func pf(s string) float64 { + v, err := strconv.ParseFloat(s, 64) + if err != nil { + panic(err) + } + return v +} + +var googleMoscow = ` +{ + "results" : [ + { + "address_components" : [ + { + "long_name" : "Moscow", + "short_name" : "Moscow", + "types" : [ "locality", "political" ] + }, + { + "long_name" : "gorod Moskva", + "short_name" : "g. Moskva", + "types" : [ "administrative_area_level_2", "political" ] + }, + { + "long_name" : "Moscow", + "short_name" : "Moscow", + "types" : [ "administrative_area_level_1", "political" ] + }, + { + "long_name" : "Russia", + "short_name" : "RU", + "types" : [ "country", "political" ] + } + ], + "formatted_address" : "Moscow, Russia", + "geometry" : { + "bounds" : { + "northeast" : { + "lat" : 56.009657, + "lng" : 37.945661 + }, + "southwest" : { + "lat" : 55.48992699999999, + "lng" : 37.319329 + } + }, + "location" : { + "lat" : 55.755826, + "lng" : 37.6173 + }, + "location_type" : "APPROXIMATE", + "viewport" : { + "northeast" : { + "lat" : 56.009657, + "lng" : 37.945661 + }, + "southwest" : { + "lat" : 55.48992699999999, + "lng" : 37.319329 + } + } + }, + "types" : [ "locality", "political" ] + }, + { + "address_components" : [ + { + "long_name" : "Moscow", + "short_name" : "Moscow", + "types" : [ "locality", "political" ] + }, + { + "long_name" : "Latah", + "short_name" : "Latah", + "types" : [ "administrative_area_level_2", "political" ] + }, + { + "long_name" : "Idaho", + "short_name" : "ID", + "types" : [ "administrative_area_level_1", "political" ] + }, + { + "long_name" : "United States", + "short_name" : "US", + "types" : [ "country", "political" ] + } + ], + "formatted_address" : "Moscow, ID, USA", + "geometry" : { + "bounds" : { + "northeast" : { + "lat" : 46.758882, + "lng" : -116.962068 + }, + "southwest" : { + "lat" : 46.710912, + "lng" : -117.039698 + } + }, + "location" : { + "lat" : 46.73238749999999, + "lng" : -117.0001651 + }, + "location_type" : "APPROXIMATE", + "viewport" : { + "northeast" : { + "lat" : 46.758882, + "lng" : -116.962068 + }, + "southwest" : { + "lat" : 46.710912, + "lng" : -117.039698 + } + } + }, + "types" : [ "locality", "political" ] + } + ], + "status" : "OK" +} +` + +// Response for "usa". +// Note the geometry bounds covering the whole world. In this case, use the viewport instead. +var googleUSA = ` +{ + "results" : [ + { + "address_components" : [ + { + "long_name" : "United States", + "short_name" : "US", + "types" : [ "country", "political" ] + } + ], + "formatted_address" : "United States", + "geometry" : { + "bounds" : { + "northeast" : { + "lat" : 90, + "lng" : 180 + }, + "southwest" : { + "lat" : -90, + "lng" : -180 + } + }, + "location" : { + "lat" : 37.09024, + "lng" : -95.712891 + }, + "location_type" : "APPROXIMATE", + "viewport" : { + "northeast" : { + "lat" : 49.38, + "lng" : -66.94 + }, + "southwest" : { + "lat" : 25.82, + "lng" : -124.39 + } + } + }, + "types" : [ "country", "political" ] + } + ], + "status" : "OK" +} +` diff --git a/vendor/github.com/camlistore/camlistore/pkg/googlestorage/README b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/README new file mode 100644 index 00000000..b1390059 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/README @@ -0,0 +1,41 @@ +Implements the Storage interface for Google Storage. +A GoogleStorage instance stores blobs in a single Google Storage bucket, with +each blob keyed by its blobref. + +Server configuration +===================== + +High-level server config is formatted like: + + "googlecloudstorage": "clientId:clientSecret:refreshToken:bucketName" + + +Testing +======== + +googlestorage_test.go contains integration tests which run against Google Storage. +In order to run these tests properly, you will need to: + +1. Set up google storage. See: + http://code.google.com/apis/storage/docs/signup.html + +2. Upload the contents of the testdata dir to a google storage bucket. Note + that all these files begin with 'test-': such files will be ignored when + the bucket is used as blobserver storage. + +3. Create the config file '~/.config/camlistore/gstestconfig.json'. The + file should look something like this: + + { + "gsconf": { + "auth": { + "client_id": "your client id", + "client_secret": "your client secret", + "refresh_token": "a refresh token" + }, + "bucket": "bucketName" + } + } + + + You can use 'camtool googinit' to help obtain the auth config object. diff --git a/vendor/github.com/camlistore/camlistore/pkg/googlestorage/googlestorage.go b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/googlestorage.go new file mode 100644 index 00000000..c629755d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/googlestorage.go @@ -0,0 +1,327 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package googlestorage is simple Google Cloud Storage client. +// +// It does not include any Camlistore-specific logic. +package googlestorage + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "unicode/utf8" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/httputil" + + "golang.org/x/net/context" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + api "google.golang.org/api/storage/v1" + "google.golang.org/cloud/compute/metadata" +) + +const ( + gsAccessURL = "https://storage.googleapis.com" + // Scope is the OAuth2 scope used for Google Cloud Storage. + Scope = "https://www.googleapis.com/auth/devstorage.read_write" +) + +// A Client provides access to Google Cloud Storage. +type Client struct { + client *http.Client + service *api.Service +} + +// An Object holds the name of an object (its bucket and key) within +// Google Cloud Storage. +type Object struct { + Bucket string + Key string +} + +func (o *Object) valid() error { + if o == nil { + return errors.New("invalid nil Object") + } + if o.Bucket == "" { + return errors.New("missing required Bucket field in Object") + } + if o.Key == "" { + return errors.New("missing required Key field in Object") + } + return nil +} + +// A SizedObject holds the bucket, key, and size of an object. +type SizedObject struct { + Object + Size int64 +} + +// NewServiceClient returns a Client for use when running on Google +// Compute Engine. This client can access buckets owned by the same +// project ID as the VM. +func NewServiceClient() (*Client, error) { + if !metadata.OnGCE() { + return nil, errors.New("not running on Google Compute Engine") + } + scopes, _ := metadata.Scopes("default") + haveScope := func(scope string) bool { + for _, x := range scopes { + if x == scope { + return true + } + } + return false + } + if !haveScope("https://www.googleapis.com/auth/devstorage.full_control") && + !haveScope("https://www.googleapis.com/auth/devstorage.read_write") { + return nil, errors.New("when this Google Compute Engine VM instance was created, it wasn't granted access to Cloud Storage") + } + client := oauth2.NewClient(context.Background(), google.ComputeTokenSource("")) + service, _ := api.New(client) + return &Client{client: client, service: service}, nil +} + +func NewClient(oauthClient *http.Client) *Client { + service, _ := api.New(oauthClient) + return &Client{ + client: oauthClient, + service: service, + } +} + +func (o *Object) String() string { + if o == nil { + return "" + } + return fmt.Sprintf("%v/%v", o.Bucket, o.Key) +} + +func (so SizedObject) String() string { + return fmt.Sprintf("%v/%v (%vB)", so.Bucket, so.Key, so.Size) +} + +// Makes a simple body-less google storage request +func (gsa *Client) simpleRequest(method, url_ string) (resp *http.Response, err error) { + // Construct the request + req, err := http.NewRequest(method, url_, nil) + if err != nil { + return + } + req.Header.Set("x-goog-api-version", "2") + + return gsa.client.Do(req) +} + +// GetObject fetches a Google Cloud Storage object. +// The caller must close rc. +func (c *Client) GetObject(obj *Object) (rc io.ReadCloser, size int64, err error) { + if err = obj.valid(); err != nil { + return + } + resp, err := c.simpleRequest("GET", gsAccessURL+"/"+obj.Bucket+"/"+obj.Key) + if err != nil { + return nil, 0, fmt.Errorf("GS GET request failed: %v\n", err) + } + if resp.StatusCode == http.StatusNotFound { + resp.Body.Close() + return nil, 0, os.ErrNotExist + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, 0, fmt.Errorf("GS GET request failed status: %v\n", resp.Status) + } + + return resp.Body, resp.ContentLength, nil +} + +// GetPartialObject fetches part of a Google Cloud Storage object. +// If length is negative, the rest of the object is returned. +// The caller must close rc. +func (c *Client) GetPartialObject(obj Object, offset, length int64) (rc io.ReadCloser, err error) { + if offset < 0 || length < 0 { + return nil, blob.ErrNegativeSubFetch + } + if err = obj.valid(); err != nil { + return + } + + req, err := http.NewRequest("GET", gsAccessURL+"/"+obj.Bucket+"/"+obj.Key, nil) + if err != nil { + return + } + req.Header.Set("x-goog-api-version", "2") + if length >= 0 { + req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1)) + } else { + req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset)) + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("GS GET request failed: %v\n", err) + } + if resp.StatusCode == http.StatusNotFound { + resp.Body.Close() + return nil, os.ErrNotExist + } + if !(resp.StatusCode == http.StatusPartialContent || (offset == 0 && resp.StatusCode == http.StatusOK)) { + resp.Body.Close() + if resp.StatusCode == http.StatusRequestedRangeNotSatisfiable { + return nil, blob.ErrOutOfRangeOffsetSubFetch + } + return nil, fmt.Errorf("GS GET request failed status: %v\n", resp.Status) + } + + return resp.Body, nil +} + +// StatObject checks for the size & existence of a Google Cloud Storage object. +// Non-existence of a file is not an error. +func (gsa *Client) StatObject(obj *Object) (size int64, exists bool, err error) { + if err = obj.valid(); err != nil { + return + } + res, err := gsa.simpleRequest("HEAD", gsAccessURL+"/"+obj.Bucket+"/"+obj.Key) + if err != nil { + return + } + res.Body.Close() // per contract but unnecessary for most RoundTrippers + + switch res.StatusCode { + case http.StatusNotFound: + return 0, false, nil + case http.StatusOK: + if size, err = strconv.ParseInt(res.Header["Content-Length"][0], 10, 64); err != nil { + return + } + return size, true, nil + default: + return 0, false, fmt.Errorf("Bad head response code: %v", res.Status) + } +} + +// PutObject uploads a Google Cloud Storage object. +// shouldRetry will be true if the put failed due to authorization, but +// credentials have been refreshed and another attempt is likely to succeed. +// In this case, content will have been consumed. +func (gsa *Client) PutObject(obj *Object, content io.Reader) error { + if err := obj.valid(); err != nil { + return err + } + const maxSlurp = 2 << 20 + var buf bytes.Buffer + n, err := io.CopyN(&buf, content, maxSlurp) + if err != nil && err != io.EOF { + return err + } + contentType := http.DetectContentType(buf.Bytes()) + if contentType == "application/octet-stream" && n < maxSlurp && utf8.Valid(buf.Bytes()) { + contentType = "text/plain; charset=utf-8" + } + + objURL := gsAccessURL + "/" + obj.Bucket + "/" + obj.Key + var req *http.Request + if req, err = http.NewRequest("PUT", objURL, ioutil.NopCloser(io.MultiReader(&buf, content))); err != nil { + return err + } + req.Header.Set("x-goog-api-version", "2") + req.Header.Set("Content-Type", contentType) + + var resp *http.Response + if resp, err = gsa.client.Do(req); err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Bad put response code: %v", resp.Status) + } + return nil +} + +// DeleteObject removes an object. +func (gsa *Client) DeleteObject(obj *Object) error { + if err := obj.valid(); err != nil { + return err + } + resp, err := gsa.simpleRequest("DELETE", gsAccessURL+"/"+obj.Bucket+"/"+obj.Key) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("Error deleting %v: bad delete response code: %v", obj, resp.Status) + } + return nil +} + +// EnumerateObjects lists the objects in a bucket. +// If after is non-empty, listing will begin with lexically greater object names. +// If limit is non-zero, the length of the list will be limited to that number. +func (gsa *Client) EnumerateObjects(bucket, after string, limit int) ([]SizedObject, error) { + // Build url, with query params + var params []string + if after != "" { + params = append(params, "marker="+url.QueryEscape(after)) + } + if limit > 0 { + params = append(params, fmt.Sprintf("max-keys=%v", limit)) + } + query := "" + if len(params) > 0 { + query = "?" + strings.Join(params, "&") + } + + resp, err := gsa.simpleRequest("GET", gsAccessURL+"/"+bucket+"/"+query) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Bad enumerate response code: %v", resp.Status) + } + + var xres struct { + Contents []SizedObject + } + defer httputil.CloseBody(resp.Body) + if err = xml.NewDecoder(resp.Body).Decode(&xres); err != nil { + return nil, err + } + + // Fill in the Bucket on all the SizedObjects + for _, o := range xres.Contents { + o.Bucket = bucket + } + + return xres.Contents, nil +} + +// BucketInfo returns information about a bucket. +func (c *Client) BucketInfo(bucket string) (*api.Bucket, error) { + return c.service.Buckets.Get(bucket).Do() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/googlestorage/googlestorage_test.go b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/googlestorage_test.go new file mode 100644 index 00000000..3ec2206b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/googlestorage_test.go @@ -0,0 +1,245 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// FYI These tests are integration tests that need to run against google +// storage. See the README for more details on necessary setup + +package googlestorage + +import ( + "bytes" + "flag" + "fmt" + "io/ioutil" + "testing" + "time" + + "camlistore.org/pkg/constants/google" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/oauthutil" + + "golang.org/x/oauth2" +) + +const testObjectContent = "Google Storage Test\n" + +type BufferCloser struct { + *bytes.Buffer +} + +func (b *BufferCloser) Close() error { + b.Reset() + return nil +} + +var gsConfigPath = flag.String("gs_config_path", "", "Path to Google Storage configuration JSON file, or empty to skip the test.") + +// Reads google storage config and creates a Client. Exits on error. +func doConfig(t *testing.T) (gsa *Client, bucket string) { + if *gsConfigPath == "" { + t.Skip("Skipping manual test. Set flag --gs_config_path to test Google Storage.") + } + + cf, err := jsonconfig.ReadFile(*gsConfigPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + + var config jsonconfig.Obj + config = cf.RequiredObject("gsconf") + if err := cf.Validate(); err != nil { + t.Fatalf("Invalid config: %v", err) + } + + auth := config.RequiredObject("auth") + bucket = config.RequiredString("bucket") + if err := config.Validate(); err != nil { + t.Fatalf("Invalid config: %v", err) + } + + gsa = NewClient(oauth2.NewClient(oauth2.NoContext, oauthutil.NewRefreshTokenSource(&oauth2.Config{ + Scopes: []string{Scope}, + Endpoint: google.Endpoint, + ClientID: auth.RequiredString("client_id"), + ClientSecret: auth.RequiredString("client_secret"), + RedirectURL: oauthutil.TitleBarRedirectURL, + }, auth.RequiredString("refresh_token")))) + + if err := auth.Validate(); err != nil { + t.Fatalf("Invalid config: %v", err) + } + return +} + +func TestGetPartialObject(t *testing.T) { + gs, bucket := doConfig(t) + + body, err := gs.GetPartialObject(Object{bucket, "test-get"}, 5, 10) + if err != nil { + t.Fatalf("Fetch failed: %v\n", err) + } + defer body.Close() + + contents, err := ioutil.ReadAll(body) + if err != nil { + t.Fatalf("Failed to get object contents: %v", err) + } + if len(contents) != 10 { + t.Fatalf("wrong contents size: got %d, want %d", len(contents), 10) + } + + if string(contents) != testObjectContent[5:15] { + t.Fatalf("Object has incorrect content.\nExpected: '%v'\nFound: '%v'\n", testObjectContent, string(contents)) + } +} + +func TestGetObject(t *testing.T) { + gs, bucket := doConfig(t) + + body, size, err := gs.GetObject(&Object{bucket, "test-get"}) + if err != nil { + t.Fatalf("Fetch failed: %v\n", err) + } + defer body.Close() + + content, err := ioutil.ReadAll(body) + if err != nil { + t.Fatalf("Failed to get object contents: %v", err) + } + if len(content) != int(size) { + t.Fatalf("wrong contents size: got %d, want %d", len(content), size) + } + + if string(content) != testObjectContent { + t.Fatalf("Object has incorrect content.\nExpected: '%v'\nFound: '%v'\n", testObjectContent, string(content)) + } +} + +func TestStatObject(t *testing.T) { + gs, bucket := doConfig(t) + + // Stat a nonexistant file + size, exists, err := gs.StatObject(&Object{bucket, "test-shouldntexist"}) + if err != nil { + t.Fatalf("Stat failed: %v\n", err) + } else { + if exists { + t.Errorf("Test object exists!") + } + if size != 0 { + t.Errorf("Expected size to be 0, found %v\n", size) + } + } + + // Try statting an object which does exist + size, exists, err = gs.StatObject(&Object{bucket, "test-stat"}) + if err != nil { + t.Fatalf("Stat failed: %v\n", err) + } else { + if !exists { + t.Errorf("Test object doesn't exist!") + } + if size != int64(len(testObjectContent)) { + t.Errorf("Test object size is wrong: \nexpected: %v\nfound: %v\n", + len(testObjectContent), size) + } + } +} + +func TestPutObject(t *testing.T) { + gs, bucket := doConfig(t) + + now := time.Now() + testKey := fmt.Sprintf("test-put-%v.%v.%v-%v.%v.%v", + now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) + + err := gs.PutObject(&Object{bucket, testKey}, + &BufferCloser{bytes.NewBufferString(testObjectContent)}) + if err != nil { + t.Fatalf("Failed to put object: %v", err) + } + + // Just stat to check that it actually uploaded, don't bother reading back + size, exists, err := gs.StatObject(&Object{bucket, testKey}) + if !exists { + t.Errorf("Test object doesn't exist!") + } + if size != int64(len(testObjectContent)) { + t.Errorf("Test object size is wrong: \nexpected: %v\nfound: %v\n", + len(testObjectContent), size) + } +} + +func TestDeleteObject(t *testing.T) { + gs, bucket := doConfig(t) + + // Try deleting a nonexitent file + err := gs.DeleteObject(&Object{bucket, "test-shouldntexist"}) + if err == nil { + t.Errorf("Tried to delete nonexistent object, succeeded.") + } + + // Create a file, try to delete it + now := time.Now() + testKey := fmt.Sprintf("test-delete-%v.%v.%v-%v.%v.%v", + now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) + err = gs.PutObject(&Object{bucket, testKey}, + &BufferCloser{bytes.NewBufferString("Delete Me")}) + if err != nil { + t.Fatalf("Failed to put file to delete.") + } + err = gs.DeleteObject(&Object{bucket, testKey}) + if err != nil { + t.Errorf("Failed to delete object: %v", err) + } +} + +func TestEnumerateBucket(t *testing.T) { + gs, bucket := doConfig(t) + + // Enumerate ALL the things! + objs, err := gs.EnumerateObjects(bucket, "", 0) + if err != nil { + t.Errorf("Enumeration failed: %v\n", err) + } else if len(objs) < 7 { + // Minimum number of blobs, equal to the number of files in testdata + t.Errorf("Expected at least 7 files, found %v", len(objs)) + } + + // Test a limited enum + objs, err = gs.EnumerateObjects(bucket, "", 5) + if err != nil { + t.Errorf("Enumeration failed: %v\n", err) + } else if len(objs) != 5 { + t.Errorf( + "Limited enum returned wrong number of blobs.\nExpected: %v\nFound: %v", + 5, len(objs)) + } + + // Test fetching a limited set from a known start point + objs, err = gs.EnumerateObjects(bucket, "test-enum", 4) + if err != nil { + t.Errorf("Enumeration failed: %v\n", err) + } else { + for i := 0; i < 4; i += 1 { + if objs[i].Key != fmt.Sprintf("test-enum-%v", i+1) { + t.Errorf( + "Enum from start point returned wrong key:\nExpected: test-enum-%v\nFound: %v", + i+1, objs[i].Key) + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum new file mode 100644 index 00000000..f2227372 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum @@ -0,0 +1 @@ +Google Storage Test diff --git a/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-1 b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-1 new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-1 @@ -0,0 +1 @@ +1 diff --git a/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-2 b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-2 new file mode 100644 index 00000000..0cfbf088 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-2 @@ -0,0 +1 @@ +2 diff --git a/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-3 b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-3 new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-3 @@ -0,0 +1 @@ +3 diff --git a/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-4 b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-4 new file mode 100644 index 00000000..b8626c4c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-enum-4 @@ -0,0 +1 @@ +4 diff --git a/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-get b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-get new file mode 100644 index 00000000..f2227372 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-get @@ -0,0 +1 @@ +Google Storage Test diff --git a/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-stat b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-stat new file mode 100644 index 00000000..f2227372 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/googlestorage/testdata/test-stat @@ -0,0 +1 @@ +Google Storage Test diff --git a/vendor/github.com/camlistore/camlistore/pkg/hashutil/hashutil.go b/vendor/github.com/camlistore/camlistore/pkg/hashutil/hashutil.go new file mode 100644 index 00000000..56847352 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/hashutil/hashutil.go @@ -0,0 +1,40 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package hashutil contains misc hashing functions lacking homes elsewhere. +package hashutil + +import ( + "crypto/sha1" + "crypto/sha256" + "fmt" +) + +// SHA256Prefix computes the SHA-256 digest of data and returns +// its first twenty lowercase hex digits. +func SHA256Prefix(data []byte) string { + h := sha256.New() + h.Write(data) + return fmt.Sprintf("%x", h.Sum(nil))[:20] +} + +// SHA1Prefix computes the SHA-1 digest of data and returns +// its first twenty lowercase hex digits. +func SHA1Prefix(data []byte) string { + h := sha1.New() + h.Write(data) + return fmt.Sprintf("%x", h.Sum(nil))[:20] +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/httputil/auth.go b/vendor/github.com/camlistore/camlistore/pkg/httputil/auth.go new file mode 100644 index 00000000..c9f84988 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/httputil/auth.go @@ -0,0 +1,101 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package httputil + +import ( + "encoding/base64" + "fmt" + "log" + "net/http" + "os" + "regexp" + "runtime" + "strings" + + "camlistore.org/pkg/netutil" +) + +var kBasicAuthPattern = regexp.MustCompile(`^Basic ([a-zA-Z0-9\+/=]+)`) + +// IsLocalhost reports whether the requesting connection is from this machine +// and has the same owner as this process. +func IsLocalhost(req *http.Request) bool { + uid := os.Getuid() + from, err := netutil.HostPortToIP(req.RemoteAddr, nil) + if err != nil { + return false + } + to, err := netutil.HostPortToIP(req.Host, from) + if err != nil { + return false + } + + // If our OS doesn't support uid. + // TODO(bradfitz): netutil on OS X uses "lsof" to figure out + // ownership of tcp connections, but when fuse is mounted and a + // request is outstanding (for instance, a fuse request that's + // making a request to camlistored and landing in this code + // path), lsof then blocks forever waiting on a lock held by the + // VFS, leading to a deadlock. Instead, on darwin, just trust + // any localhost connection here, which is kinda lame, but + // whatever. Macs aren't very multi-user anyway. + if uid == -1 || runtime.GOOS == "darwin" { + return from.IP.IsLoopback() && to.IP.IsLoopback() + } + if uid == 0 { + log.Printf("camlistored running as root. Don't do that.") + return false + } + if uid > 0 { + connUid, err := netutil.AddrPairUserid(from, to) + if err == nil { + if uid == connUid { + return true + } + log.Printf("auth: local connection uid %d doesn't match server uid %d", connUid, uid) + } + } + return false +} + +// BasicAuth parses the Authorization header on req +// If absent or invalid, an error is returned. +func BasicAuth(req *http.Request) (username, password string, err error) { + auth := req.Header.Get("Authorization") + if auth == "" { + err = fmt.Errorf("Missing \"Authorization\" in header") + return + } + matches := kBasicAuthPattern.FindStringSubmatch(auth) + if len(matches) != 2 { + err = fmt.Errorf("Bogus Authorization header") + return + } + encoded := matches[1] + enc := base64.StdEncoding + decBuf := make([]byte, enc.DecodedLen(len(encoded))) + n, err := enc.Decode(decBuf, []byte(encoded)) + if err != nil { + return + } + pieces := strings.SplitN(string(decBuf[0:n]), ":", 2) + if len(pieces) != 2 { + err = fmt.Errorf("didn't get two pieces") + return + } + return pieces[0], pieces[1], nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/httputil/auth_test.go b/vendor/github.com/camlistore/camlistore/pkg/httputil/auth_test.go new file mode 100644 index 00000000..fba2371f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/httputil/auth_test.go @@ -0,0 +1,182 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package httputil + +import ( + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "testing" +) + +func testServer(t *testing.T, l net.Listener) *httptest.Server { + ts := &httptest.Server{ + Listener: l, + Config: &http.Server{ + Handler: http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if IsLocalhost(r) { + fmt.Fprintf(rw, "authorized") + return + } + fmt.Fprintf(rw, "unauthorized") + }), + }, + } + ts.Start() + + return ts +} + +func TestLocalhostAuthIPv6(t *testing.T) { + l, err := net.Listen("tcp", "[::1]:0") + if err != nil { + t.Skip("skipping IPv6 test; can't listen on [::1]:0") + } + _, port, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + t.Fatal(err) + } + + // See if IPv6 works on this machine first. It seems the above + // Listen can pass on Linux but fail here in the dial. + c, err := net.Dial("tcp6", l.Addr().String()) + if err != nil { + t.Skipf("skipping IPv6 test; dial back to %s failed with %v", l.Addr(), err) + } + c.Close() + + ts := testServer(t, l) + defer ts.Close() + + // Use an explicit transport to force IPv6 (http.Get resolves localhost in IPv4 otherwise) + trans := &http.Transport{ + Dial: func(network, addr string) (net.Conn, error) { + c, err := net.Dial("tcp6", addr) + return c, err + }, + } + + testLoginRequest(t, &http.Client{Transport: trans}, "http://[::1]:"+port) + + // See if we can get an IPv6 from resolving localhost + localips, err := net.LookupIP("localhost") + if err != nil { + t.Skipf("skipping IPv6 test; resolving localhost failed with %v", err) + } + if hasIPv6(localips) { + testLoginRequest(t, &http.Client{Transport: trans}, "http://localhost:"+port) + } else { + t.Logf("incomplete IPv6 test; resolving localhost didn't return any IPv6 addresses") + } +} + +func hasIPv6(ips []net.IP) bool { + for _, ip := range ips { + if ip.To4() == nil { + return true + } + } + return false +} + +func TestLocalhostAuthIPv4(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Skip("skipping IPv4 test; can't listen on 127.0.0.1:0") + } + _, port, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + t.Fatal(err) + } + + ts := testServer(t, l) + defer ts.Close() + + testLoginRequest(t, &http.Client{}, "http://127.0.0.1:"+port) + testLoginRequest(t, &http.Client{}, "http://localhost:"+port) +} + +func testLoginRequest(t *testing.T, client *http.Client, URL string) { + res, err := client.Get(URL) + if err != nil { + t.Fatal(err) + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + const exp = "authorized" + if string(body) != exp { + t.Errorf("got %q (instead of %v)", string(body), exp) + } +} + +func TestBasicAuth(t *testing.T) { + for _, d := range []struct { + header string + u, pw string + valid bool + }{ + // Empty is invalid. + {}, + { + // Missing password. + header: "Basic QWxhZGRpbg==", + }, + { + // Malformed base64 encoding. + header: "Basic foo", + }, + { + // Malformed header, no 'Basic ' prefix. + header: "QWxhZGRpbg==", + }, + { + header: "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", + u: "Aladdin", + pw: "open sesame", + valid: true, + }, + } { + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + if d.header != "" { + req.Header.Set("Authorization", d.header) + } + + u, pw, err := BasicAuth(req) + t.Log(d.header, err) + if d.valid && err != nil { + t.Error("Want success parse of auth header, got", err) + } + if !d.valid && err == nil { + t.Error("Want error parsing", d.header) + } + + if d.u != u { + t.Errorf("Want user %q, got %q", d.u, u) + } + + if d.pw != pw { + t.Errorf("Want password %q, got %q", d.pw, pw) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/httputil/certs.go b/vendor/github.com/camlistore/camlistore/pkg/httputil/certs.go new file mode 100644 index 00000000..28fdd3d5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/httputil/certs.go @@ -0,0 +1,5383 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package httputil + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "log" + "math/big" + "net/http" + "runtime" + "sync" + "time" + + "camlistore.org/pkg/hashutil" + "camlistore.org/pkg/legal" + "camlistore.org/pkg/wkfs" +) + +var ( + poolOnce sync.Once + pool *x509.CertPool +) + +var ( + sysRootsOnce sync.Once + sysRootsGood bool +) + +// GenSelfTLS generates a self-signed certificate and key for hostname. +func GenSelfTLS(hostname string) (certPEM, keyPEM []byte, err error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return certPEM, keyPEM, fmt.Errorf("failed to generate private key: %s", err) + } + + now := time.Now() + + if hostname == "" { + hostname = "localhost" + } + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + log.Fatalf("failed to generate serial number: %s", err) + } + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: hostname, + Organization: []string{hostname}, + }, + NotBefore: now.Add(-5 * time.Minute).UTC(), + NotAfter: now.AddDate(1, 0, 0).UTC(), + SubjectKeyId: []byte{1, 2, 3, 4}, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + IsCA: true, + BasicConstraintsValid: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return certPEM, keyPEM, fmt.Errorf("failed to create certificate: %s", err) + } + var buf bytes.Buffer + if err := pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { + return certPEM, keyPEM, fmt.Errorf("error writing self-signed HTTPS cert: %v", err) + } + certPEM = []byte(string(buf.Bytes())) + + buf.Reset() + if err := pem.Encode(&buf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil { + return certPEM, keyPEM, fmt.Errorf("error writing self-signed HTTPS private key: %v", err) + } + keyPEM = buf.Bytes() + return certPEM, keyPEM, nil +} + +// CertFingerprint returns the SHA-256 prefix of the x509 certificate encoded in certPEM. +func CertFingerprint(certPEM []byte) (string, error) { + p, _ := pem.Decode(certPEM) + if p == nil { + return "", errors.New("no valid PEM data found") + } + cert, err := x509.ParseCertificate(p.Bytes) + if err != nil { + return "", fmt.Errorf("failed to parse certificate: %v", err) + } + return hashutil.SHA256Prefix(cert.Raw), nil +} + +// CertFingerprints returns a map of hash prefixes of the x509 certificate encoded in +// certPEM. The hashes are keyed by name ("SHA-1", and "SHA-256"). +func CertFingerprints(certPEM []byte) (map[string]string, error) { + p, _ := pem.Decode(certPEM) + if p == nil { + return nil, errors.New("no valid PEM data found") + } + cert, err := x509.ParseCertificate(p.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate: %v", err) + } + return map[string]string{ + "SHA-1": hashutil.SHA1Prefix(cert.Raw), + "SHA-256": hashutil.SHA256Prefix(cert.Raw), + }, nil +} + +// GenSelfTLSFiles generates a self-signed certificate and key for hostname, +// and writes them to the given paths. If it succeeds it also returns +// the SHA256 prefix of the new cert. +func GenSelfTLSFiles(hostname, certPath, keyPath string) (fingerprint string, err error) { + cert, key, err := GenSelfTLS(hostname) + if err != nil { + return "", err + } + sig, err := CertFingerprint(cert) + if err != nil { + return "", fmt.Errorf("could not get SHA-256 fingerprint of certificate: %v", err) + } + if err := wkfs.WriteFile(certPath, cert, 0666); err != nil { + return "", fmt.Errorf("failed to write self-signed TLS cert: %v", err) + } + if err := wkfs.WriteFile(keyPath, key, 0600); err != nil { + return "", fmt.Errorf("failed to write self-signed TLS key: %v", err) + } + return sig, nil +} + +// InstallCerts adds Mozilla's Certificate Authority root set to +// http.DefaultTransport's configuration if the current operating +// system's root CAs are not available. (for instance, if running inside +// a Docker container without a filesystem) +func InstallCerts() { + if !SystemCARootsAvailable() { + if tr, ok := http.DefaultTransport.(*http.Transport); ok { + tlsConf := tr.TLSClientConfig + if tlsConf == nil { + tlsConf = &tls.Config{} + tr.TLSClientConfig = tlsConf + } + if tlsConf.RootCAs == nil { + tlsConf.RootCAs = RootCAPool() + } + } + } +} + +// RootCAPool returns the Mozilla Root Certificate Authority pool, +// as statically compiled into the binary. +func RootCAPool() *x509.CertPool { + poolOnce.Do(buildPool) + return pool +} + +func buildPool() { + pool = x509.NewCertPool() + pool.AppendCertsFromPEM([]byte(certData)) +} + +// SystemCARootsAvailable reports whether the operating system's root +// CA files are available. +func SystemCARootsAvailable() bool { + sysRootsOnce.Do(checkSystemRoots) + return sysRootsGood +} + +func checkSystemRoots() { + if runtime.GOOS == "windows" { + // Windows is special somehow, and won't be running as + // a static Docker binary anywhere (which is what this + // whole file is about), so just say it's fine and we + // won't use the static CA set below. + sysRootsGood = true + return + } + + // Verify a dummy cert just to test whether the system roots + // are available. This depends on knowing that the x509 + // package returns this type of error first, before checking + // the certificate's validity. + _, err := new(x509.Certificate).Verify(x509.VerifyOptions{}) + _, isSysRootError := err.(x509.SystemRootsError) + sysRootsGood = !isSysRootError +} + +func init() { + legal.RegisterLicense(` +For Mozilla Root CA set, +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +`) +} + +// Generated from https://github.com/agl/extract-nss-root-certs +var certData = ` +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Issuer: CN=GTE CyberTrust Global Root O=GTE Corporation OU=GTE CyberTrust Solutions, Inc. +# Subject: CN=GTE CyberTrust Global Root O=GTE Corporation OU=GTE CyberTrust Solutions, Inc. +# Label: "GTE CyberTrust Global Root" +# Serial: 421 +# MD5 Fingerprint: ca:3d:d3:68:f1:03:5c:d0:32:fa:b8:2b:59:e8:5a:db +# SHA1 Fingerprint: 97:81:79:50:d8:1c:96:70:cc:34:d8:09:cf:79:44:31:36:7e:f4:74 +# SHA256 Fingerprint: a5:31:25:18:8d:21:10:aa:96:4b:02:c7:b7:c6:da:32:03:17:08:94:e5:fb:71:ff:fb:66:67:d5:e6:81:0a:36 +-----BEGIN CERTIFICATE----- +MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYD +VQQKEw9HVEUgQ29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNv +bHV0aW9ucywgSW5jLjEjMCEGA1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJv +b3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEzMjM1OTAwWjB1MQswCQYDVQQGEwJV +UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU +cnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds +b2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrH +iM3dFw4usJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTS +r41tiGeA5u2ylc9yMcqlHHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X4 +04Wqk2kmhXBIgD8SFcd5tB8FLztimQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAG3r +GwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMWM4ETCJ57NE7fQMh017l9 +3PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OFNMQkpw0P +lZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/ +-----END CERTIFICATE----- + +# Issuer: CN=Thawte Server CA O=Thawte Consulting cc OU=Certification Services Division +# Subject: CN=Thawte Server CA O=Thawte Consulting cc OU=Certification Services Division +# Label: "Thawte Server CA" +# Serial: 1 +# MD5 Fingerprint: c5:70:c4:a2:ed:53:78:0c:c8:10:53:81:64:cb:d0:1d +# SHA1 Fingerprint: 23:e5:94:94:51:95:f2:41:48:03:b4:d5:64:d2:a3:a3:f5:d8:8b:8c +# SHA256 Fingerprint: b4:41:0b:73:e2:e6:ea:ca:47:fb:c4:2f:8f:a4:01:8a:f4:38:1d:c5:4c:fa:a8:44:50:46:1e:ed:09:45:4d:e9 +-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm +MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx +MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT +DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3 +dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl +cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3 +DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91 +yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX +L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj +EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG +7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e +QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ +qdq5snUb9kLy78fyGPmJvKP/iiMucEc= +-----END CERTIFICATE----- + +# Issuer: CN=Thawte Premium Server CA O=Thawte Consulting cc OU=Certification Services Division +# Subject: CN=Thawte Premium Server CA O=Thawte Consulting cc OU=Certification Services Division +# Label: "Thawte Premium Server CA" +# Serial: 1 +# MD5 Fingerprint: 06:9f:69:79:16:66:90:02:1b:8c:8c:a2:c3:07:6f:3a +# SHA1 Fingerprint: 62:7f:8d:78:27:65:63:99:d2:7d:7f:90:44:c9:fe:b3:f3:3e:fa:9a +# SHA256 Fingerprint: ab:70:36:36:5c:71:54:aa:29:c2:c2:9f:5d:41:91:16:3b:16:2a:22:25:01:13:57:d5:6d:07:ff:a7:bc:1f:72 +-----BEGIN CERTIFICATE----- +MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy +dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t +MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB +MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG +A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl +cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE +VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ +ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR +uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI +hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM +pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== +-----END CERTIFICATE----- + +# Issuer: O=Equifax OU=Equifax Secure Certificate Authority +# Subject: O=Equifax OU=Equifax Secure Certificate Authority +# Label: "Equifax Secure CA" +# Serial: 903804111 +# MD5 Fingerprint: 67:cb:9d:c0:13:24:8a:82:9b:b2:17:1e:d1:1b:ec:d4 +# SHA1 Fingerprint: d2:32:09:ad:23:d3:14:23:21:74:e4:0d:7f:9d:62:13:97:86:63:3a +# SHA256 Fingerprint: 08:29:7a:40:47:db:a2:36:80:c7:31:db:6e:31:76:53:ca:78:48:e1:be:bd:3a:0b:01:79:a7:07:f9:2c:f1:78 +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- + +# Issuer: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority - G2/(c) 1998 VeriSign, Inc. - For authorized use only/VeriSign Trust Network +# Subject: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority - G2/(c) 1998 VeriSign, Inc. - For authorized use only/VeriSign Trust Network +# Label: "Verisign Class 3 Public Primary Certification Authority - G2" +# Serial: 167285380242319648451154478808036881606 +# MD5 Fingerprint: a2:33:9b:4c:74:78:73:d4:6c:e7:c1:f3:8d:cb:5c:e9 +# SHA1 Fingerprint: 85:37:1c:a6:e5:50:14:3d:ce:28:03:47:1b:de:3a:09:e8:f8:77:0f +# SHA256 Fingerprint: 83:ce:3c:12:29:68:8a:59:3d:48:5f:81:97:3c:0f:91:95:43:1e:da:37:cc:5e:36:43:0e:79:c7:a8:88:63:8b +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 +pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 +13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk +U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i +F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY +oJ2daZH9 +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA +# Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA +# Label: "GlobalSign Root CA" +# Serial: 4835703278459707669005204 +# MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a +# SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c +# SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99 +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 +# Label: "GlobalSign Root CA - R2" +# Serial: 4835703278459682885658125 +# MD5 Fingerprint: 94:14:77:7e:3e:5e:fd:8f:30:bd:41:b0:cf:e7:d0:30 +# SHA1 Fingerprint: 75:e0:ab:b6:13:85:12:27:1c:04:f8:5f:dd:de:38:e4:b7:24:2e:fe +# SHA256 Fingerprint: ca:42:dd:41:74:5f:d0:b8:1e:b9:02:36:2c:f9:d8:bf:71:9d:a1:bd:1b:1e:fc:94:6f:5b:4c:99:f4:2c:1b:9e +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Label: "Verisign Class 3 Public Primary Certification Authority - G3" +# Serial: 206684696279472310254277870180966723415 +# MD5 Fingerprint: cd:68:b6:a7:c7:c4:ce:75:e0:1d:4f:57:44:61:92:09 +# SHA1 Fingerprint: 13:2d:0d:45:53:4b:69:97:cd:b2:d5:c3:39:e2:55:76:60:9b:5c:c6 +# SHA256 Fingerprint: eb:04:cf:5e:b1:f3:9a:fa:76:2f:2b:b1:20:f2:96:cb:a5:20:c1:b9:7d:b1:58:95:65:b8:1c:b9:a1:7b:72:44 +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 4 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 4 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Label: "Verisign Class 4 Public Primary Certification Authority - G3" +# Serial: 314531972711909413743075096039378935511 +# MD5 Fingerprint: db:c8:f2:27:2e:b1:ea:6a:29:23:5d:fe:56:3e:33:df +# SHA1 Fingerprint: c8:ec:8c:87:92:69:cb:4b:ab:39:e9:8d:7e:57:67:f3:14:95:73:9d +# SHA256 Fingerprint: e3:89:36:0d:0f:db:ae:b3:d2:50:58:4b:47:30:31:4e:22:2f:39:c1:56:a0:20:14:4e:8d:96:05:61:79:15:06 +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1 +GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ ++mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd +U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm +NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY +ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ +ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1 +CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq +g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm +fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c +2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/ +bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== +-----END CERTIFICATE----- + +# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Label: "Entrust.net Premium 2048 Secure Server CA" +# Serial: 946069240 +# MD5 Fingerprint: ee:29:31:bc:32:7e:9a:e6:e8:b5:f7:51:b4:34:71:90 +# SHA1 Fingerprint: 50:30:06:09:1d:97:d4:f5:ae:39:f7:cb:e7:92:7d:7d:65:2d:34:31 +# SHA256 Fingerprint: 6d:c4:71:72:e0:1c:bc:b0:bf:62:58:0d:89:5f:e2:b8:ac:9a:d4:f8:73:80:1e:0c:10:b9:c8:37:d2:1e:b1:77 +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 +MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub +j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo +U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b +u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ +bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er +fF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +# Issuer: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust +# Subject: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust +# Label: "Baltimore CyberTrust Root" +# Serial: 33554617 +# MD5 Fingerprint: ac:b6:94:a5:9c:17:e0:d7:91:52:9b:b1:97:06:a6:e4 +# SHA1 Fingerprint: d4:de:20:d0:5e:66:fc:53:fe:1a:50:88:2c:78:db:28:52:ca:e4:74 +# SHA256 Fingerprint: 16:af:57:a9:f6:76:b0:ab:12:60:95:aa:5e:ba:de:f2:2a:b3:11:19:d6:44:ac:95:cd:4b:93:db:f3:f2:6a:eb +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +# Issuer: CN=Equifax Secure Global eBusiness CA-1 O=Equifax Secure Inc. +# Subject: CN=Equifax Secure Global eBusiness CA-1 O=Equifax Secure Inc. +# Label: "Equifax Secure Global eBusiness CA" +# Serial: 1 +# MD5 Fingerprint: 8f:5d:77:06:27:c4:98:3c:5b:93:78:e7:d7:7d:9b:cc +# SHA1 Fingerprint: 7e:78:4a:10:1c:82:65:cc:2d:e1:f1:6d:47:b4:40:ca:d9:0a:19:45 +# SHA256 Fingerprint: 5f:0b:62:ea:b5:e3:53:ea:65:21:65:16:58:fb:b6:53:59:f4:43:28:0a:4a:fb:d1:04:d7:7d:10:f9:f0:4c:07 +-----BEGIN CERTIFICATE----- +MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT +ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw +MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj +dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l +c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC +UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc +58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/ +o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr +aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA +A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA +Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv +8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV +-----END CERTIFICATE----- + +# Issuer: CN=Equifax Secure eBusiness CA-1 O=Equifax Secure Inc. +# Subject: CN=Equifax Secure eBusiness CA-1 O=Equifax Secure Inc. +# Label: "Equifax Secure eBusiness CA 1" +# Serial: 4 +# MD5 Fingerprint: 64:9c:ef:2e:44:fc:c6:8f:52:07:d0:51:73:8f:cb:3d +# SHA1 Fingerprint: da:40:18:8b:91:89:a3:ed:ee:ae:da:97:fe:2f:9d:f5:b7:d1:8a:41 +# SHA256 Fingerprint: cf:56:ff:46:a4:a1:86:10:9d:d9:65:84:b5:ee:b5:8a:51:0c:42:75:b0:e5:f9:4f:40:bb:ae:86:5e:19:f6:73 +-----BEGIN CERTIFICATE----- +MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT +ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw +MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j +LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo +RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu +WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw +Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK +eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM +zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+ +WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN +/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ== +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Class 1 CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Class 1 CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Low-Value Services Root" +# Serial: 1 +# MD5 Fingerprint: 1e:42:95:02:33:92:6b:b9:5f:c0:7f:da:d6:b2:4b:fc +# SHA1 Fingerprint: cc:ab:0e:a0:4c:23:01:d6:69:7b:dd:37:9f:cd:12:eb:24:e3:94:9d +# SHA256 Fingerprint: 8c:72:09:27:9a:c0:4e:27:5e:16:d0:7f:d3:b7:75:e8:01:54:b5:96:80:46:e3:1f:52:dd:25:76:63:24:e9:a7 +-----BEGIN CERTIFICATE----- +MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMw +MTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD +VQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ul +CDtbKRY654eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6n +tGO0/7Gcrjyvd7ZWxbWroulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyl +dI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1Zmne3yzxbrww2ywkEtvrNTVokMsAsJch +PXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJuiGMx1I4S+6+JNM3GOGvDC ++Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8wHQYDVR0O +BBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBl +MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFk +ZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENB +IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxtZBsfzQ3duQH6lmM0MkhHma6X +7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0PhiVYrqW9yTkkz +43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY +eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJl +pz/+0WatC7xrmYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOA +WiFeIc9TVPC6b4nbqKqVz4vjccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network +# Subject: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network +# Label: "AddTrust External Root" +# Serial: 1 +# MD5 Fingerprint: 1d:35:54:04:85:78:b0:3f:42:42:4d:bf:20:73:0a:3f +# SHA1 Fingerprint: 02:fa:f3:e2:91:43:54:68:60:78:57:69:4d:f5:e4:5b:68:85:18:68 +# SHA256 Fingerprint: 68:7f:a4:51:38:22:78:ff:f0:c8:b1:1f:8d:43:d5:76:67:1c:6e:b2:bc:ea:b4:13:fb:83:d9:65:d0:6d:2f:f2 +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Public CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Public CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Public Services Root" +# Serial: 1 +# MD5 Fingerprint: c1:62:3e:23:c5:82:73:9c:03:59:4b:2b:e9:77:49:7f +# SHA1 Fingerprint: 2a:b6:28:48:5e:78:fb:f3:ad:9e:79:10:dd:6b:df:99:72:2c:96:e5 +# SHA256 Fingerprint: 07:91:ca:07:49:b2:07:82:aa:d3:c7:d7:bd:0c:df:c9:48:58:35:84:3e:b2:d7:99:60:09:ce:43:ab:6c:69:27 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSAwHgYDVQQDExdBZGRUcnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAx +MDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtB +ZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV +BAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV +6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX +GCwwfQ56HmIexkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnP +dzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSGAa2Il+tmzV7R/9x98oTaunet3IAIx6eH +1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAwHM+A+WD+eeSI8t0A65RF +62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0GA1UdDgQW +BBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDEL +MAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRU +cnVzdCBUVFAgTmV0d29yazEgMB4GA1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJv +b3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4JNojVhaTdt02KLmuG7jD8WS6 +IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL+YPoRNWyQSW/ +iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao +GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh +4SINhwBk/ox9Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQm +XiLsks3/QppEIW1cxeMiHV9HEufOX1362KqxMy3ZdvJOOjMMK7MtkAY= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Qualified CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Qualified CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Qualified Certificates Root" +# Serial: 1 +# MD5 Fingerprint: 27:ec:39:47:cd:da:5a:af:e2:9a:01:65:21:a9:4c:bb +# SHA1 Fingerprint: 4d:23:78:ec:91:95:39:b5:00:7f:75:8f:03:3b:21:1e:c5:4d:8b:cf +# SHA256 Fingerprint: 80:95:21:08:05:db:4b:bc:35:5e:44:28:d8:fd:6e:c2:cd:e3:ab:5f:b9:7a:99:42:98:8e:b8:f4:dc:d0:60:16 +-----BEGIN CERTIFICATE----- +MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSMwIQYDVQQDExpBZGRUcnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1 +MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcxCzAJBgNVBAYTAlNFMRQwEgYDVQQK +EwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIzAh +BgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwq +xBb/4Oxx64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G +87B4pfYOQnrjfxvM0PC3KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i +2O+tCBGaKZnhqkRFmhJePp1tUvznoD1oL/BLcHwTOK28FSXx1s6rosAx1i+f4P8U +WfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GRwVY18BTcZTYJbqukB8c1 +0cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HUMIHRMB0G +A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6Fr +pGkwZzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQL +ExRBZGRUcnVzdCBUVFAgTmV0d29yazEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlm +aWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBABmrder4i2VhlRO6aQTv +hsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxGGuoYQ992zPlm +hpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X +dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3 +P6CxB9bpT9zeRXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9Y +iQBCYz95OdBEsIJuQRno3eDBiFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5no +xqE= +-----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Label: "Entrust Root Certification Authority" +# Serial: 1164660820 +# MD5 Fingerprint: d6:a5:c3:ed:5d:dd:3e:00:c1:3d:87:92:1f:1d:3f:e4 +# SHA1 Fingerprint: b3:1e:b1:b7:40:e3:6c:84:02:da:dc:37:d4:4d:f5:d4:67:49:52:f9 +# SHA256 Fingerprint: 73:c1:76:43:4f:1b:c6:d5:ad:f4:5b:0e:76:e7:27:28:7c:8d:e5:76:16:c1:e6:e6:14:1a:2b:2c:bc:7d:8e:4c +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +# Issuer: O=RSA Security Inc OU=RSA Security 2048 V3 +# Subject: O=RSA Security Inc OU=RSA Security 2048 V3 +# Label: "RSA Security 2048 v3" +# Serial: 13297492616345471454730593562152402946 +# MD5 Fingerprint: 77:0d:19:b1:21:fd:00:42:9c:3e:0c:a5:dd:0b:02:8e +# SHA1 Fingerprint: 25:01:90:19:cf:fb:d9:99:1c:b7:68:25:74:8d:94:5f:30:93:95:42 +# SHA256 Fingerprint: af:8b:67:62:a1:e5:28:22:81:61:a9:5d:5c:55:9e:e2:66:27:8f:75:d7:9e:83:01:89:a5:03:50:6a:bd:6b:4c +-----BEGIN CERTIFICATE----- +MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6 +MRkwFwYDVQQKExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJp +dHkgMjA0OCBWMzAeFw0wMTAyMjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAX +BgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAbBgNVBAsTFFJTQSBTZWN1cml0eSAy +MDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt49VcdKA3Xtp +eafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7Jylg +/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGl +wSMiuLgbWhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnh +AMFRD0xS+ARaqn1y07iHKrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2 +PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpu +AWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4EFgQUB8NR +MKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYc +HnmYv/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/ +Zb5gEydxiKRz44Rj0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+ +f00/FGj1EVDVwfSQpQgdMWD/YIwjVAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVO +rSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395nzIlQnQFgCi/vcEkllgVsRch +6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kApKnXwiJPZ9d3 +7CAFYd4= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Global CA O=GeoTrust Inc. +# Subject: CN=GeoTrust Global CA O=GeoTrust Inc. +# Label: "GeoTrust Global CA" +# Serial: 144470 +# MD5 Fingerprint: f7:75:ab:29:fb:51:4e:b7:77:5e:ff:05:3c:99:8e:f5 +# SHA1 Fingerprint: de:28:f4:a4:ff:e5:b9:2f:a3:c5:03:d1:a3:49:a7:f9:96:2a:82:12 +# SHA256 Fingerprint: ff:85:6a:2d:25:1d:cd:88:d3:66:56:f4:50:12:67:98:cf:ab:aa:de:40:79:9c:72:2d:e4:d2:b5:db:36:a7:3a +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Global CA 2 O=GeoTrust Inc. +# Subject: CN=GeoTrust Global CA 2 O=GeoTrust Inc. +# Label: "GeoTrust Global CA 2" +# Serial: 1 +# MD5 Fingerprint: 0e:40:a7:6c:de:03:5d:8f:d1:0f:e4:d1:8d:f9:6c:a9 +# SHA1 Fingerprint: a9:e9:78:08:14:37:58:88:f2:05:19:b0:6d:2b:0d:2b:60:16:90:7d +# SHA256 Fingerprint: ca:2d:82:a0:86:77:07:2f:8a:b6:76:4f:f0:35:67:6c:fe:3e:5e:32:5e:01:21:72:df:3f:92:09:6d:b7:9b:85 +-----BEGIN CERTIFICATE----- +MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFs +IENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3Qg +R2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvPE1A +PRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/NTL8 +Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hL +TytCOb1kLUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL +5mkWRxHCJ1kDs6ZgwiFAVvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7 +S4wMcoKK+xfNAGw6EzywhIdLFnopsk/bHdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe +2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNHK266ZUap +EBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6td +EPx7srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv +/NgdRN3ggX+d6YvhZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywN +A0ZF66D0f0hExghAzN4bcLUprbqLOzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0 +abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkCx1YAzUm5s2x7UwQa4qjJqhIF +I8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqFH4z1Ir+rzoPz +4iIprn2DQKi6bA== +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Universal CA O=GeoTrust Inc. +# Subject: CN=GeoTrust Universal CA O=GeoTrust Inc. +# Label: "GeoTrust Universal CA" +# Serial: 1 +# MD5 Fingerprint: 92:65:58:8b:a2:1a:31:72:73:68:5c:b4:a5:7a:07:48 +# SHA1 Fingerprint: e6:21:f3:35:43:79:05:9a:4b:68:30:9d:8a:2f:74:22:15:87:ec:79 +# SHA256 Fingerprint: a0:45:9b:9f:63:b2:25:59:f5:fa:5d:4c:6d:b3:f9:f7:2f:f1:93:42:03:35:78:f0:73:bf:1d:1b:46:cb:b9:12 +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy +c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 +IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV +VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 +cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT +QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh +F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v +c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w +mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd +VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX +teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ +f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe +Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ +nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY +MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX +IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn +ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z +uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN +Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja +QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW +koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 +ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt +DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm +bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. +# Subject: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. +# Label: "GeoTrust Universal CA 2" +# Serial: 1 +# MD5 Fingerprint: 34:fc:b8:d0:36:db:9e:14:b3:c2:f2:db:8f:e4:94:c7 +# SHA1 Fingerprint: 37:9a:19:7b:41:85:45:35:0c:a6:03:69:f3:3c:2e:af:47:4f:20:79 +# SHA256 Fingerprint: a0:23:4f:3b:c8:52:7c:a5:62:8e:ec:81:ad:5d:69:89:5d:a5:68:0d:c9:1d:1c:b8:47:7f:33:f8:78:b9:5b:0b +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy +c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD +VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 +c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 +WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG +FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq +XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL +se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb +KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd +IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 +y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt +hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc +QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 +Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV +HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ +KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ +L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr +Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo +ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY +T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz +GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m +1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV +OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH +6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX +QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +# Issuer: CN=America Online Root Certification Authority 1 O=America Online Inc. +# Subject: CN=America Online Root Certification Authority 1 O=America Online Inc. +# Label: "America Online Root Certification Authority 1" +# Serial: 1 +# MD5 Fingerprint: 14:f1:08:ad:9d:fa:64:e2:89:e7:1c:cf:a8:ad:7d:5e +# SHA1 Fingerprint: 39:21:c1:15:c1:5d:0e:ca:5c:cb:5b:c4:f0:7d:21:d8:05:0b:56:6a +# SHA256 Fingerprint: 77:40:73:12:c6:3a:15:3d:5b:c0:0b:4e:51:75:9c:df:da:c2:37:dc:2a:33:b6:79:46:e9:8e:9b:fa:68:0a:e3 +-----BEGIN CERTIFICATE----- +MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP +bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2 +MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft +ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lk +hsmj76CGv2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym +1BW32J/X3HGrfpq/m44zDyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsW +OqMFf6Dch9Wc/HKpoH145LcxVR5lu9RhsCFg7RAycsWSJR74kEoYeEfffjA3PlAb +2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP8c9GsEsPPt2IYriMqQko +O3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAU +AK3Zo/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +BQUAA4IBAQB8itEfGDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkF +Zu90821fnZmv9ov761KyBZiibyrFVL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAb +LjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft3OJvx8Fi8eNy1gTIdGcL+oir +oQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43gKd8hdIaC2y+C +MMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds +sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7 +-----END CERTIFICATE----- + +# Issuer: CN=America Online Root Certification Authority 2 O=America Online Inc. +# Subject: CN=America Online Root Certification Authority 2 O=America Online Inc. +# Label: "America Online Root Certification Authority 2" +# Serial: 1 +# MD5 Fingerprint: d6:ed:3c:ca:e2:66:0f:af:10:43:0d:77:9b:04:09:bf +# SHA1 Fingerprint: 85:b5:ff:67:9b:0c:79:96:1f:c8:6e:44:22:00:46:13:db:17:92:84 +# SHA256 Fingerprint: 7d:3b:46:5a:60:14:e5:26:c0:af:fc:ee:21:27:d2:31:17:27:ad:81:1c:26:84:2d:00:6a:f3:73:06:cc:80:bd +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP +bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2 +MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft +ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC +206B89enfHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFci +KtZHgVdEglZTvYYUAQv8f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2 +JxhP7JsowtS013wMPgwr38oE18aO6lhOqKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9 +BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JNRvCAOVIyD+OEsnpD8l7e +Xz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0gBe4lL8B +PeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67 +Xnfn6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEq +Z8A9W6Wa6897GqidFEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZ +o2C7HK2JNDJiuEMhBnIMoVxtRsX6Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3 ++L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnjB453cMor9H124HhnAgMBAAGj +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3OpaaEg5+31IqEj +FNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmn +xPBUlgtk87FYT15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2 +LHo1YGwRgJfMqZJS5ivmae2p+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzccc +obGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXgJXUjhx5c3LqdsKyzadsXg8n33gy8 +CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//ZoyzH1kUQ7rVyZ2OuMe +IjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgOZtMA +DjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2F +AjgQ5ANh1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUX +Om/9riW99XJZZLF0KjhfGEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPb +AZO1XB4Y3WRayhgoPmMEEf0cjQAPuDffZ4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQl +Zvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuPcX/9XhmgD0uRuMRUvAaw +RY8mkaKO/qk= +-----END CERTIFICATE----- + +# Issuer: CN=Visa eCommerce Root O=VISA OU=Visa International Service Association +# Subject: CN=Visa eCommerce Root O=VISA OU=Visa International Service Association +# Label: "Visa eCommerce Root" +# Serial: 25952180776285836048024890241505565794 +# MD5 Fingerprint: fc:11:b8:d8:08:93:30:00:6d:23:f9:7e:eb:52:1e:02 +# SHA1 Fingerprint: 70:17:9b:86:8c:00:a4:fa:60:91:52:22:3f:9f:3e:32:bd:e0:05:62 +# SHA256 Fingerprint: 69:fa:c9:bd:55:fb:0a:c7:8d:53:bb:ee:5c:f1:d5:97:98:9f:d0:aa:ab:20:a2:51:51:bd:f1:73:3e:e7:d1:22 +-----BEGIN CERTIFICATE----- +MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr +MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl +cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv +bW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2WhcNMjIwNjI0MDAxNjEyWjBrMQsw +CQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5h +dGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1l +cmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h +2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E +lpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV +ZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq +299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI/k4+oKsGGelT84ATB+0t +vz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL +dXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUF +AAOCAQEAX/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcR +zCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3 +LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd +7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQa7FkKMcPcw +++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt +398znM/jra6O1I7mT1GvFpLgXPYHDw== +-----END CERTIFICATE----- + +# Issuer: CN=Certum CA O=Unizeto Sp. z o.o. +# Subject: CN=Certum CA O=Unizeto Sp. z o.o. +# Label: "Certum Root CA" +# Serial: 65568 +# MD5 Fingerprint: 2c:8f:9f:66:1d:18:90:b1:47:26:9d:8e:86:82:8c:a9 +# SHA1 Fingerprint: 62:52:dc:40:f7:11:43:a2:2f:de:9e:f7:34:8e:06:42:51:b1:81:18 +# SHA256 Fingerprint: d8:e0:fe:bc:1d:b2:e3:8d:00:94:0f:37:d2:7d:41:34:4d:99:3e:73:4b:99:d5:65:6d:97:78:d4:d8:14:36:24 +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E +jG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo +ePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI +ULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu +Ob7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg +AKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7 +HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA +uI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa +TOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg +xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q +CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x +O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs +6GAqm4VKQPNriiTsBhYscw== +-----END CERTIFICATE----- + +# Issuer: CN=AAA Certificate Services O=Comodo CA Limited +# Subject: CN=AAA Certificate Services O=Comodo CA Limited +# Label: "Comodo AAA Services root" +# Serial: 1 +# MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0 +# SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49 +# SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4 +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +# Issuer: CN=Secure Certificate Services O=Comodo CA Limited +# Subject: CN=Secure Certificate Services O=Comodo CA Limited +# Label: "Comodo Secure Services root" +# Serial: 1 +# MD5 Fingerprint: d3:d9:bd:ae:9f:ac:67:24:b3:c8:1b:52:e1:b9:a9:bd +# SHA1 Fingerprint: 4a:65:d5:f4:1d:ef:39:b8:b8:90:4a:4a:d3:64:81:33:cf:c7:a1:d1 +# SHA256 Fingerprint: bd:81:ce:3b:4f:65:91:d1:1a:67:b5:fc:7a:47:fd:ef:25:52:1b:f9:aa:4e:18:b9:e3:df:2e:34:a7:80:3b:e8 +-----BEGIN CERTIFICATE----- +MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp +ZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow +fjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV +BAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM +cm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S +HpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996 +CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk +3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz +6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV +HQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud +EwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv +Y2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw +Oi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww +DQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0 +5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj +Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI +gKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ +aD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl +izeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk= +-----END CERTIFICATE----- + +# Issuer: CN=Trusted Certificate Services O=Comodo CA Limited +# Subject: CN=Trusted Certificate Services O=Comodo CA Limited +# Label: "Comodo Trusted Services root" +# Serial: 1 +# MD5 Fingerprint: 91:1b:3f:6e:cd:9e:ab:ee:07:fe:1f:71:d2:b3:61:27 +# SHA1 Fingerprint: e1:9f:e3:0e:8b:84:60:9e:80:9b:17:0d:72:a8:c5:ba:6e:14:09:bd +# SHA256 Fingerprint: 3f:06:e5:56:81:d4:96:f5:be:16:9e:b5:38:9f:9f:2b:8f:f6:1e:17:08:df:68:81:72:48:49:cd:5d:27:cb:69 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0 +aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla +MH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO +BgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD +VQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW +fnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt +TGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL +fhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW +1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7 +kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G +A1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v +ZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo +dHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu +Y3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/ +HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32 +pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS +jBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+ +xqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn +dBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root Certification Authority O=QuoVadis Limited OU=Root Certification Authority +# Subject: CN=QuoVadis Root Certification Authority O=QuoVadis Limited OU=Root Certification Authority +# Label: "QuoVadis Root CA" +# Serial: 985026699 +# MD5 Fingerprint: 27:de:36:fe:72:b7:00:03:00:9d:f4:f0:1e:6c:04:24 +# SHA1 Fingerprint: de:3f:40:bd:50:93:d3:9b:6c:60:f6:da:bc:07:62:01:00:89:76:c9 +# SHA256 Fingerprint: a4:5e:de:3b:bb:f0:9c:8a:e1:5c:72:ef:c0:72:68:d6:93:a2:1c:99:6f:d5:1e:67:ca:07:94:60:fd:6d:88:73 +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz +MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw +IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR +dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp +li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D +rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ +WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug +F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU +xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC +Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv +dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw +ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl +IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh +c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy +ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI +KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T +KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq +y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p +dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD +VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk +fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8 +7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R +cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y +mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW +xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK +SnQ2+Q== +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2" +# Serial: 1289 +# MD5 Fingerprint: 5e:39:7b:dd:f8:ba:ec:82:e9:ac:62:ba:0c:54:00:2b +# SHA1 Fingerprint: ca:3a:fb:cf:12:40:36:4b:44:b2:16:20:88:80:48:39:19:93:7c:f7 +# SHA256 Fingerprint: 85:a0:dd:7d:d7:20:ad:b7:ff:05:f8:3d:54:2b:20:9d:c7:ff:45:28:f7:d6:77:b1:83:89:fe:a5:e5:c4:9e:86 +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3" +# Serial: 1478 +# MD5 Fingerprint: 31:85:3c:62:94:97:63:b9:aa:fd:89:4e:af:6f:e0:cf +# SHA1 Fingerprint: 1f:49:14:f7:d8:74:95:1d:dd:ae:02:c0:be:fd:3a:2d:82:75:51:85 +# SHA256 Fingerprint: 18:f1:fc:7f:20:5d:f8:ad:dd:eb:7f:e0:07:dd:57:e3:af:37:5a:9c:4d:8d:73:54:6b:f4:f1:fe:d1:e1:8d:35 +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust.net OU=Security Communication RootCA1 +# Subject: O=SECOM Trust.net OU=Security Communication RootCA1 +# Label: "Security Communication Root CA" +# Serial: 0 +# MD5 Fingerprint: f1:bc:63:6a:54:e0:b5:27:f5:cd:e7:1a:e3:4d:6e:4a +# SHA1 Fingerprint: 36:b1:2b:49:f9:81:9e:d7:4c:9e:bc:38:0f:c6:56:8f:5d:ac:b2:f7 +# SHA256 Fingerprint: e7:5e:72:ed:9f:56:0e:ec:6e:b4:80:00:73:a4:3f:c3:ad:19:19:5a:39:22:82:01:78:95:97:4a:99:02:6b:6c +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY +MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t +dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 +WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD +VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 +9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ +DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 +Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N +QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ +xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G +A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG +kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr +Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 +Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU +JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot +RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== +-----END CERTIFICATE----- + +# Issuer: CN=Sonera Class2 CA O=Sonera +# Subject: CN=Sonera Class2 CA O=Sonera +# Label: "Sonera Class 2 Root CA" +# Serial: 29 +# MD5 Fingerprint: a3:ec:75:0f:2e:88:df:fa:48:01:4e:0b:5c:48:6f:fb +# SHA1 Fingerprint: 37:f7:6d:e6:07:7c:90:c5:b1:3e:93:1a:b7:41:10:b4:f2:e4:9a:27 +# SHA256 Fingerprint: 79:08:b4:03:14:c1:38:10:0b:51:8d:07:35:80:7f:fb:fc:f8:51:8a:00:95:33:71:05:ba:38:6b:15:3d:d9:27 +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP +MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx +MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV +BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o +Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt +5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s +3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej +vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu +8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw +DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG +MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil +zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/ +3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD +FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6 +Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2 +ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M +-----END CERTIFICATE----- + +# Issuer: CN=Staat der Nederlanden Root CA O=Staat der Nederlanden +# Subject: CN=Staat der Nederlanden Root CA O=Staat der Nederlanden +# Label: "Staat der Nederlanden Root CA" +# Serial: 10000010 +# MD5 Fingerprint: 60:84:7c:5a:ce:db:0c:d4:cb:a7:e9:fe:02:c6:a9:c0 +# SHA1 Fingerprint: 10:1d:fa:3f:d5:0b:cb:bb:9b:b5:60:0c:19:55:a4:1a:f4:73:3a:04 +# SHA256 Fingerprint: d4:1d:82:9e:8c:16:59:82:2a:f9:3f:ce:62:bf:fc:de:26:4f:c8:4e:8b:95:0c:5f:f2:75:d0:52:35:46:95:a3 +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJO +TDEeMBwGA1UEChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEy +MTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4wHAYDVQQKExVTdGFhdCBkZXIgTmVk +ZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxhbmRlbiBSb290IENB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFtvszn +ExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw71 +9tV2U02PjLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MO +hXeiD+EwR+4A5zN9RGcaC1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+U +tFE5A3+y3qcym7RHjm+0Sq7lr7HcsBthvJly3uSJt3omXdozSVtSnA71iq3DuD3o +BmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn622r+I/q85Ej0ZytqERAh +SQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRVHSAAMDww +OgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMv +cm9vdC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA +7Jbg0zTBLL9s+DANBgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k +/rvuFbQvBgwp8qiSpGEN/KtcCFtREytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzm +eafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbwMVcoEoJz6TMvplW0C5GUR5z6 +u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3ynGQI0DvDKcWy +7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR +iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw== +-----END CERTIFICATE----- + +# Issuer: CN=UTN - DATACorp SGC O=The USERTRUST Network OU=http://www.usertrust.com +# Subject: CN=UTN - DATACorp SGC O=The USERTRUST Network OU=http://www.usertrust.com +# Label: "UTN DATACorp SGC Root CA" +# Serial: 91374294542884689855167577680241077609 +# MD5 Fingerprint: b3:a5:3e:77:21:6d:ac:4a:c0:c9:fb:d5:41:3d:ca:06 +# SHA1 Fingerprint: 58:11:9f:0e:12:82:87:ea:50:fd:d9:87:45:6f:4f:78:dc:fa:d6:d4 +# SHA256 Fingerprint: 85:fb:2f:91:dd:12:27:5a:01:45:b6:36:53:4f:84:02:4a:d6:8b:69:b8:ee:88:68:4f:f7:11:37:58:05:b3:48 +-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCB +kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw +IFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBaMIGTMQswCQYDVQQG +EwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYD +VQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cu +dXNlcnRydXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6 +E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ysraP6LnD43m77VkIVni5c7yPeIbkFdicZ +D0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlowHDyUwDAXlCCpVZvNvlK +4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA9P4yPykq +lXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulW +bfXv33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQAB +o4GrMIGoMAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRT +MtGzz3/64PGgXYVOktKeRR20TzA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3Js +LnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dDLmNybDAqBgNVHSUEIzAhBggr +BgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3DQEBBQUAA4IB +AQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft +Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyj +j98C5OBxOvG0I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVH +KWss5nbZqSl9Mt3JNjy9rjXxEZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv +2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwPDPafepE39peC4N1xaf92P2BNPM/3 +mfnGV/TJVTl4uix5yaaIK/QI +-----END CERTIFICATE----- + +# Issuer: CN=UTN-USERFirst-Hardware O=The USERTRUST Network OU=http://www.usertrust.com +# Subject: CN=UTN-USERFirst-Hardware O=The USERTRUST Network OU=http://www.usertrust.com +# Label: "UTN USERFirst Hardware Root CA" +# Serial: 91374294542884704022267039221184531197 +# MD5 Fingerprint: 4c:56:41:e5:0d:bb:2b:e8:ca:a3:ed:18:08:ad:43:39 +# SHA1 Fingerprint: 04:83:ed:33:99:ac:36:08:05:87:22:ed:bc:5e:46:00:e3:be:f9:d7 +# SHA256 Fingerprint: 6e:a5:47:41:d0:04:66:7e:ed:1b:48:16:63:4a:a3:a7:9e:6e:4b:96:95:0f:82:79:da:fc:8d:9b:d8:81:21:37 +-----BEGIN CERTIFICATE----- +MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB +lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt +SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG +A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe +MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v +d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh +cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn +0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ +M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a +MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd +oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI +DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy +oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0 +dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy +bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF +BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM +//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli +CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE +CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t +3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS +KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA== +-----END CERTIFICATE----- + +# Issuer: CN=Chambers of Commerce Root O=AC Camerfirma SA CIF A82743287 OU=http://www.chambersign.org +# Subject: CN=Chambers of Commerce Root O=AC Camerfirma SA CIF A82743287 OU=http://www.chambersign.org +# Label: "Camerfirma Chambers of Commerce Root" +# Serial: 0 +# MD5 Fingerprint: b0:01:ee:14:d9:af:29:18:94:76:8e:f1:69:33:2a:84 +# SHA1 Fingerprint: 6e:3a:55:a4:19:0c:19:5c:93:84:3c:c0:db:72:2e:31:30:61:f0:b1 +# SHA256 Fingerprint: 0c:25:8a:12:a5:67:4a:ef:25:f2:8b:a7:dc:fa:ec:ee:a3:48:e5:41:e6:f5:cc:4e:e6:3b:71:b3:61:60:6a:c3 +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEn +MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL +ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMg +b2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAxNjEzNDNaFw0zNzA5MzAxNjEzNDRa +MH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZpcm1hIFNBIENJRiBB +ODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3JnMSIw +IAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0B +AQEFAAOCAQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtb +unXF/KGIJPov7coISjlUxFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0d +BmpAPrMMhe5cG3nCYsS4No41XQEMIwRHNaqbYE6gZj3LJgqcQKH0XZi/caulAGgq +7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jWDA+wWFjbw2Y3npuRVDM3 +0pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFVd9oKDMyX +roDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIG +A1UdEwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5j +aGFtYmVyc2lnbi5vcmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p +26EpW1eLTXYGduHRooowDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIA +BzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hhbWJlcnNpZ24ub3JnMCcGA1Ud +EgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYDVR0gBFEwTzBN +BgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz +aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEB +AAxBl8IahsAifJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZd +p0AJPaxJRUXcLo0waLIJuvvDL8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi +1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wNUPf6s+xCX6ndbcj0dc97wXImsQEc +XCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/nADydb47kMgkdTXg0 +eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1erfu +tGWaIZDgqtCYvDi1czyL+Nw= +-----END CERTIFICATE----- + +# Issuer: CN=Global Chambersign Root O=AC Camerfirma SA CIF A82743287 OU=http://www.chambersign.org +# Subject: CN=Global Chambersign Root O=AC Camerfirma SA CIF A82743287 OU=http://www.chambersign.org +# Label: "Camerfirma Global Chambersign Root" +# Serial: 0 +# MD5 Fingerprint: c5:e6:7b:bf:06:d0:4f:43:ed:c4:7a:65:8a:fb:6b:19 +# SHA1 Fingerprint: 33:9b:6b:14:50:24:9b:55:7a:01:87:72:84:d9:e0:2f:c3:d2:d8:e9 +# SHA256 Fingerprint: ef:3c:b4:17:fc:8e:bf:6f:97:87:6c:9e:4e:ce:39:de:1e:a5:fe:64:91:41:d1:02:8b:7d:11:c0:b2:29:8c:ed +-----BEGIN CERTIFICATE----- +MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEn +MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL +ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENo +YW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYxNDE4WhcNMzcwOTMwMTYxNDE4WjB9 +MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgy +NzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4G +A1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUA +A4IBDQAwggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0 +Mi+ITaFgCPS3CU6gSS9J1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/s +QJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8Oby4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpV +eAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl6DJWk0aJqCWKZQbua795 +B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c8lCrEqWh +z0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0T +AQH/BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1i +ZXJzaWduLm9yZy9jaGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4w +TcbOX60Qq+UDpfqpFDAOBgNVHQ8BAf8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAH +MCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBjaGFtYmVyc2lnbi5vcmcwKgYD +VR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9yZzBbBgNVHSAE +VDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh +bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0B +AQUFAAOCAQEAPDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUM +bKGKfKX0j//U2K0X1S0E0T9YgOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXi +ryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJPJ7oKXqJ1/6v/2j1pReQvayZzKWG +VwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4IBHNfTIzSJRUTN3c +ecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREest2d/ +AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A== +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Kozjegyzoi (Class A) Tanusitvanykiado O=NetLock Halozatbiztonsagi Kft. OU=Tanusitvanykiadok +# Subject: CN=NetLock Kozjegyzoi (Class A) Tanusitvanykiado O=NetLock Halozatbiztonsagi Kft. OU=Tanusitvanykiadok +# Label: "NetLock Notary (Class A) Root" +# Serial: 259 +# MD5 Fingerprint: 86:38:6d:5e:49:63:6c:85:5c:db:6d:dc:94:b7:d0:f7 +# SHA1 Fingerprint: ac:ed:5f:65:53:fd:25:ce:01:5f:1f:7a:48:3b:6a:74:9f:61:78:c6 +# SHA256 Fingerprint: 7f:12:cd:5f:7e:5e:29:0e:c7:d8:51:79:d5:b7:2c:20:a5:be:75:08:ff:db:5b:f8:1a:b9:68:4a:7f:c9:f6:67 +-----BEGIN CERTIFICATE----- +MIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhV +MRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMe +TmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0 +dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFzcyBB +KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oXDTE5MDIxOTIzMTQ0 +N1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhC +dWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQu +MRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBL +b3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSMD7tM9DceqQWC2ObhbHDqeLVu0ThEDaiD +zl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZz+qMkjvN9wfcZnSX9EUi +3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC/tmwqcm8 +WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LY +Oph7tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2Esi +NCubMvJIH5+hCoR64sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCC +ApswDgYDVR0PAQH/BAQDAgAGMBIGA1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4 +QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZRUxFTSEgRXplbiB0 +YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRhdGFz +aSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu +IEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtm +ZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMg +ZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVs +amFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJhc2EgbWVndGFsYWxoYXRv +IGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBzOi8vd3d3 +Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6 +ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1 +YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3Qg +dG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRs +b2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNAbmV0bG9jay5uZXQuMA0G +CSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5ayZrU3/b39/zcT0mwBQO +xmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjPytoUMaFP +0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQ +QeJBCWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxk +f1qbFFgBJ34TUMdrKuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK +8CtmdWOMovsEPoMOmzbwGOQmIMOM8CgHrTwXZoi1/baI +-----END CERTIFICATE----- + +# Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com +# Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com +# Label: "XRamp Global CA Root" +# Serial: 107108908803651509692980124233745014957 +# MD5 Fingerprint: a1:0b:44:b3:ca:10:d8:00:6e:9d:0f:d8:0f:92:0a:d1 +# SHA1 Fingerprint: b8:01:86:d1:eb:9c:86:a5:41:04:cf:30:54:f3:4c:52:b7:e5:58:c6 +# SHA256 Fingerprint: ce:cd:dc:90:50:99:d8:da:df:c5:b1:d2:09:b7:37:cb:e2:c1:8c:fb:2c:10:c0:ff:0b:cf:0d:32:86:fc:1a:a2 +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +# Issuer: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority +# Subject: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority +# Label: "Go Daddy Class 2 CA" +# Serial: 0 +# MD5 Fingerprint: 91:de:06:25:ab:da:fd:32:17:0c:bb:25:17:2a:84:67 +# SHA1 Fingerprint: 27:96:ba:e6:3f:18:01:e2:77:26:1b:a0:d7:77:70:02:8f:20:ee:e4 +# SHA256 Fingerprint: c3:84:6b:f2:4b:9e:93:ca:64:27:4c:0e:c6:7c:1e:cc:5e:02:4f:fc:ac:d2:d7:40:19:35:0e:81:fe:54:6a:e4 +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- + +# Issuer: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority +# Subject: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority +# Label: "Starfield Class 2 CA" +# Serial: 0 +# MD5 Fingerprint: 32:4a:4b:bb:c8:63:69:9b:be:74:9a:c6:dd:1d:46:24 +# SHA1 Fingerprint: ad:7e:1c:28:b0:64:ef:8f:60:03:40:20:14:c3:d0:e3:37:0e:b5:8a +# SHA256 Fingerprint: 14:65:fa:20:53:97:b8:76:fa:a6:f0:a9:95:8e:55:90:e4:0f:cc:7f:aa:4f:b7:c2:c8:67:75:21:fb:5f:b6:58 +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Subject: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Label: "StartCom Certification Authority" +# Serial: 1 +# MD5 Fingerprint: 22:4d:8f:8a:fc:f7:35:c2:bb:57:34:90:7b:8b:22:16 +# SHA1 Fingerprint: 3e:2b:f7:f2:03:1b:96:f3:8c:e6:c4:d8:a8:5d:3e:2d:58:47:6a:0f +# SHA256 Fingerprint: c7:66:a9:be:f2:d4:07:1c:86:3a:31:aa:49:20:e8:13:b2:d1:98:60:8c:b7:b7:cf:e2:11:43:b8:36:df:09:ea +-----BEGIN CERTIFICATE----- +MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE +FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j +ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js +LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM +BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0 +Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy +dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh +cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh +YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg +dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp +bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ +YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT +TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ +9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8 +jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW +FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz +ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1 +ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L +EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu +L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq +yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC +O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V +um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh +NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14= +-----END CERTIFICATE----- + +# Issuer: O=Government Root Certification Authority +# Subject: O=Government Root Certification Authority +# Label: "Taiwan GRCA" +# Serial: 42023070807708724159991140556527066870 +# MD5 Fingerprint: 37:85:44:53:32:45:1f:20:f0:f3:95:e1:25:c4:43:4e +# SHA1 Fingerprint: f4:8b:11:bf:de:ab:be:94:54:20:71:e6:41:de:6b:be:88:2b:40:b9 +# SHA256 Fingerprint: 76:00:29:5e:ef:e8:5b:9e:1f:d6:24:db:76:06:2a:aa:ae:59:81:8a:54:d2:77:4c:d4:c0:b2:c0:11:31:e1:b3 +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/ +MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow +PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR +IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q +gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy +yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts +F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2 +jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx +ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC +VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK +YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH +EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN +Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud +DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE +MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK +UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ +TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf +qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK +ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE +JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7 +hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1 +EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm +nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX +udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz +ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe +LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl +pYYsfPQS +-----END CERTIFICATE----- + +# Issuer: CN=Swisscom Root CA 1 O=Swisscom OU=Digital Certificate Services +# Subject: CN=Swisscom Root CA 1 O=Swisscom OU=Digital Certificate Services +# Label: "Swisscom Root CA 1" +# Serial: 122348795730808398873664200247279986742 +# MD5 Fingerprint: f8:38:7c:77:88:df:2c:16:68:2e:c2:e2:52:4b:b8:f9 +# SHA1 Fingerprint: 5f:3a:fc:0a:8b:64:f6:86:67:34:74:df:7e:a9:a2:fe:f9:fa:7a:51 +# SHA256 Fingerprint: 21:db:20:12:36:60:bb:2e:d4:18:20:5d:a1:1e:e7:a8:5a:65:e2:bc:6e:55:b5:af:7e:78:99:c8:a2:66:d9:2e +-----BEGIN CERTIFICATE----- +MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBk +MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 +YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg +Q0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4MTgyMjA2MjBaMGQxCzAJBgNVBAYT +AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp +Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9 +m2BtRsiMMW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdih +FvkcxC7mlSpnzNApbjyFNDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/ +TilftKaNXXsLmREDA/7n29uj/x2lzZAeAR81sH8A25Bvxn570e56eqeqDFdvpG3F +EzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkCb6dJtDZd0KTeByy2dbco +kdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn7uHbHaBu +HYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNF +vJbNcA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo +19AOeCMgkckkKmUpWyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjC +L3UcPX7ape8eYIVpQtPM+GP+HkM5haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJW +bjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNYMUJDLXT5xp6mig/p/r+D5kNX +JLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw +FDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j +BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzc +K6FptWfUjNP9MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzf +ky9NfEBWMXrrpA9gzXrzvsMnjgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7Ik +Vh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQMbFamIp1TpBcahQq4FJHgmDmHtqB +sfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4HVtA4oJVwIHaM190e +3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtlvrsR +ls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ip +mXeascClOS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HH +b6D0jqTsNFFbjCYDcKF31QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksf +rK/7DZBaZmBwXarNeNQk7shBoJMBkpxqnvy5JMWzFYJ+vq6VK+uxwNrjAWALXmms +hFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCyx/yP2FS1k2Kdzs9Z+z0Y +zirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMWNY6E0F/6 +MBr1mmz0DlP5OlvRHA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root CA" +# Serial: 17154717934120587862167794914071425081 +# MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72 +# SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43 +# SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root CA" +# Serial: 10944719598952040374951832963794454346 +# MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e +# SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36 +# SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61 +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert High Assurance EV Root CA" +# Serial: 3553400076410547919724730734378100087 +# MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a +# SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25 +# SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +# Issuer: CN=Class 2 Primary CA O=Certplus +# Subject: CN=Class 2 Primary CA O=Certplus +# Label: "Certplus Class 2 Primary CA" +# Serial: 177770208045934040241468760488327595043 +# MD5 Fingerprint: 88:2c:8c:52:b8:a2:3c:f3:f7:bb:03:ea:ae:ac:42:0b +# SHA1 Fingerprint: 74:20:74:41:72:9c:dd:92:ec:79:31:d8:23:10:8d:c2:81:92:e2:bb +# SHA256 Fingerprint: 0f:99:3c:8a:ef:97:ba:af:56:87:14:0e:d5:9a:d1:82:1b:b4:af:ac:f0:aa:9a:58:b5:d5:7a:33:8a:3a:fb:cb +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw +PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz +cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9 +MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz +IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ +ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR +VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL +kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd +EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas +H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0 +HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud +DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4 +QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu +Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/ +AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8 +yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR +FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA +ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB +kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 +l7+ijrRU +-----END CERTIFICATE----- + +# Issuer: CN=DST Root CA X3 O=Digital Signature Trust Co. +# Subject: CN=DST Root CA X3 O=Digital Signature Trust Co. +# Label: "DST Root CA X3" +# Serial: 91299735575339953335919266965803778155 +# MD5 Fingerprint: 41:03:52:dc:0f:f7:50:1b:16:f0:02:8e:ba:6f:45:c5 +# SHA1 Fingerprint: da:c9:02:4f:54:d8:f6:df:94:93:5f:b1:73:26:38:ca:6a:d7:7c:13 +# SHA256 Fingerprint: 06:87:26:03:31:a7:24:03:d9:09:f1:05:e6:9b:cf:0d:32:e1:bd:24:93:ff:c6:d9:20:6d:11:bc:d6:77:07:39 +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- + +# Issuer: CN=DST ACES CA X6 O=Digital Signature Trust OU=DST ACES +# Subject: CN=DST ACES CA X6 O=Digital Signature Trust OU=DST ACES +# Label: "DST ACES CA X6" +# Serial: 17771143917277623872238992636097467865 +# MD5 Fingerprint: 21:d8:4c:82:2b:99:09:33:a2:eb:14:24:8d:8e:5f:e8 +# SHA1 Fingerprint: 40:54:da:6f:1c:3f:40:74:ac:ed:0f:ec:cd:db:79:d1:53:fb:90:1d +# SHA256 Fingerprint: 76:7c:95:5a:76:41:2c:89:af:68:8e:90:a1:c7:0f:55:6c:fd:6b:60:25:db:ea:10:41:6d:7e:b6:83:1f:8c:40 +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBb +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3Qx +ETAPBgNVBAsTCERTVCBBQ0VTMRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0w +MzExMjAyMTE5NThaFw0xNzExMjAyMTE5NThaMFsxCzAJBgNVBAYTAlVTMSAwHgYD +VQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UECxMIRFNUIEFDRVMx +FzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPu +ktKe1jzIDZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7 +gLFViYsx+tC3dr5BPTCapCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZH +fAjIgrrep4c9oW24MFbCswKBXy314powGCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4a +ahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPyMjwmR/onJALJfh1biEIT +ajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rk +c3QuY29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjto +dHRwOi8vd3d3LnRydXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMt +aW5kZXguaHRtbDAdBgNVHQ4EFgQUCXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZI +hvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V25FYrnJmQ6AgwbN99Pe7lv7Uk +QIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6tFr8hlxCBPeP/ +h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq +nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpR +rscL9yuwNwXsvFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf2 +9w4LTJxoeHtxMcfrHuBnQfO3oKfN5XozNmr6mis= +-----END CERTIFICATE----- + +# Issuer: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı O=(c) 2005 TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. +# Subject: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı O=(c) 2005 TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. +# Label: "TURKTRUST Certificate Services Provider Root 1" +# Serial: 1 +# MD5 Fingerprint: f1:6a:22:18:c9:cd:df:ce:82:1d:1d:b7:78:5c:a9:a5 +# SHA1 Fingerprint: 79:98:a3:08:e1:4d:65:85:e6:c2:1e:15:3a:71:9f:ba:5a:d3:4a:d9 +# SHA256 Fingerprint: 44:04:e3:3b:5e:14:0d:cf:99:80:51:fd:fc:80:28:c7:c8:16:15:c5:ee:73:7b:11:1b:58:82:33:a9:b5:35:a0 +-----BEGIN CERTIFICATE----- +MIID+zCCAuOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBtzE/MD0GA1UEAww2VMOc +UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMQswCQYDVQQGDAJUUjEPMA0GA1UEBwwGQU5LQVJBMVYwVAYDVQQKDE0oYykg +MjAwNSBUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8 +dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjAeFw0wNTA1MTMxMDI3MTdaFw0xNTAz +MjIxMDI3MTdaMIG3MT8wPQYDVQQDDDZUw5xSS1RSVVNUIEVsZWt0cm9uaWsgU2Vy +dGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLExCzAJBgNVBAYMAlRSMQ8wDQYD +VQQHDAZBTktBUkExVjBUBgNVBAoMTShjKSAyMDA1IFTDnFJLVFJVU1QgQmlsZ2kg +xLBsZXRpxZ9pbSB2ZSBCaWxpxZ9pbSBHw7x2ZW5sacSfaSBIaXptZXRsZXJpIEEu +xZ4uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAylIF1mMD2Bxf3dJ7 +XfIMYGFbazt0K3gNfUW9InTojAPBxhEqPZW8qZSwu5GXyGl8hMW0kWxsE2qkVa2k +heiVfrMArwDCBRj1cJ02i67L5BuBf5OI+2pVu32Fks66WJ/bMsW9Xe8iSi9BB35J +YbOG7E6mQW6EvAPs9TscyB/C7qju6hJKjRTP8wrgUDn5CDX4EVmt5yLqS8oUBt5C +urKZ8y1UiBAG6uEaPj1nH/vO+3yC6BFdSsG5FOpU2WabfIl9BJpiyelSPJ6c79L1 +JuTm5Rh8i27fbMx4W09ysstcP4wFjdFMjK2Sx+F4f2VsSQZQLJ4ywtdKxnWKWU51 +b0dewQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAV +9VX/N5aAWSGk/KEVTCD21F/aAyT8z5Aa9CEKmu46sWrv7/hg0Uw2ZkUd82YCdAR7 +kjCo3gp2D++Vbr3JN+YaDayJSFvMgzbC9UZcWYJWtNX+I7TYVBxEq8Sn5RTOPEFh +fEPmzcSBCYsk+1Ql1haolgxnB2+zUEfjHCQo3SqYpGH+2+oSN7wBGjSFvW5P55Fy +B0SFHljKVETd96y5y4khctuPwGkplyqjrhgjlxxBKot8KsF8kOipKMDTkcatKIdA +aLX/7KfS0zgYnNN9aV3wxqUeJBujR/xpB2jn5Jq07Q+hh4cCzofSSE7hvP/L8XKS +RGQDJereW26fyfJOrN3H +-----END CERTIFICATE----- + +# Issuer: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. (c) Kasım 2005 +# Subject: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. (c) Kasım 2005 +# Label: "TURKTRUST Certificate Services Provider Root 2" +# Serial: 1 +# MD5 Fingerprint: 37:a5:6e:d4:b1:25:84:97:b7:fd:56:15:7a:f9:a2:00 +# SHA1 Fingerprint: b4:35:d4:e1:11:9d:1c:66:90:a7:49:eb:b3:94:bd:63:7b:a7:82:b7 +# SHA256 Fingerprint: c4:70:cf:54:7e:23:02:b9:77:fb:29:dd:71:a8:9a:7b:6c:1f:60:77:7b:03:29:f5:60:17:f3:28:bf:4f:6b:e6 +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOc +UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xS +S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg +SGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcNMDUxMTA3MTAwNzU3 +WhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVrdHJv +bmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJU +UjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSw +bGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWe +LiAoYykgS2FzxLFtIDIwMDUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqeLCDe2JAOCtFp0if7qnef +J1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKIx+XlZEdh +R3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJ +Qv2gQrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGX +JHpsmxcPbe9TmJEr5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1p +zpwACPI2/z7woQ8arBT9pmAPAgMBAAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58S +Fq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8GA1UdEwEB/wQFMAMBAf8wDQYJ +KoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/nttRbj2hWyfIvwq +ECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4 +Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFz +gw2lGh1uEpJ+hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotH +uFEJjOp9zYhys2AzsfAKRO8P9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LS +y3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5UrbnBEI= +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign Gold CA - G2 O=SwissSign AG +# Subject: CN=SwissSign Gold CA - G2 O=SwissSign AG +# Label: "SwissSign Gold CA - G2" +# Serial: 13492815561806991280 +# MD5 Fingerprint: 24:77:d9:a8:91:d1:3b:fa:88:2d:c2:ff:f8:cd:33:93 +# SHA1 Fingerprint: d8:c5:38:8a:b7:30:1b:1b:6e:d4:7a:e6:45:25:3a:6f:9f:1a:27:61 +# SHA256 Fingerprint: 62:dd:0b:e9:b9:f5:0a:16:3e:a0:f8:e7:5c:05:3b:1e:ca:57:ea:55:c8:68:8f:64:7c:68:81:f2:c8:35:7b:95 +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign Silver CA - G2 O=SwissSign AG +# Subject: CN=SwissSign Silver CA - G2 O=SwissSign AG +# Label: "SwissSign Silver CA - G2" +# Serial: 5700383053117599563 +# MD5 Fingerprint: e0:06:a1:c9:7d:cf:c9:fc:0d:c0:56:75:96:d8:62:13 +# SHA1 Fingerprint: 9b:aa:e5:9f:56:ee:21:cb:43:5a:be:25:93:df:a7:f0:40:d1:1d:cb +# SHA256 Fingerprint: be:6c:4d:a2:bb:b9:ba:59:b6:f3:93:97:68:37:42:46:c3:c0:05:99:3f:a9:8f:02:0d:1d:ed:be:d4:8a:81:d5 +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. +# Subject: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. +# Label: "GeoTrust Primary Certification Authority" +# Serial: 32798226551256963324313806436981982369 +# MD5 Fingerprint: 02:26:c3:01:5e:08:30:37:43:a9:d0:7d:cf:37:e6:bf +# SHA1 Fingerprint: 32:3c:11:8e:1b:f7:b8:b6:52:54:e2:e2:10:0d:d6:02:90:37:f0:96 +# SHA256 Fingerprint: 37:d5:10:06:c5:12:ea:ab:62:64:21:f1:ec:8c:92:01:3f:c5:f8:2a:e9:8e:e5:33:eb:46:19:b8:de:b4:d0:6c +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo +R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx +MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 +AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA +ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 +7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W +kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI +mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ +KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 +6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl +4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K +oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj +UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU +AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA" +# Serial: 69529181992039203566298953787712940909 +# MD5 Fingerprint: 8c:ca:dc:0b:22:ce:f5:be:72:ac:41:1a:11:a8:d8:12 +# SHA1 Fingerprint: 91:c6:d6:ee:3e:8a:c8:63:84:e5:48:c2:99:29:5c:75:6c:81:7b:81 +# SHA256 Fingerprint: 8d:72:2f:81:a9:c1:13:c0:79:1d:f1:36:a2:96:6d:b2:6c:95:0a:97:1d:b4:6b:41:99:f4:ea:54:b7:8b:fb:9f +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Class 3 Public Primary Certification Authority - G5" +# Serial: 33037644167568058970164719475676101450 +# MD5 Fingerprint: cb:17:e4:31:67:3e:e2:09:fe:45:57:93:f3:0a:fa:1c +# SHA1 Fingerprint: 4e:b6:d5:78:49:9b:1c:cf:5f:58:1e:ad:56:be:3d:9b:67:44:a5:e5 +# SHA256 Fingerprint: 9a:cf:ab:7e:43:c8:d8:80:d0:6b:26:2a:94:de:ee:e4:b4:65:99:89:c3:d0:ca:f1:9b:af:64:05:e4:1a:b7:df +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +# Issuer: CN=SecureTrust CA O=SecureTrust Corporation +# Subject: CN=SecureTrust CA O=SecureTrust Corporation +# Label: "SecureTrust CA" +# Serial: 17199774589125277788362757014266862032 +# MD5 Fingerprint: dc:32:c3:a7:6d:25:57:c7:68:09:9d:ea:2d:a9:a2:d1 +# SHA1 Fingerprint: 87:82:c6:c3:04:35:3b:cf:d2:96:92:d2:59:3e:7d:44:d9:34:ff:11 +# SHA256 Fingerprint: f1:c1:b5:0a:e5:a2:0d:d8:03:0e:c9:f6:bc:24:82:3d:d3:67:b5:25:57:59:b4:e7:1b:61:fc:e9:f7:37:5d:73 +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +# Issuer: CN=Secure Global CA O=SecureTrust Corporation +# Subject: CN=Secure Global CA O=SecureTrust Corporation +# Label: "Secure Global CA" +# Serial: 9751836167731051554232119481456978597 +# MD5 Fingerprint: cf:f4:27:0d:d4:ed:dc:65:16:49:6d:3d:da:bf:6e:de +# SHA1 Fingerprint: 3a:44:73:5a:e5:81:90:1f:24:86:61:46:1e:3b:9c:c4:5f:f5:3a:1b +# SHA256 Fingerprint: 42:00:f5:04:3a:c8:59:0e:bb:52:7d:20:9e:d1:50:30:29:fb:cb:d4:1c:a1:b5:06:ec:27:f1:5a:de:7d:ac:69 +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO Certification Authority O=COMODO CA Limited +# Label: "COMODO Certification Authority" +# Serial: 104350513648249232941998508985834464573 +# MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75 +# SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b +# SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66 +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- + +# Issuer: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C. +# Subject: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C. +# Label: "Network Solutions Certificate Authority" +# Serial: 116697915152937497490437556386812487904 +# MD5 Fingerprint: d3:f3:a6:16:c0:fa:6b:1d:59:b1:2d:96:4d:0e:11:2e +# SHA1 Fingerprint: 74:f8:a3:c3:ef:e7:b3:90:06:4b:83:90:3c:21:64:60:20:e5:df:ce +# SHA256 Fingerprint: 15:f0:ba:00:a3:ac:7a:f3:ac:88:4c:07:2b:10:11:a0:77:bd:77:c0:97:f4:01:64:b2:f8:59:8a:bd:83:86:0c +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi +MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV +UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO +ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz +c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP +OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl +mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF +BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4 +qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw +gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu +bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp +dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8 +6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/ +h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH +/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN +pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +# Issuer: CN=WellsSecure Public Root Certificate Authority O=Wells Fargo WellsSecure OU=Wells Fargo Bank NA +# Subject: CN=WellsSecure Public Root Certificate Authority O=Wells Fargo WellsSecure OU=Wells Fargo Bank NA +# Label: "WellsSecure Public Root Certificate Authority" +# Serial: 1 +# MD5 Fingerprint: 15:ac:a5:c2:92:2d:79:bc:e8:7f:cb:67:ed:02:cf:36 +# SHA1 Fingerprint: e7:b4:f6:9d:61:ec:90:69:db:7e:90:a7:40:1a:3c:f4:7d:4f:e8:ee +# SHA256 Fingerprint: a7:12:72:ae:aa:a3:cf:e8:72:7f:7f:b3:9f:0f:b3:d1:e5:42:6e:90:60:b0:6e:e6:f1:3e:9a:3c:58:33:cd:43 +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMx +IDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxs +cyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9v +dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDcxMjEzMTcwNzU0WhcNMjIxMjE0 +MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdl +bGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQD +DC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+r +WxxTkqxtnt3CxC5FlAM1iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjU +Dk/41itMpBb570OYj7OeUt9tkTmPOL13i0Nj67eT/DBMHAGTthP796EfvyXhdDcs +HqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8bJVhHlfXBIEyg1J55oNj +z7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiBK0HmOFaf +SZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/Slwxl +AgMBAAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqG +KGh0dHA6Ly9jcmwucGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0P +AQH/BAQDAgHGMB0GA1UdDgQWBBQmlRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0j +BIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGBi6SBiDCBhTELMAkGA1UEBhMC +VVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNX +ZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEB +ALkVsUSRzCPIK0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd +/ZDJPHV3V3p9+N701NX3leZ0bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pB +A4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSljqHyita04pO2t/caaH/+Xc/77szWn +k4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+esE2fDbbFwRnzVlhE9 +iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJtylv +2G0xffX8oRAHh84vWdw+WNs= +-----END CERTIFICATE----- + +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Issuer: CN=IGC/A O=PM/SGDN OU=DCSSI +# Subject: CN=IGC/A O=PM/SGDN OU=DCSSI +# Label: "IGC/A" +# Serial: 245102874772 +# MD5 Fingerprint: 0c:7f:dd:6a:f4:2a:b9:c8:9b:bd:20:7e:a9:db:5c:37 +# SHA1 Fingerprint: 60:d6:89:74:b5:c2:65:9e:8a:0f:c1:88:7c:88:d2:46:69:1b:18:2c +# SHA256 Fingerprint: b9:be:a7:86:0a:96:2e:a3:61:1d:ab:97:ab:6d:a3:e2:1c:10:68:b9:7d:55:57:5e:d0:e1:12:79:c1:1c:89:32 +-----BEGIN CERTIFICATE----- +MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYT +AkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQ +TS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG +9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMB4XDTAyMTIxMzE0MjkyM1oXDTIw +MTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAM +BgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEO +MAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2 +LmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaI +s9z4iPf930Pfeo2aSVz2TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2 +xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCWSo7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4 +u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYyHF2fYPepraX/z9E0+X1b +F8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNdfrGoRpAx +Vs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGd +PDPQtQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNV +HSAEDjAMMAoGCCqBegF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAx +NjAfBgNVHSMEGDAWgBSjBS8YYFDCiQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUF +AAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RKq89toB9RlPhJy3Q2FLwV3duJ +L92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3QMZsyK10XZZOY +YLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg +Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2a +NjSaTFR+FwNIlQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R +0982gaEbeC9xs/FZTEYYKKuF0mBWWg== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication EV RootCA1 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication EV RootCA1 +# Label: "Security Communication EV RootCA1" +# Serial: 0 +# MD5 Fingerprint: 22:2d:a6:01:ea:7c:0a:f7:f0:6c:56:43:3f:77:76:d3 +# SHA1 Fingerprint: fe:b8:c4:32:dc:f9:76:9a:ce:ae:3d:d8:90:8f:fd:28:86:65:64:7d +# SHA256 Fingerprint: a2:2d:ba:68:1e:97:37:6e:2d:39:7d:72:8a:ae:3a:9b:62:96:b9:fd:ba:60:bc:2e:11:f6:47:f2:c6:75:fb:37 +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMh +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIz +MloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09N +IFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNlY3VyaXR5IENvbW11 +bmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSE +RMqm4miO/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gO +zXppFodEtZDkBp2uoQSXWHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5 +bmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4zZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDF +MxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4bepJz11sS6/vmsJWXMY1 +VkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK9U2vP9eC +OKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HW +tWS3irO4G8za+6xmiEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZ +q51ihPZRwSzJIxXYKLerJRO1RuGGAv8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDb +EJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnWmHyojf6GPgcWkuF75x3sM3Z+ +Qi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEWT1MKZPlO9L9O +VL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490 +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GA CA" +# Serial: 86718877871133159090080555911823548314 +# MD5 Fingerprint: bc:6c:51:33:a7:e9:d3:66:63:54:15:72:1b:21:92:93 +# SHA1 Fingerprint: 59:22:a1:e1:5a:ea:16:35:21:f8:98:39:6a:46:46:b0:44:1b:0f:a9 +# SHA256 Fingerprint: 41:c9:23:86:6a:b4:ca:d6:b7:ad:57:80:81:58:2e:02:07:97:a6:cb:df:4f:ff:78:ce:83:96:b3:89:37:d7:f5 +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB +ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly +aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w +NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G +A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX +SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR +VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 +w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF +mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg +4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 +4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw +EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx +SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 +ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 +vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi +Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ +/L7fCg0= +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA O=Microsec Ltd. OU=e-Szigno CA +# Subject: CN=Microsec e-Szigno Root CA O=Microsec Ltd. OU=e-Szigno CA +# Label: "Microsec e-Szigno Root CA" +# Serial: 272122594155480254301341951808045322001 +# MD5 Fingerprint: f0:96:b6:2f:c5:10:d5:67:8e:83:25:32:e8:5e:2e:e5 +# SHA1 Fingerprint: 23:88:c9:d3:71:cc:9e:96:3d:ff:7d:3c:a7:ce:fc:d6:25:ec:19:0d +# SHA256 Fingerprint: 32:7a:3d:76:1a:ba:de:a0:34:eb:99:84:06:27:5c:b1:a4:77:6e:fd:ae:2f:df:6d:01:68:ea:1c:4f:55:67:d0 +-----BEGIN CERTIFICATE----- +MIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAw +cjELMAkGA1UEBhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNy +b3NlYyBMdGQuMRQwEgYDVQQLEwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9z +ZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0MDYxMjI4NDRaFw0xNzA0MDYxMjI4 +NDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEWMBQGA1UEChMN +TWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMTGU1p +Y3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2u +uO/TEdyB5s87lozWbxXGd36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+ +LMsvfUh6PXX5qqAnu3jCBspRwn5mS6/NoqdNAoI/gqyFxuEPkEeZlApxcpMqyabA +vjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjcQR/Ji3HWVBTji1R4P770 +Yjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJPqW+jqpx +62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcB +AQRbMFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3Aw +LQYIKwYBBQUHMAKGIWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAP +BgNVHRMBAf8EBTADAQH/MIIBcwYDVR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIB +AQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3LmUtc3ppZ25vLmh1L1NaU1ov +MIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0AdAB2AOEAbgB5 +ACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn +AGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABT +AHoAbwBsAGcA4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABh +ACAAcwB6AGUAcgBpAG4AdAAgAGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABo +AHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMAegBpAGcAbgBvAC4AaAB1AC8AUwBa +AFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6Ly93d3cuZS1zemln +bm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NOPU1p +Y3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxP +PU1pY3Jvc2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZv +Y2F0aW9uTGlzdDtiaW5hcnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuB +EGluZm9AZS1zemlnbm8uaHWkdzB1MSMwIQYDVQQDDBpNaWNyb3NlYyBlLVN6aWdu +w7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhTWjEWMBQGA1UEChMNTWlj +cm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhVMIGsBgNV +HSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJI +VTERMA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDAS +BgNVBAsTC2UtU3ppZ25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBS +b290IENBghEAzLjnv04pGv2i3GalHCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS +8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMTnGZjWS7KXHAM/IO8VbH0jgds +ZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FEaGAHQzAxQmHl +7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a +86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfR +hUZLphK3dehKyVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/ +MPMMNz7UwiiAc7EBt51alhQBS6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU= +-----END CERTIFICATE----- + +# Issuer: CN=Certigna O=Dhimyotis +# Subject: CN=Certigna O=Dhimyotis +# Label: "Certigna" +# Serial: 18364802974209362175 +# MD5 Fingerprint: ab:57:a6:5b:7d:42:82:19:b5:d8:58:26:28:5e:fd:ff +# SHA1 Fingerprint: b1:2e:13:63:45:86:a4:6f:1a:b2:60:68:37:58:2d:c4:ac:fd:94:97 +# SHA256 Fingerprint: e3:b6:a2:db:2e:d7:ce:48:84:2f:7a:c5:32:41:c7:b7:1d:54:14:4b:fb:40:c1:1f:3f:1d:0b:42:f5:ee:a1:2d +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +# Issuer: CN=TC TrustCenter Class 2 CA II O=TC TrustCenter GmbH OU=TC TrustCenter Class 2 CA +# Subject: CN=TC TrustCenter Class 2 CA II O=TC TrustCenter GmbH OU=TC TrustCenter Class 2 CA +# Label: "TC TrustCenter Class 2 CA II" +# Serial: 941389028203453866782103406992443 +# MD5 Fingerprint: ce:78:33:5c:59:78:01:6e:18:ea:b9:36:a0:b9:2e:23 +# SHA1 Fingerprint: ae:50:83:ed:7c:f4:5c:bc:8f:61:c6:21:fe:68:5d:79:42:21:15:6e +# SHA256 Fingerprint: e6:b8:f8:76:64:85:f8:07:ae:7f:8d:ac:16:70:46:1f:07:c0:a1:3e:ef:3a:1f:f7:17:53:8d:7a:ba:d3:91:b4 +-----BEGIN CERTIFICATE----- +MIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV +BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0 +Q2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYwMTEyMTQzODQzWhcNMjUxMjMxMjI1 +OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i +SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UEAxMc +VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jf +tMjWQ+nEdVl//OEd+DFwIxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKg +uNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2J +XjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQXa7pIXSSTYtZgo+U4+lK +8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7uSNQZu+99 +5OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3 +kUrL84J6E1wIqzCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy +dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6 +Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz +JTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290 +Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u +TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iS +GNn3Bzn1LL4GdXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprt +ZjluS5TmVfwLG4t3wVMTZonZKNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8 +au0WOB9/WIFaGusyiC2y8zl3gK9etmF1KdsjTYjKUCjLhdLTEKJZbtOTVAB6okaV +hgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kPJOzHdiEoZa5X6AeI +dUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfkvQ== +-----END CERTIFICATE----- + +# Issuer: CN=TC TrustCenter Class 3 CA II O=TC TrustCenter GmbH OU=TC TrustCenter Class 3 CA +# Subject: CN=TC TrustCenter Class 3 CA II O=TC TrustCenter GmbH OU=TC TrustCenter Class 3 CA +# Label: "TC TrustCenter Class 3 CA II" +# Serial: 1506523511417715638772220530020799 +# MD5 Fingerprint: 56:5f:aa:80:61:12:17:f6:67:21:e6:2b:6d:61:56:8e +# SHA1 Fingerprint: 80:25:ef:f4:6e:70:c8:d4:72:24:65:84:fe:40:3b:8a:8d:6a:db:f5 +# SHA256 Fingerprint: 8d:a0:84:fc:f9:9c:e0:77:22:f8:9b:32:05:93:98:06:fa:5c:b8:11:e1:c8:13:f6:a1:08:c7:d3:36:b3:40:8e +-----BEGIN CERTIFICATE----- +MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV +BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0 +Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYwMTEyMTQ0MTU3WhcNMjUxMjMxMjI1 +OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i +SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UEAxMc +VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJW +Ht4bNwcwIi9v8Qbxq63WyKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+Q +Vl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo6SI7dYnWRBpl8huXJh0obazovVkdKyT2 +1oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZuV3bOx4a+9P/FRQI2Alq +ukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk2ZyqBwi1 +Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NX +XAek0CSnwPIA1DCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy +dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6 +Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz +JTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290 +Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u +TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlN +irTzwppVMXzEO2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8 +TtXqluJucsG7Kv5sbviRmEb8yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6 +g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9IJqDnxrcOfHFcqMRA/07QlIp2+gB +95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal092Y+tTmBvTwtiBj +S+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc5A== +-----END CERTIFICATE----- + +# Issuer: CN=TC TrustCenter Universal CA I O=TC TrustCenter GmbH OU=TC TrustCenter Universal CA +# Subject: CN=TC TrustCenter Universal CA I O=TC TrustCenter GmbH OU=TC TrustCenter Universal CA +# Label: "TC TrustCenter Universal CA I" +# Serial: 601024842042189035295619584734726 +# MD5 Fingerprint: 45:e1:a5:72:c5:a9:36:64:40:9e:f5:e4:58:84:67:8c +# SHA1 Fingerprint: 6b:2f:34:ad:89:58:be:62:fd:b0:6b:5c:ce:bb:9d:d9:4f:4e:39:f3 +# SHA256 Fingerprint: eb:f3:c0:2a:87:89:b1:fb:7d:51:19:95:d6:63:b7:29:06:d9:13:ce:0d:5e:10:56:8a:8a:77:e2:58:61:67:e7 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV +BAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1 +c3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcNMDYwMzIyMTU1NDI4WhcNMjUxMjMx +MjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIg +R21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYwJAYD +VQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSR +JJZ4Hgmgm5qVSkr1YnwCqMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3T +fCZdzHd55yx4Oagmcw6iXSVphU9VDprvxrlE4Vc93x9UIuVvZaozhDrzznq+VZeu +jRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtwag+1m7Z3W0hZneTvWq3z +wZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9OgdwZu5GQ +fezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYD +VR0jBBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0G +CSqGSIb3DQEBBQUAA4IBAQAo0uCG1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X1 +7caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/CyvwbZ71q+s2IhtNerNXxTPqYn +8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3ghUJGooWMNjs +ydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT +ujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/ +2TYcuiUaUj0a7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY +-----END CERTIFICATE----- + +# Issuer: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center +# Subject: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center +# Label: "Deutsche Telekom Root CA 2" +# Serial: 38 +# MD5 Fingerprint: 74:01:4a:91:b1:08:c4:58:ce:47:cd:f0:dd:11:53:08 +# SHA1 Fingerprint: 85:a4:08:c0:9c:19:3e:5d:51:58:7d:cd:d6:13:30:fd:8c:de:37:bf +# SHA256 Fingerprint: b6:19:1a:50:d0:c3:97:7f:7d:a9:9b:cd:aa:c8:6a:22:7d:ae:b9:67:9e:c7:0b:a3:b0:c9:d9:22:71:c1:70:d3 +-----BEGIN CERTIFICATE----- +MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc +MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj +IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB +IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE +RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl +U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290 +IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU +ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC +QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr +rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S +NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc +QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH +txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP +BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC +AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp +tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa +IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl +6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+ +xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU +Cm26OWMohpLzGITY+9HPBVZkVw== +-----END CERTIFICATE----- + +# Issuer: CN=ComSign Secured CA O=ComSign +# Subject: CN=ComSign Secured CA O=ComSign +# Label: "ComSign Secured CA" +# Serial: 264725503855295744117309814499492384489 +# MD5 Fingerprint: 40:01:25:06:8d:21:43:6a:0e:43:00:9c:e7:43:f3:d5 +# SHA1 Fingerprint: f9:cd:0e:2c:da:76:24:c1:8f:bd:f0:f0:ab:b6:45:b8:f7:fe:d5:7a +# SHA256 Fingerprint: 50:79:41:c7:44:60:a0:b4:70:86:22:0d:4e:99:32:57:2a:b5:d1:b5:bb:cb:89:80:ab:1c:b1:76:51:a8:44:d2 +-----BEGIN CERTIFICATE----- +MIIDqzCCApOgAwIBAgIRAMcoRwmzuGxFjB36JPU2TukwDQYJKoZIhvcNAQEFBQAw +PDEbMBkGA1UEAxMSQ29tU2lnbiBTZWN1cmVkIENBMRAwDgYDVQQKEwdDb21TaWdu +MQswCQYDVQQGEwJJTDAeFw0wNDAzMjQxMTM3MjBaFw0yOTAzMTYxNTA0NTZaMDwx +GzAZBgNVBAMTEkNvbVNpZ24gU2VjdXJlZCBDQTEQMA4GA1UEChMHQ29tU2lnbjEL +MAkGA1UEBhMCSUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGtWhf +HZQVw6QIVS3joFd67+l0Kru5fFdJGhFeTymHDEjWaueP1H5XJLkGieQcPOqs49oh +gHMhCu95mGwfCP+hUH3ymBvJVG8+pSjsIQQPRbsHPaHA+iqYHU4Gk/v1iDurX8sW +v+bznkqH7Rnqwp9D5PGBpX8QTz7RSmKtUxvLg/8HZaWSLWapW7ha9B20IZFKF3ue +Mv5WJDmyVIRD9YTC2LxBkMyd1mja6YJQqTtoz7VdApRgFrFD2UNd3V2Hbuq7s8lr +9gOUCXDeFhF6K+h2j0kQmHe5Y1yLM5d19guMsqtb3nQgJT/j8xH5h2iGNXHDHYwt +6+UarA9z1YJZQIDTAgMBAAGjgacwgaQwDAYDVR0TBAUwAwEB/zBEBgNVHR8EPTA7 +MDmgN6A1hjNodHRwOi8vZmVkaXIuY29tc2lnbi5jby5pbC9jcmwvQ29tU2lnblNl +Y3VyZWRDQS5jcmwwDgYDVR0PAQH/BAQDAgGGMB8GA1UdIwQYMBaAFMFL7XC29z58 +ADsAj8c+DkWfHl3sMB0GA1UdDgQWBBTBS+1wtvc+fAA7AI/HPg5Fnx5d7DANBgkq +hkiG9w0BAQUFAAOCAQEAFs/ukhNQq3sUnjO2QiBq1BW9Cav8cujvR3qQrFHBZE7p +iL1DRYHjZiM/EoZNGeQFsOY3wo3aBijJD4mkU6l1P7CW+6tMM1X5eCZGbxs2mPtC +dsGCuY7e+0X5YxtiOzkGynd6qDwJz2w2PQ8KRUtpFhpFfTMDZflScZAmlaxMDPWL +kz/MdXSFmLr/YnpNH4n+rr2UAJm/EaXc4HnFFgt9AmEd6oX5AhVP51qJThRv4zdL +hfXBPGHg/QVBspJ/wx2g0K5SZGBrGMYmnNj1ZOQ2GmKfig8+/21OGVZOIJFsnzQz +OjRXUDpvgV4GxvU+fE6OK85lBi5d0ipTdF7Tbieejw== +-----END CERTIFICATE----- + +# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc +# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc +# Label: "Cybertrust Global Root" +# Serial: 4835703278459682877484360 +# MD5 Fingerprint: 72:e4:4a:87:e3:69:40:80:77:ea:bc:e3:f4:ff:f0:e1 +# SHA1 Fingerprint: 5f:43:e5:b1:bf:f8:78:8c:ac:1c:c7:ca:4a:9a:c6:22:2b:cc:34:c6 +# SHA256 Fingerprint: 96:0a:df:00:63:e9:63:56:75:0c:29:65:dd:0a:08:67:da:0b:9c:bd:6e:77:71:4a:ea:fb:23:49:ab:39:3d:a3 +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG +A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh +bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE +ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 +7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS +J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y +HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP +t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz +FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY +XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw +hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js +MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA +A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj +Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx +XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o +omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc +A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +# Issuer: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority +# Subject: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority +# Label: "ePKI Root Certification Authority" +# Serial: 28956088682735189655030529057352760477 +# MD5 Fingerprint: 1b:2e:00:ca:26:06:90:3d:ad:fe:6f:15:68:d3:6b:b3 +# SHA1 Fingerprint: 67:65:0d:f1:7e:8e:7e:5b:82:40:a4:f4:56:4b:cf:e2:3d:69:c6:f0 +# SHA256 Fingerprint: c0:a6:f4:dc:63:a2:4b:fd:cf:54:ef:2a:6a:08:2a:0a:72:de:35:80:3e:2f:f5:ff:52:7a:e5:d8:72:06:df:d5 +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +# Issuer: CN=TÜBİTAK UEKAE Kök Sertifika Hizmet Sağlayıcısı - Sürüm 3 O=Türkiye Bilimsel ve Teknolojik Araştırma Kurumu - TÜBİTAK OU=Ulusal Elektronik ve Kriptoloji Araştırma Enstitüsü - UEKAE/Kamu Sertifikasyon Merkezi +# Subject: CN=TÜBİTAK UEKAE Kök Sertifika Hizmet Sağlayıcısı - Sürüm 3 O=Türkiye Bilimsel ve Teknolojik Araştırma Kurumu - TÜBİTAK OU=Ulusal Elektronik ve Kriptoloji Araştırma Enstitüsü - UEKAE/Kamu Sertifikasyon Merkezi +# Label: "T\xc3\x9c\x42\xC4\xB0TAK UEKAE K\xC3\xB6k Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1 - S\xC3\xBCr\xC3\xBCm 3" +# Serial: 17 +# MD5 Fingerprint: ed:41:f5:8c:50:c5:2b:9c:73:e6:ee:6c:eb:c2:a8:26 +# SHA1 Fingerprint: 1b:4b:39:61:26:27:6b:64:91:a2:68:6d:d7:02:43:21:2d:1f:1d:96 +# SHA256 Fingerprint: e4:c7:34:30:d7:a5:b5:09:25:df:43:37:0a:0d:21:6e:9a:79:b9:d6:db:83:73:a0:c6:9e:b1:cc:31:c7:c5:2a +-----BEGIN CERTIFICATE----- +MIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRS +MRgwFgYDVQQHDA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJp +bGltc2VsIHZlIFRla25vbG9qaWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSw +VEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ryb25payB2ZSBLcmlwdG9sb2ppIEFy +YcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNVBAsMGkthbXUgU2Vy +dGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUgS8O2 +ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAe +Fw0wNzA4MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIx +GDAWBgNVBAcMD0dlYnplIC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmls +aW1zZWwgdmUgVGVrbm9sb2ppayBBcmHFn3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBU +QUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZlIEtyaXB0b2xvamkgQXJh +xZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2FtdSBTZXJ0 +aWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7Zr +IFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4h +gb46ezzb8R1Sf1n68yJMlaCQvEhOEav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yK +O7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1xnnRFDDtG1hba+818qEhTsXO +fJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR6Oqeyjh1jmKw +lZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL +hmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQID +AQABo0IwQDAdBgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmP +NOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4N5EY3ATIZJkrGG2AA1nJrvhY0D7t +wyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLTy9LQQfMmNkqblWwM +7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYhLBOh +gLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5n +oN+J1q2MdqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUs +yZyQ2uypQjyttgI= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 CA 1 O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 CA 1 O=Buypass AS-983163327 +# Label: "Buypass Class 2 CA 1" +# Serial: 1 +# MD5 Fingerprint: b8:08:9a:f0:03:cc:1b:0d:c8:6c:0b:76:a1:75:64:23 +# SHA1 Fingerprint: a0:a1:ab:90:c9:fc:84:7b:3b:12:61:e8:97:7d:5f:d3:22:61:d3:cc +# SHA256 Fingerprint: 0f:4e:9c:dd:26:4b:02:55:50:d1:70:80:63:40:21:4f:e9:44:34:c9:b0:2f:69:7e:c7:10:fc:5f:ea:fb:5e:38 +-----BEGIN CERTIFICATE----- +MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg +Q2xhc3MgMiBDQSAxMB4XDTA2MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzEL +MAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD +VQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7McXA0 +ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLX +l18xoS830r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVB +HfCuuCkslFJgNJQ72uA40Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B +5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/RuFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3 +WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0PAQH/BAQD +AgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLP +gcIV1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+ +DKhQ7SLHrQVMdvvt7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKu +BctN518fV4bVIJwo+28TOPX2EZL2fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHs +h7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5wwDX3OaJdZtB7WZ+oRxKaJyOk +LY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 3 CA 1 O=Buypass AS-983163327 +# Subject: CN=Buypass Class 3 CA 1 O=Buypass AS-983163327 +# Label: "Buypass Class 3 CA 1" +# Serial: 2 +# MD5 Fingerprint: df:3c:73:59:81:e7:39:50:81:04:4c:34:a2:cb:b3:7b +# SHA1 Fingerprint: 61:57:3a:11:df:0e:d8:7e:d5:92:65:22:ea:d0:56:d7:44:b3:23:71 +# SHA256 Fingerprint: b7:b1:2b:17:1f:82:1d:aa:99:0c:d0:fe:50:87:b1:28:44:8b:a8:e5:18:4f:84:c5:1e:02:b5:c8:fb:96:2b:24 +-----BEGIN CERTIFICATE----- +MIIDUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg +Q2xhc3MgMyBDQSAxMB4XDTA1MDUwOTE0MTMwM1oXDTE1MDUwOTE0MTMwM1owSzEL +MAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD +VQQDDBRCdXlwYXNzIENsYXNzIDMgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKSO13TZKWTeXx+HgJHqTjnmGcZEC4DVC69TB4sSveZn8AKxifZg +isRbsELRwCGoy+Gb72RRtqfPFfV0gGgEkKBYouZ0plNTVUhjP5JW3SROjvi6K//z +NIqeKNc0n6wv1g/xpC+9UrJJhW05NfBEMJNGJPO251P7vGGvqaMU+8IXF4Rs4HyI ++MkcVyzwPX6UvCWThOiaAJpFBUJXgPROztmuOfbIUxAMZTpHe2DC1vqRycZxbL2R +hzyRhkmr8w+gbCZ2Xhysm3HljbybIR6c1jh+JIAVMYKWsUnTYjdbiAwKYjT+p0h+ +mbEwi5A3lRyoH6UsjfRVyNvdWQrCrXig9IsCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUOBTmyPCppAP0Tj4io1vy1uCtQHQwDgYDVR0PAQH/BAQD +AgEGMA0GCSqGSIb3DQEBBQUAA4IBAQABZ6OMySU9E2NdFm/soT4JXJEVKirZgCFP +Bdy7pYmrEzMqnji3jG8CcmPHc3ceCQa6Oyh7pEfJYWsICCD8igWKH7y6xsL+z27s +EzNxZy5p+qksP2bAEllNC1QCkoS72xLvg3BweMhT+t/Gxv/ciC8HwEmdMldg0/L2 +mSlf56oBzKwzqBwKu5HEA6BvtjT5htOzdlSY9EqBs1OdTUDs5XcTRa9bqh/YL0yC +e/4qxFi7T/ye/QNlGioOw6UgFpRreaaiErS7GqQjel/wroQk5PMr+4okoyeYZdow +dXb8GZHo2+ubPzK/QJcHJrrM85SFSnonk8+QQtS4Wxam58tAA915 +-----END CERTIFICATE----- + +# Issuer: CN=EBG Elektronik Sertifika Hizmet Sağlayıcısı O=EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. +# Subject: CN=EBG Elektronik Sertifika Hizmet Sağlayıcısı O=EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. +# Label: "EBG Elektronik Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1" +# Serial: 5525761995591021570 +# MD5 Fingerprint: 2c:20:26:9d:cb:1a:4a:00:85:b5:b7:5a:ae:c2:01:37 +# SHA1 Fingerprint: 8c:96:ba:eb:dd:2b:07:07:48:ee:30:32:66:a0:f3:98:6e:7c:ae:58 +# SHA256 Fingerprint: 35:ae:5b:dd:d8:f7:ae:63:5c:ff:ba:56:82:a8:f0:0b:95:f4:84:62:c7:10:8e:e9:a0:e5:29:2b:07:4a:af:b2 +-----BEGIN CERTIFICATE----- +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 +QPl1K5srRmSGjR70j/sHd9DqSaIcjVIUpgqT +-----END CERTIFICATE----- + +# Issuer: O=certSIGN OU=certSIGN ROOT CA +# Subject: O=certSIGN OU=certSIGN ROOT CA +# Label: "certSIGN ROOT CA" +# Serial: 35210227249154 +# MD5 Fingerprint: 18:98:c0:d6:e9:3a:fc:f9:b0:f5:0c:f7:4b:01:44:17 +# SHA1 Fingerprint: fa:b7:ee:36:97:26:62:fb:2d:b0:2a:f6:bf:03:fd:e8:7c:4b:2f:9b +# SHA256 Fingerprint: ea:a9:62:c4:fa:4a:6b:af:eb:e4:15:19:6d:35:1c:cd:88:8d:4f:53:f3:fa:8a:e6:d7:c4:66:a9:4e:60:42:bb +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +# Issuer: CN=CNNIC ROOT O=CNNIC +# Subject: CN=CNNIC ROOT O=CNNIC +# Label: "CNNIC ROOT" +# Serial: 1228079105 +# MD5 Fingerprint: 21:bc:82:ab:49:c4:13:3b:4b:b2:2b:5c:6b:90:9c:19 +# SHA1 Fingerprint: 8b:af:4c:9b:1d:f0:2a:92:f7:da:12:8e:b9:1b:ac:f4:98:60:4b:6f +# SHA256 Fingerprint: e2:83:93:77:3d:a8:45:a6:79:f2:08:0c:c7:fb:44:a3:b7:a1:c3:79:2c:b7:eb:77:29:fd:cb:6a:8d:99:ae:a7 +-----BEGIN CERTIFICATE----- +MIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJD +TjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2 +MDcwOTE0WhcNMjcwNDE2MDcwOTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMF +Q05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzDo+/hn7E7SIX1mlwh +IhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tizVHa6 +dLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZO +V/kbZKKTVrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrC +GHn2emU1z5DrvTOTn1OrczvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gN +v7Sg2Ca+I19zN38m5pIEo3/PIKe38zrKy5nLAgMBAAGjczBxMBEGCWCGSAGG+EIB +AQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscCwQ7vptU7ETAPBgNVHRMB +Af8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991SlgrHAsEO +76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnK +OOK5Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvH +ugDnuL8BV8F3RTIMO/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7Hgvi +yJA/qIYM/PmLXoXLT1tLYhFHxUV8BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fL +buXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2G8kS1sHNzYDzAgE8yGnLRUhj +2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5mmxE= +-----END CERTIFICATE----- + +# Issuer: O=Japanese Government OU=ApplicationCA +# Subject: O=Japanese Government OU=ApplicationCA +# Label: "ApplicationCA - Japanese Government" +# Serial: 49 +# MD5 Fingerprint: 7e:23:4e:5b:a7:a5:b4:25:e9:00:07:74:11:62:ae:d6 +# SHA1 Fingerprint: 7f:8a:b0:cf:d0:51:87:6a:66:f3:36:0f:47:c8:8d:8c:d3:35:fc:74 +# SHA256 Fingerprint: 2d:47:43:7d:e1:79:51:21:5a:12:f3:c5:8e:51:c7:29:a5:80:26:ef:1f:cc:0a:5f:b3:d9:dc:01:2f:60:0d:19 +-----BEGIN CERTIFICATE----- +MIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEc +MBoGA1UEChMTSmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRp +b25DQTAeFw0wNzEyMTIxNTAwMDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYT +AkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zlcm5tZW50MRYwFAYDVQQLEw1BcHBs +aWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp23gdE6H +j6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4fl+K +f5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55 +IrmTwcrNwVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cw +FO5cjFW6WY2H/CPek9AEjP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDiht +QWEjdnjDuGWk81quzMKq2edY3rZ+nYVunyoKb58DKTCXKB28t89UKU5RMfkntigm +/qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRUWssmP3HMlEYNllPqa0jQ +k/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNVBAYTAkpQ +MRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOC +seODvOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADlqRHZ3ODrso2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJ +hyzjVOGjprIIC8CFqMjSnHH2HZ9g/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+ +eKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYDio+nEhEMy/0/ecGc/WLuo89U +DNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmWdupwX3kSa+Sj +B1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL +rosot4LKGAfmt1t06SAZf7IbiVQ= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only +# Subject: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only +# Label: "GeoTrust Primary Certification Authority - G3" +# Serial: 28809105769928564313984085209975885599 +# MD5 Fingerprint: b5:e8:34:36:c9:10:44:58:48:70:6d:2e:83:d4:b8:05 +# SHA1 Fingerprint: 03:9e:ed:b8:0b:e7:a0:3c:69:53:89:3b:20:d2:d9:32:3a:4c:2a:fd +# SHA256 Fingerprint: b4:78:b8:12:25:0d:f8:78:63:5c:2a:a7:ec:7d:15:5e:aa:62:5e:e8:29:16:e2:cd:29:43:61:88:6c:d1:fb:d4 +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT +MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ +BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 +BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz ++uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm +hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn +5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W +JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL +DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC +huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB +AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB +zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN +kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH +SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G +spki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA - G2" +# Serial: 71758320672825410020661621085256472406 +# MD5 Fingerprint: 74:9d:ea:60:24:c4:fd:22:53:3e:cc:3a:72:d9:29:4f +# SHA1 Fingerprint: aa:db:bc:22:23:8f:c4:01:a1:27:bb:38:dd:f4:1d:db:08:9e:f0:12 +# SHA256 Fingerprint: a4:31:0d:50:af:18:a6:44:71:90:37:2a:86:af:af:8b:95:1f:fb:43:1d:83:7f:1e:56:88:b4:59:71:ed:15:57 +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp +IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi +BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw +MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig +YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v +dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ +BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 +papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K +DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 +KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox +XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA - G3" +# Serial: 127614157056681299805556476275995414779 +# MD5 Fingerprint: fb:1b:5d:43:8a:94:cd:44:c6:76:f2:43:4b:47:e7:31 +# SHA1 Fingerprint: f1:8b:53:8d:1b:e9:03:b6:a6:f0:56:43:5b:17:15:89:ca:f3:6b:f2 +# SHA256 Fingerprint: 4b:03:f4:58:07:ad:70:f2:1b:fc:2c:ae:71:c9:fd:e4:60:4c:06:4c:f5:ff:b6:86:ba:e5:db:aa:d7:fd:d3:4c +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB +rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV +BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa +Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl +LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u +MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm +gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 +YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf +b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 +9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S +zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk +OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA +2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW +oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c +KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM +m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu +MdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only +# Subject: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only +# Label: "GeoTrust Primary Certification Authority - G2" +# Serial: 80682863203381065782177908751794619243 +# MD5 Fingerprint: 01:5e:d8:6b:bd:6f:3d:8e:a1:31:f8:12:e0:98:73:6a +# SHA1 Fingerprint: 8d:17:84:d5:37:f3:03:7d:ec:70:fe:57:8b:51:9a:99:e6:10:d7:b0 +# SHA256 Fingerprint: 5e:db:7a:c4:3b:82:a0:6a:87:61:e8:d7:be:49:79:eb:f2:61:1f:7d:d7:9b:f9:1c:1c:6b:56:6a:21:9e:d7:66 +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL +MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj +KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2 +MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw +NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV +BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL +So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal +tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG +CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT +qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz +rD6ogRLQy7rQkgu2npaqBA+K +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Universal Root Certification Authority" +# Serial: 85209574734084581917763752644031726877 +# MD5 Fingerprint: 8e:ad:b5:01:aa:4d:81:e4:8c:1d:d1:e1:14:00:95:19 +# SHA1 Fingerprint: 36:79:ca:35:66:87:72:30:4d:30:a5:fb:87:3b:0f:a7:7b:b7:0d:54 +# SHA256 Fingerprint: 23:99:56:11:27:a5:71:25:de:8c:ef:ea:61:0d:df:2f:a0:78:b5:c8:06:7f:4e:82:82:90:bf:b8:60:e8:4b:3c +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB +vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W +ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0 +IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y +IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh +bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF +9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH +H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H +LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN +/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT +rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw +WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs +exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4 +sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+ +seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz +4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+ +BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR +lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 +7M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Class 3 Public Primary Certification Authority - G4" +# Serial: 63143484348153506665311985501458640051 +# MD5 Fingerprint: 3a:52:e1:e7:fd:6f:3a:e3:6f:f3:6f:99:1b:f9:22:41 +# SHA1 Fingerprint: 22:d5:d8:df:8f:02:31:d1:8d:f7:9d:b7:cf:8a:2d:64:c9:3f:6c:3a +# SHA256 Fingerprint: 69:dd:d7:ea:90:bb:57:c9:3e:13:5d:c8:5e:a6:fc:d5:48:0b:60:32:39:bd:c4:54:fc:75:8b:2a:26:cf:7f:79 +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp +U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg +SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln +biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm +GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve +fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ +aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj +aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW +kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC +4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga +FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Arany (Class Gold) Főtanúsítvány O=NetLock Kft. OU=Tanúsítványkiadók (Certification Services) +# Subject: CN=NetLock Arany (Class Gold) Főtanúsítvány O=NetLock Kft. OU=Tanúsítványkiadók (Certification Services) +# Label: "NetLock Arany (Class Gold) Főtanúsítvány" +# Serial: 80544274841616 +# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 +# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 +# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Issuer: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden +# Subject: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden +# Label: "Staat der Nederlanden Root CA - G2" +# Serial: 10000012 +# MD5 Fingerprint: 7c:a5:0f:f8:5b:9a:7d:6d:30:ae:54:5a:e3:42:a2:8a +# SHA1 Fingerprint: 59:af:82:79:91:86:c7:b4:75:07:cb:cf:03:57:46:eb:04:dd:b7:16 +# SHA256 Fingerprint: 66:8c:83:94:7d:a6:3b:72:4b:ec:e1:74:3c:31:a0:e6:ae:d0:db:8e:c5:b3:1b:e3:77:bb:78:4f:91:b6:71:6f +-----BEGIN CERTIFICATE----- +MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX +DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291 +qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp +uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU +Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE +pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp +5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M +UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN +GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy +5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv +6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK +eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6 +B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/ +BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov +L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG +SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS +CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen +5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897 +IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK +gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL ++63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL +vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm +bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk +N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC +Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z +ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ== +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig O=Disig a.s. +# Subject: CN=CA Disig O=Disig a.s. +# Label: "CA Disig" +# Serial: 1 +# MD5 Fingerprint: 3f:45:96:39:e2:50:87:f7:bb:fe:98:0c:3c:20:98:e6 +# SHA1 Fingerprint: 2a:c8:d5:8b:57:ce:bf:2f:49:af:f2:fc:76:8f:51:14:62:90:7a:41 +# SHA256 Fingerprint: 92:bf:51:19:ab:ec:ca:d0:b1:33:2d:c4:e1:d0:5f:ba:75:b5:67:90:44:ee:0c:a2:6e:93:1f:74:4f:2f:33:cf +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJTSzET +MBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UE +AxMIQ0EgRGlzaWcwHhcNMDYwMzIyMDEzOTM0WhcNMTYwMzIyMDEzOTM0WjBKMQsw +CQYDVQQGEwJTSzETMBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcg +YS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCS9jHBfYj9mQGp2HvycXXxMcbzdWb6UShGhJd4NLxs/LxFWYgmGErE +Nx+hSkS943EE9UQX4j/8SFhvXJ56CbpRNyIjZkMhsDxkovhqFQ4/61HhVKndBpnX +mjxUizkDPw/Fzsbrg3ICqB9x8y34dQjbYkzo+s7552oftms1grrijxaSfQUMbEYD +XcDtab86wYqg6I7ZuUUohwjstMoVvoLdtUSLLa2GDGhibYVW8qwUYzrG0ZmsNHhW +S8+2rT+MitcE5eN4TPWGqvWP+j1scaMtymfraHtuM6kMgiioTGohQBUgDCZbg8Kp +FhXAJIJdKxatymP2dACw30PEEGBWZ2NFAgMBAAGjgf8wgfwwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUjbJJaJ1yCCW5wCf1UJNWSEZx+Y8wDgYDVR0PAQH/BAQD +AgEGMDYGA1UdEQQvMC2BE2Nhb3BlcmF0b3JAZGlzaWcuc2uGFmh0dHA6Ly93d3cu +ZGlzaWcuc2svY2EwZgYDVR0fBF8wXTAtoCugKYYnaHR0cDovL3d3dy5kaXNpZy5z +ay9jYS9jcmwvY2FfZGlzaWcuY3JsMCygKqAohiZodHRwOi8vY2EuZGlzaWcuc2sv +Y2EvY3JsL2NhX2Rpc2lnLmNybDAaBgNVHSAEEzARMA8GDSuBHpGT5goAAAABAQEw +DQYJKoZIhvcNAQEFBQADggEBAF00dGFMrzvY/59tWDYcPQuBDRIrRhCA/ec8J9B6 +yKm2fnQwM6M6int0wHl5QpNt/7EpFIKrIYwvF/k/Ji/1WcbvgAa3mkkp7M5+cTxq +EEHA9tOasnxakZzArFvITV734VP/Q3f8nktnbNfzg9Gg4H8l37iYC5oyOGwwoPP/ +CBUz91BKez6jPiCp3C9WgArtQVCwyfTssuMmRAAOb54GvCKWU3BlxFAKRmukLyeB +EicTXxChds6KezfqwzlhA5WYOudsiCUI/HloDYd9Yvi0X/vF2Ey9WLw/Q1vUHgFN +PGO+I++MzVpQuGhU+QqZMxEA4Z7CRneC9VkGjCFMhwnN5ag= +-----END CERTIFICATE----- + +# Issuer: CN=Juur-SK O=AS Sertifitseerimiskeskus +# Subject: CN=Juur-SK O=AS Sertifitseerimiskeskus +# Label: "Juur-SK" +# Serial: 999181308 +# MD5 Fingerprint: aa:8e:5d:d9:f8:db:0a:58:b7:8d:26:87:6c:82:35:55 +# SHA1 Fingerprint: 40:9d:4b:d9:17:b5:5c:27:b6:9b:64:cb:98:22:44:0d:cd:09:b8:89 +# SHA256 Fingerprint: ec:c3:e9:c3:40:75:03:be:e0:91:aa:95:2f:41:34:8f:f8:8b:aa:86:3b:22:64:be:fa:c8:07:90:15:74:e9:39 +-----BEGIN CERTIFICATE----- +MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcN +AQkBFglwa2lAc2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZp +dHNlZXJpbWlza2Vza3VzMRAwDgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMw +MVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMQsw +CQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEQ +MA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOB +SvZiF3tfTQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkz +ABpTpyHhOEvWgxutr2TC+Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvH +LCu3GFH+4Hv2qEivbDtPL+/40UceJlfwUR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMP +PbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDaTpxt4brNj3pssAki14sL +2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQFMAMBAf8w +ggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwIC +MIHDHoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDk +AGwAagBhAHMAdABhAHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0 +AHMAZQBlAHIAaQBtAGkAcwBrAGUAcwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABz +AGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABrAGkAbgBuAGkAdABhAG0AaQBz +AGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nwcy8wKwYDVR0f +BCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE +FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcY +P2/v6X2+MA4GA1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOi +CfP+JmeaUOTDBS8rNXiRTHyoERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+g +kcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyLabVAyJRld/JXIWY7zoVAtjNjGr95 +HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678IIbsSt4beDI3poHS +na9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkhMp6q +qIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0Z +TbvGRNs2yyqcjg== +-----END CERTIFICATE----- + +# Issuer: CN=Hongkong Post Root CA 1 O=Hongkong Post +# Subject: CN=Hongkong Post Root CA 1 O=Hongkong Post +# Label: "Hongkong Post Root CA 1" +# Serial: 1000 +# MD5 Fingerprint: a8:0d:6f:39:78:b9:43:6d:77:42:6d:98:5a:cc:23:ca +# SHA1 Fingerprint: d6:da:a8:20:8d:09:d2:15:4d:24:b5:2f:cb:34:6e:b2:58:b2:8a:58 +# SHA256 Fingerprint: f9:e6:7d:33:6c:51:00:2a:c0:54:c6:32:02:2d:66:dd:a2:e7:e3:ff:f1:0a:d0:61:ed:31:d8:bb:b4:10:cf:b2 +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx +FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg +Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG +A1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr +b25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ +jVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn +PzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh +ZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9 +nnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h +q5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED +MA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC +mEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3 +7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB +oiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs +EhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO +fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi +AmvZWg== +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign RootCA11 O=Japan Certification Services, Inc. +# Subject: CN=SecureSign RootCA11 O=Japan Certification Services, Inc. +# Label: "SecureSign RootCA11" +# Serial: 1 +# MD5 Fingerprint: b7:52:74:e2:92:b4:80:93:f2:75:e4:cc:d7:f2:ea:26 +# SHA1 Fingerprint: 3b:c4:9f:48:f8:f3:73:a0:9c:1e:bd:f8:5b:b1:c3:65:c7:d8:11:b3 +# SHA256 Fingerprint: bf:0f:ee:fb:9e:3a:58:1a:d5:f9:e9:db:75:89:98:57:43:d2:61:08:5c:4d:31:4f:6f:5d:72:59:aa:42:16:12 +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr +MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG +A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0 +MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp +Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD +QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz +i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8 +h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV +MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9 +UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni +8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC +h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD +VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB +AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm +KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ +X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr +QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5 +pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN +QSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +# Issuer: CN=ACEDICOM Root O=EDICOM OU=PKI +# Subject: CN=ACEDICOM Root O=EDICOM OU=PKI +# Label: "ACEDICOM Root" +# Serial: 7029493972724711941 +# MD5 Fingerprint: 42:81:a0:e2:1c:e3:55:10:de:55:89:42:65:96:22:e6 +# SHA1 Fingerprint: e0:b4:32:2e:b2:f6:a5:68:b6:54:53:84:48:18:4a:50:36:87:43:84 +# SHA256 Fingerprint: 03:95:0f:b4:9a:53:1f:3e:19:91:94:23:98:df:a9:e0:ea:32:d7:ba:1c:dd:9b:c8:5d:b5:7e:d9:40:0b:43:4a +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UE +AwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00x +CzAJBgNVBAYTAkVTMB4XDTA4MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEW +MBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZF +RElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHkWLn7 +09gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7 +XBZXehuDYAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5P +Grjm6gSSrj0RuVFCPYewMYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAK +t0SdE3QrwqXrIhWYENiLxQSfHY9g5QYbm8+5eaA9oiM/Qj9r+hwDezCNzmzAv+Yb +X79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbkHQl/Sog4P75n/TSW9R28 +MHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTTxKJxqvQU +fecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI +2Sf23EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyH +K9caUPgn6C9D4zq92Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEae +ZAwUswdbxcJzbPEHXEUkFDWug/FqTYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAP +BgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz4SsrSbbXc6GqlPUB53NlTKxQ +MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU9QHnc2VMrFAw +RAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv +bS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWIm +fQwng4/F9tqgaHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3 +gvoFNTPhNahXwOf9jU8/kzJPeGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKe +I6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1PwkzQSulgUV1qzOMPPKC8W64iLgpq0i +5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1ThCojz2GuHURwCRi +ipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oIKiMn +MCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZ +o5NjEFIqnxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6 +zqylfDJKZ0DcMDQj3dcEI2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacN +GHk0vFQYXlPKNFHtRQrmjseCNj6nOGOpMCwXEGCSn1WHElkQwg9naRHMTh5+Spqt +r0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3otkYNbn5XOmeUwssfnHdK +Z05phkOTOPu220+DkdRgfks+KzgHVZhepA== +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Label: "Microsec e-Szigno Root CA 2009" +# Serial: 14014712776195784473 +# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 +# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e +# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# Issuer: CN=e-Guven Kok Elektronik Sertifika Hizmet Saglayicisi O=Elektronik Bilgi Guvenligi A.S. +# Subject: CN=e-Guven Kok Elektronik Sertifika Hizmet Saglayicisi O=Elektronik Bilgi Guvenligi A.S. +# Label: "E-Guven Kok Elektronik Sertifika Hizmet Saglayicisi" +# Serial: 91184789765598910059173000485363494069 +# MD5 Fingerprint: 3d:41:29:cb:1e:aa:11:74:cd:5d:b0:62:af:b0:43:5b +# SHA1 Fingerprint: dd:e1:d2:a9:01:80:2e:1d:87:5e:84:b3:80:7e:4b:b1:fd:99:41:34 +# SHA256 Fingerprint: e6:09:07:84:65:a4:19:78:0c:b6:ac:4c:1c:0b:fb:46:53:d9:d9:cc:6e:b3:94:6e:b7:f3:d6:99:97:ba:d5:98 +-----BEGIN CERTIFICATE----- +MIIDtjCCAp6gAwIBAgIQRJmNPMADJ72cdpW56tustTANBgkqhkiG9w0BAQUFADB1 +MQswCQYDVQQGEwJUUjEoMCYGA1UEChMfRWxla3Ryb25payBCaWxnaSBHdXZlbmxp +Z2kgQS5TLjE8MDoGA1UEAxMzZS1HdXZlbiBLb2sgRWxla3Ryb25payBTZXJ0aWZp +a2EgSGl6bWV0IFNhZ2xheWljaXNpMB4XDTA3MDEwNDExMzI0OFoXDTE3MDEwNDEx +MzI0OFowdTELMAkGA1UEBhMCVFIxKDAmBgNVBAoTH0VsZWt0cm9uaWsgQmlsZ2kg +R3V2ZW5saWdpIEEuUy4xPDA6BgNVBAMTM2UtR3V2ZW4gS29rIEVsZWt0cm9uaWsg +U2VydGlmaWthIEhpem1ldCBTYWdsYXlpY2lzaTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMMSIJ6wXgBljU5Gu4Bc6SwGl9XzcslwuedLZYDBS75+PNdU +MZTe1RK6UxYC6lhj71vY8+0qGqpxSKPcEC1fX+tcS5yWCEIlKBHMilpiAVDV6wlT +L/jDj/6z/P2douNffb7tC+Bg62nsM+3YjfsSSYMAyYuXjDtzKjKzEve5TfL0TW3H +5tYmNwjy2f1rXKPlSFxYvEK+A1qBuhw1DADT9SN+cTAIJjjcJRFHLfO6IxClv7wC +90Nex/6wN1CZew+TzuZDLMN+DfIcQ2Zgy2ExR4ejT669VmxMvLz4Bcpk9Ok0oSy1 +c+HCPujIyTQlCFzz7abHlJ+tiEMl1+E5YP6sOVkCAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ/uRLOU1fqRTy7ZVZoE +VtstxNulMA0GCSqGSIb3DQEBBQUAA4IBAQB/X7lTW2M9dTLn+sR0GstG30ZpHFLP +qk/CaOv/gKlR6D1id4k9CnU58W5dF4dvaAXBlGzZXd/aslnLpRCKysw5zZ/rTt5S +/wzw9JKp8mxTq5vSR6AfdPebmvEvFZ96ZDAYBzwqD2fK/A+JYZ1lpTzlvBNbCNvj +/+27BrtqBrF6T2XGgv0enIu1De5Iu7i9qgi0+6N8y5/NkHZchpZ4Vwpm+Vganf2X +KWDeEaaQHBkc7gGWIjQ0LpH5t8Qn0Xvmv/uARFoW5evg1Ao4vOSR49XrXMGs3xtq +fJ7lddK2l4fbzIcrQzqECK+rPNv3PGYxhrCdU3nt+CPeQuMtgvEP5fqX +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" +# Serial: 6047274297262753887 +# MD5 Fingerprint: 73:3a:74:7a:ec:bb:a3:96:a6:c2:e4:e2:c8:9b:c0:c3 +# SHA1 Fingerprint: ae:c5:fb:3f:c8:e1:bf:c4:e5:4f:03:07:5a:9a:e8:00:b7:f7:b6:fa +# SHA256 Fingerprint: 04:04:80:28:bf:1f:28:64:d4:8f:9a:d4:d8:32:94:36:6a:82:88:56:55:3f:3b:14:30:3f:90:14:7f:5d:40:ef +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy +MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD +VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv +ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl +AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF +661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9 +am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1 +ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481 +PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS +3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k +SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF +3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM +ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g +StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz +Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB +jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +# Issuer: CN=Izenpe.com O=IZENPE S.A. +# Subject: CN=Izenpe.com O=IZENPE S.A. +# Label: "Izenpe.com" +# Serial: 917563065490389241595536686991402621 +# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 +# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 +# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- + +# Issuer: CN=Chambers of Commerce Root - 2008 O=AC Camerfirma S.A. +# Subject: CN=Chambers of Commerce Root - 2008 O=AC Camerfirma S.A. +# Label: "Chambers of Commerce Root - 2008" +# Serial: 11806822484801597146 +# MD5 Fingerprint: 5e:80:9e:84:5a:0e:65:0b:17:02:f3:55:18:2a:3e:d7 +# SHA1 Fingerprint: 78:6a:74:ac:76:ab:14:7f:9c:6a:30:50:ba:9e:a8:7e:fe:9a:ce:3c +# SHA256 Fingerprint: 06:3e:4a:fa:c4:91:df:d3:32:f3:08:9b:85:42:e9:46:17:d8:93:d7:fe:94:4e:10:a7:93:7e:e2:9d:96:93:c0 +-----BEGIN CERTIFICATE----- +MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz +IG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz +MTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj +dXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw +EAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp +MCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9 +28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq +VKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q +DuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR +5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL +ZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a +Sd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl +UlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s ++12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5 +Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj +ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx +hduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV +HQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1 ++HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN +YWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t +L2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy +ZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt +IDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV +HSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w +DQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW +PJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF +5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1 +glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH +FoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2 +pSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD +xvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG +tjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq +jktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De +fhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg +OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ +d0jQ +-----END CERTIFICATE----- + +# Issuer: CN=Global Chambersign Root - 2008 O=AC Camerfirma S.A. +# Subject: CN=Global Chambersign Root - 2008 O=AC Camerfirma S.A. +# Label: "Global Chambersign Root - 2008" +# Serial: 14541511773111788494 +# MD5 Fingerprint: 9e:80:ff:78:01:0c:2e:c1:36:bd:fe:96:90:6e:08:f3 +# SHA1 Fingerprint: 4a:bd:ee:ec:95:0d:35:9c:89:ae:c7:52:a1:2c:5b:29:f6:d6:aa:0c +# SHA256 Fingerprint: 13:63:35:43:93:34:a7:69:80:16:a0:d3:24:de:72:28:4e:07:9d:7b:52:20:bb:8f:bd:74:78:16:ee:be:ba:ca +-----BEGIN CERTIFICATE----- +MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD +aGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx +MjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy +cmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG +A1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl +BgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed +KYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7 +G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2 +zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4 +ddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG +HoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2 +Id3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V +yJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e +beksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r +6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh +wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog +zCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW +BBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr +ru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp +ZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk +cmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt +YSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC +CQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI +hvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ +UohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz +X1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x +fxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz +a2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd +Yhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd +SqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O +AP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso +M0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge +v8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z +09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B +-----END CERTIFICATE----- + +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Services Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 +# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f +# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Commercial O=AffirmTrust +# Subject: CN=AffirmTrust Commercial O=AffirmTrust +# Label: "AffirmTrust Commercial" +# Serial: 8608355977964138876 +# MD5 Fingerprint: 82:92:ba:5b:ef:cd:8a:6f:a6:3d:55:f9:84:f6:d6:b7 +# SHA1 Fingerprint: f9:b5:b6:32:45:5f:9c:be:ec:57:5f:80:dc:e9:6e:2c:c7:b2:78:b7 +# SHA256 Fingerprint: 03:76:ab:1d:54:c5:f9:80:3c:e4:b2:e2:01:a0:ee:7e:ef:7b:57:b6:36:e8:a9:3c:9b:8d:48:60:c9:6f:5f:a7 +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Networking O=AffirmTrust +# Subject: CN=AffirmTrust Networking O=AffirmTrust +# Label: "AffirmTrust Networking" +# Serial: 8957382827206547757 +# MD5 Fingerprint: 42:65:ca:be:01:9a:9a:4c:a9:8c:41:49:cd:c0:d5:7f +# SHA1 Fingerprint: 29:36:21:02:8b:20:ed:02:f5:66:c5:32:d1:d6:ed:90:9f:45:00:2f +# SHA256 Fingerprint: 0a:81:ec:5a:92:97:77:f1:45:90:4a:f3:8d:5d:50:9f:66:b5:e2:c5:8f:cd:b5:31:05:8b:0e:17:f3:f0:b4:1b +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Premium O=AffirmTrust +# Subject: CN=AffirmTrust Premium O=AffirmTrust +# Label: "AffirmTrust Premium" +# Serial: 7893706540734352110 +# MD5 Fingerprint: c4:5d:0e:48:b6:ac:28:30:4e:0a:bc:f9:38:16:87:57 +# SHA1 Fingerprint: d8:a6:33:2c:e0:03:6f:b1:85:f6:63:4f:7d:6a:06:65:26:32:28:27 +# SHA256 Fingerprint: 70:a7:3f:7f:37:6b:60:07:42:48:90:45:34:b1:14:82:d5:bf:0e:69:8e:cc:49:8d:f5:25:77:eb:f2:e9:3b:9a +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Premium ECC O=AffirmTrust +# Subject: CN=AffirmTrust Premium ECC O=AffirmTrust +# Label: "AffirmTrust Premium ECC" +# Serial: 8401224907861490260 +# MD5 Fingerprint: 64:b0:09:55:cf:b1:d5:99:e2:be:13:ab:a6:5d:ea:4d +# SHA1 Fingerprint: b8:23:6b:00:2f:1d:16:86:53:01:55:6c:11:a4:37:ca:eb:ff:c3:bb +# SHA256 Fingerprint: bd:71:fd:f6:da:97:e4:cf:62:d1:64:7a:dd:25:81:b0:7d:79:ad:f8:39:7e:b4:ec:ba:9c:5e:84:88:82:14:23 +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA" +# Serial: 279744 +# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 +# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e +# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# Issuer: CN=Certinomis - Autorité Racine O=Certinomis OU=0002 433998903 +# Subject: CN=Certinomis - Autorité Racine O=Certinomis OU=0002 433998903 +# Label: "Certinomis - Autorité Racine" +# Serial: 1 +# MD5 Fingerprint: 7f:30:78:8c:03:e3:ca:c9:0a:e2:c9:ea:1e:aa:55:1a +# SHA1 Fingerprint: 2e:14:da:ec:28:f0:fa:1e:8e:38:9a:4e:ab:eb:26:c0:0a:d3:83:c3 +# SHA256 Fingerprint: fc:bf:e2:88:62:06:f7:2b:27:59:3c:8b:07:02:97:e1:2d:76:9e:d1:0e:d7:93:07:05:a8:09:8e:ff:c1:4d:17 +-----BEGIN CERTIFICATE----- +MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjET +MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAk +BgNVBAMMHUNlcnRpbm9taXMgLSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4 +Mjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNl +cnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYwJAYDVQQDDB1DZXJ0 +aW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jY +F1AMnmHawE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N +8y4oH3DfVS9O7cdxbwlyLu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWe +rP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K +/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92NjMD2AR5vpTESOH2VwnHu +7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9qc1pkIuVC +28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6 +lSTClrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1E +nn1So2+WLhl+HPNbxxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB +0iSVL1N6aaLwD4ZFjliCK0wi1F6g530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql09 +5gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna4NH4+ej9Uji29YnfAgMBAAGj +WzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQN +jLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ +KoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9s +ov3/4gbIOZ/xWqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZM +OH8oMDX/nyNTt7buFHAAQCvaR6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q +619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40nJ+U8/aGH88bc62UeYdocMMzpXDn +2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1BCxMjidPJC+iKunqj +o3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjvJL1v +nxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG +5ERQL1TEqkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWq +pdEdnV1j6CTmNhTih60bWfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZb +dsLLO7XSAPCjDuGtbkD326C00EauFddEwk01+dIL8hf2rGbVJLJP0RyZwG71fet0 +BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/vgt2Fl43N+bYdJeimUV5 +-----END CERTIFICATE----- + +# Issuer: CN=Root CA Generalitat Valenciana O=Generalitat Valenciana OU=PKIGVA +# Subject: CN=Root CA Generalitat Valenciana O=Generalitat Valenciana OU=PKIGVA +# Label: "Root CA Generalitat Valenciana" +# Serial: 994436456 +# MD5 Fingerprint: 2c:8c:17:5e:b1:54:ab:93:17:b5:36:5a:db:d1:c6:f2 +# SHA1 Fingerprint: a0:73:e5:c5:bd:43:61:0d:86:4c:21:13:0a:85:58:57:cc:9c:ea:46 +# SHA256 Fingerprint: 8c:4e:df:d0:43:48:f3:22:96:9e:7e:29:a4:cd:4d:ca:00:46:55:06:1c:16:e1:b0:76:42:2e:f3:42:ad:63:0e +-----BEGIN CERTIFICATE----- +MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJF +UzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJ +R1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcN +MDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3WjBoMQswCQYDVQQGEwJFUzEfMB0G +A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScw +JQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+ +WmmmO3I2F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKj +SgbwJ/BXufjpTjJ3Cj9BZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGl +u6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQD0EbtFpKd71ng+CT516nDOeB0/RSrFOy +A8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXteJajCq+TA81yc477OMUxk +Hl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMBAAGjggM7 +MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBr +aS5ndmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIIC +IwYKKwYBBAG/VQIBADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8A +cgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIA +YQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIAYQBsAGkAdABhAHQAIABWAGEA +bABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQByAGEAYwBpAPMA +bgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA +aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMA +aQBvAG4AYQBtAGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQA +ZQAgAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEA +YwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBuAHQAcgBhACAAZQBuACAAbABhACAA +ZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAAOgAvAC8AdwB3AHcA +LgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0dHA6 +Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+y +eAT8MIGVBgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQsw +CQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0G +A1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVu +Y2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRhTvW1yEICKrNcda3Fbcrn +lD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdzCkj+IHLt +b8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg +9J63NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XF +ducTZnV+ZfsBn5OHiJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmC +IoaZM3Fa6hlXPZHNqcCjbgcTpsnt+GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM= +-----END CERTIFICATE----- + +# Issuer: CN=A-Trust-nQual-03 O=A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH OU=A-Trust-nQual-03 +# Subject: CN=A-Trust-nQual-03 O=A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH OU=A-Trust-nQual-03 +# Label: "A-Trust-nQual-03" +# Serial: 93214 +# MD5 Fingerprint: 49:63:ae:27:f4:d5:95:3d:d8:db:24:86:b8:9c:07:53 +# SHA1 Fingerprint: d3:c0:63:f2:19:ed:07:3e:34:ad:5d:75:0b:32:76:29:ff:d5:9a:f2 +# SHA256 Fingerprint: 79:3c:bf:45:59:b9:fd:e3:8a:b2:2d:f1:68:69:f6:98:81:ae:14:c4:b0:13:9a:c7:88:a7:8a:1a:fc:ca:02:fb +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJB +VDFIMEYGA1UECgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBp +bSBlbGVrdHIuIERhdGVudmVya2VociBHbWJIMRkwFwYDVQQLDBBBLVRydXN0LW5R +dWFsLTAzMRkwFwYDVQQDDBBBLVRydXN0LW5RdWFsLTAzMB4XDTA1MDgxNzIyMDAw +MFoXDTE1MDgxNzIyMDAwMFowgY0xCzAJBgNVBAYTAkFUMUgwRgYDVQQKDD9BLVRy +dXN0IEdlcy4gZi4gU2ljaGVyaGVpdHNzeXN0ZW1lIGltIGVsZWt0ci4gRGF0ZW52 +ZXJrZWhyIEdtYkgxGTAXBgNVBAsMEEEtVHJ1c3QtblF1YWwtMDMxGTAXBgNVBAMM +EEEtVHJ1c3QtblF1YWwtMDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCtPWFuA/OQO8BBC4SAzewqo51ru27CQoT3URThoKgtUaNR8t4j8DRE/5TrzAUj +lUC5B3ilJfYKvUWG6Nm9wASOhURh73+nyfrBJcyFLGM/BWBzSQXgYHiVEEvc+RFZ +znF/QJuKqiTfC0Li21a8StKlDJu3Qz7dg9MmEALP6iPESU7l0+m0iKsMrmKS1GWH +2WrX9IWf5DMiJaXlyDO6w8dB3F/GaswADm0yqLaHNgBid5seHzTLkDx4iHQF63n1 +k3Flyp3HaxgtPVxO59X4PzF9j4fsCiIvI+n+u33J4PTs63zEsMMtYrWacdaxaujs +2e3Vcuy+VwHOBVWf3tFgiBCzAgMBAAGjNjA0MA8GA1UdEwEB/wQFMAMBAf8wEQYD +VR0OBAoECERqlWdVeRFPMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC +AQEAVdRU0VlIXLOThaq/Yy/kgM40ozRiPvbY7meIMQQDbwvUB/tOdQ/TLtPAF8fG +KOwGDREkDg6lXb+MshOWcdzUzg4NCmgybLlBMRmrsQd7TZjTXLDR8KdCoLXEjq/+ +8T/0709GAHbrAvv5ndJAlseIOrifEXnzgGWovR/TeIGgUUw3tKZdJXDRZslo+S4R +FGjxVJgIrCaSD96JntT6s3kr0qN51OyLrIdTaEJMUVF0HhsnLuP1Hyl0Te2v9+GS +mYHovjrHF1D2t8b8m7CKa9aIA5GPBnc6hQLdmNVDeD/GMBWsm2vLV7eJUYs66MmE +DNuxUCAKGkq6ahq97BvIxYSazQ== +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Label: "TWCA Root Certification Authority" +# Serial: 1 +# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 +# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 +# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Label: "Security Communication RootCA2" +# Serial: 0 +# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 +# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 +# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions RootCA 2011" +# Serial: 0 +# MD5 Fingerprint: 73:9f:4c:4b:73:5b:79:e9:fa:ba:1c:ef:6e:cb:d5:c9 +# SHA1 Fingerprint: fe:45:65:9b:79:03:5b:98:a1:61:b5:51:2e:ac:da:58:09:48:22:4d +# SHA256 Fingerprint: bc:10:4f:15:a4:8b:e7:09:dc:a5:42:a7:e1:d4:b9:df:6f:05:45:27:e8:02:ea:a9:2d:59:54:44:25:8a:fe:71 +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix +RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p +YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw +NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK +EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl +cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz +dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ +fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns +bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD +75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP +FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV +HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp +5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu +b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA +A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p +6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7 +dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys +Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI +l7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- + +# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Label: "Actalis Authentication Root CA" +# Serial: 6271844772424770508 +# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 +# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac +# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# Issuer: O=Trustis Limited OU=Trustis FPS Root CA +# Subject: O=Trustis Limited OU=Trustis FPS Root CA +# Label: "Trustis FPS Root CA" +# Serial: 36053640375399034304724988975563710553 +# MD5 Fingerprint: 30:c9:e7:1e:6b:e6:14:eb:65:b2:16:69:20:31:67:4d +# SHA1 Fingerprint: 3b:c0:38:0b:33:c3:f6:a6:0c:86:15:22:93:d9:df:f5:4b:81:c0:04 +# SHA256 Fingerprint: c1:b4:82:99:ab:a5:20:8f:e9:63:0a:ce:55:ca:68:a0:3e:da:5a:51:9c:88:02:a0:d3:a6:73:be:8f:8e:55:7d +-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL +ExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx +MzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc +MBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+ +AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH +iTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj +vSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA +0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB +OrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/ +BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E +FgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01 +GX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW +zaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4 +1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE +f1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F +jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN +ZetX2fNXlrtIzYE= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Subject: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Label: "StartCom Certification Authority" +# Serial: 45 +# MD5 Fingerprint: c9:3b:0d:84:41:fc:a4:76:79:23:08:57:de:10:19:16 +# SHA1 Fingerprint: a3:f1:33:3f:e2:42:bf:cf:c5:d1:4e:8f:39:42:98:40:68:10:d1:a0 +# SHA256 Fingerprint: e1:78:90:ee:09:a3:fb:f4:f4:8b:9c:41:4a:17:d6:37:b7:a5:06:47:e9:bc:75:23:22:72:7f:cc:17:42:a9:11 +-----BEGIN CERTIFICATE----- +MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM3WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFul +F2mHMMo0aEPQQa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCC +ATgwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w +ZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL2ludGVybWVk +aWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENvbW1lcmNpYWwgKFN0 +YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0aGUg +c2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93 +d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgG +CWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5fPGFf59Jb2vKXfuM/gTF +wWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWmN3PH/UvS +Ta0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst +0OcNOrg+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNc +pRJvkrKTlMeIFw6Ttn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKl +CcWw0bdT82AUuoVpaiF8H3VhFyAXe2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVF +P0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA2MFrLH9ZXF2RsXAiV+uKa0hK +1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBsHvUwyKMQ5bLm +KhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE +JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ +8dCAWZvLMdibD4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnm +fyWl8kgAwKQB2j8= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority G2 O=StartCom Ltd. +# Subject: CN=StartCom Certification Authority G2 O=StartCom Ltd. +# Label: "StartCom Certification Authority G2" +# Serial: 59 +# MD5 Fingerprint: 78:4b:fb:9e:64:82:0a:d3:b8:4c:62:f3:64:f2:90:64 +# SHA1 Fingerprint: 31:f1:fd:68:22:63:20:ee:c6:3b:3f:9d:ea:4a:3e:53:7c:7c:39:17 +# SHA256 Fingerprint: c7:ba:65:67:de:93:a7:98:ae:1f:aa:79:1e:71:2d:37:8f:ae:1f:93:c4:39:7f:ea:44:1b:b7:cb:e6:fd:59:95 +-----BEGIN CERTIFICATE----- +MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkgRzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1 +OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoG +A1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRzIwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8Oo1XJ +JZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsD +vfOpL9HG4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnoo +D/Uefyf3lLE3PbfHkffiAez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/ +Q0kGi4xDuFby2X8hQxfqp0iVAXV16iulQ5XqFYSdCI0mblWbq9zSOdIxHWDirMxW +RST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbsO+wmETRIjfaAKxojAuuK +HDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8HvKTlXcxN +nw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM +0D4LnMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/i +UUjXuG+v+E5+M5iSFGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9 +Ha90OrInwMEePnWjFqmveiJdnxMaz6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHg +TuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJKoZIhvcNAQEL +BQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K +2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfX +UfEpY9Z1zRbkJ4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl +6/2o1PXWT6RbdejF0mCy2wl+JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK +9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG/+gyRr61M3Z3qAFdlsHB1b6uJcDJ +HgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTcnIhT76IxW1hPkWLI +wpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/XldblhY +XzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5l +IxKVCCIcl85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoo +hdVddLHRDiBYmxOlsGOm7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulr +so8uBtjRkcfGEvRM/TAXw8HaOFvjqermobp573PYtlNXLfbQ4ddI +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 2 Root CA" +# Serial: 2 +# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 +# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 +# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 3 Root CA" +# Serial: 2 +# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec +# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 +# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 3" +# Serial: 1 +# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef +# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 +# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# Issuer: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus +# Subject: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus +# Label: "EE Certification Centre Root CA" +# Serial: 112324828676200291871926431888494945866 +# MD5 Fingerprint: 43:5e:88:d4:7d:1a:4a:7e:fd:84:2e:52:eb:01:d4:6f +# SHA1 Fingerprint: c9:a8:b9:e7:55:80:5e:58:e3:53:77:a7:25:eb:af:c3:7b:27:cc:d7 +# SHA256 Fingerprint: 3e:84:ba:43:42:90:85:16:e7:75:73:c0:99:2f:09:79:ca:08:4e:46:85:68:1f:f1:95:cc:ba:8a:22:9b:8a:76 +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG +CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy +MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl +ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS +b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy +euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO +bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw +WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d +MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE +1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/ +zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB +BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF +BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV +v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG +E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u +uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW +iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v +GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0= +-----END CERTIFICATE----- + +# Issuer: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. (c) Aralık 2007 +# Subject: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. (c) Aralık 2007 +# Label: "TURKTRUST Certificate Services Provider Root 2007" +# Serial: 1 +# MD5 Fingerprint: 2b:70:20:56:86:82:a0:18:c8:07:53:12:28:70:21:72 +# SHA1 Fingerprint: f1:7f:6f:b6:31:dc:99:e3:a3:c8:7f:fe:1c:f1:81:10:88:d9:60:33 +# SHA256 Fingerprint: 97:8c:d9:66:f2:fa:a0:7b:a7:aa:95:00:d9:c0:2e:9d:77:f2:cd:ad:a6:ad:6b:a7:4a:f4:b9:1c:66:59:3c:50 +-----BEGIN CERTIFICATE----- +MIIEPTCCAyWgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvzE/MD0GA1UEAww2VMOc +UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV4wXAYDVQQKDFVUw5xS +S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg +SGl6bWV0bGVyaSBBLsWeLiAoYykgQXJhbMSxayAyMDA3MB4XDTA3MTIyNTE4Mzcx +OVoXDTE3MTIyMjE4MzcxOVowgb8xPzA9BgNVBAMMNlTDnFJLVFJVU1QgRWxla3Ry +b25payBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTELMAkGA1UEBhMC +VFIxDzANBgNVBAcMBkFua2FyYTFeMFwGA1UECgxVVMOcUktUUlVTVCBCaWxnaSDE +sGxldGnFn2ltIHZlIEJpbGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkgQS7F +ni4gKGMpIEFyYWzEsWsgMjAwNzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKu3PgqMyKVYFeaK7yc9SrToJdPNM8Ig3BnuiD9NYvDdE3ePYakqtdTyuTFY +KTsvP2qcb3N2Je40IIDu6rfwxArNK4aUyeNgsURSsloptJGXg9i3phQvKUmi8wUG ++7RP2qFsmmaf8EMJyupyj+sA1zU511YXRxcw9L6/P8JorzZAwan0qafoEGsIiveG +HtyaKhUG9qPw9ODHFNRRf8+0222vR5YXm3dx2KdxnSQM9pQ/hTEST7ruToK4uT6P +IzdezKKqdfcYbwnTrqdUKDT74eA7YH2gvnmJhsifLfkKS8RQouf9eRbHegsYz85M +733WB2+Y8a+xwXrXgTW4qhe04MsCAwEAAaNCMEAwHQYDVR0OBBYEFCnFkKslrxHk +Yb+j/4hhkeYO/pyBMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQAQDdr4Ouwo0RSVgrESLFF6QSU2TJ/sPx+EnWVUXKgW +AkD6bho3hO9ynYYKVZ1WKKxmLNA6VpM0ByWtCLCPyA8JWcqdmBzlVPi5RX9ql2+I +aE1KBiY3iAIOtsbWcpnOa3faYjGkVh+uX4132l32iPwa2Z61gfAyuOOI0JzzaqC5 +mxRZNTZPz/OOXl0XrRWV2N2y1RVuAE6zS89mlOTgzbUF2mNXi+WzqtvALhyQRNsa +XRik7r4EW5nVcV9VZWRi1aKbBFmGyGJ353yCRWo9F7/snXUMrqNvWtMvmDb08PUZ +qxFdyKbjKlhqQgnDvZImZjINXQhVdP+MmNAKpoRq0Tl9 +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 2009" +# Serial: 623603 +# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f +# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 +# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 EV 2009" +# Serial: 623604 +# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 +# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 +# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Raiz del Estado Venezolano O=Sistema Nacional de Certificacion Electronica OU=Superintendencia de Servicios de Certificacion Electronica +# Subject: CN=PSCProcert O=Sistema Nacional de Certificacion Electronica OU=Proveedor de Certificados PROCERT +# Label: "PSCProcert" +# Serial: 11 +# MD5 Fingerprint: e6:24:e9:12:01:ae:0c:de:8e:85:c4:ce:a3:12:dd:ec +# SHA1 Fingerprint: 70:c1:8d:74:b4:28:81:0a:e4:fd:a5:75:d7:01:9f:99:b0:3d:50:74 +# SHA256 Fingerprint: 3c:fc:3c:14:d1:f6:84:ff:17:e3:8c:43:ca:44:0c:00:b9:67:ec:93:3e:8b:fe:06:4c:a1:d7:2c:90:f2:ad:b0 +-----BEGIN CERTIFICATE----- +MIIJhjCCB26gAwIBAgIBCzANBgkqhkiG9w0BAQsFADCCAR4xPjA8BgNVBAMTNUF1 +dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIFJhaXogZGVsIEVzdGFkbyBWZW5lem9s +YW5vMQswCQYDVQQGEwJWRTEQMA4GA1UEBxMHQ2FyYWNhczEZMBcGA1UECBMQRGlz +dHJpdG8gQ2FwaXRhbDE2MDQGA1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0 +aWZpY2FjaW9uIEVsZWN0cm9uaWNhMUMwQQYDVQQLEzpTdXBlcmludGVuZGVuY2lh +IGRlIFNlcnZpY2lvcyBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9uaWNhMSUwIwYJ +KoZIhvcNAQkBFhZhY3JhaXpAc3VzY2VydGUuZ29iLnZlMB4XDTEwMTIyODE2NTEw +MFoXDTIwMTIyNTIzNTk1OVowgdExJjAkBgkqhkiG9w0BCQEWF2NvbnRhY3RvQHBy +b2NlcnQubmV0LnZlMQ8wDQYDVQQHEwZDaGFjYW8xEDAOBgNVBAgTB01pcmFuZGEx +KjAoBgNVBAsTIVByb3ZlZWRvciBkZSBDZXJ0aWZpY2Fkb3MgUFJPQ0VSVDE2MDQG +A1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9u +aWNhMQswCQYDVQQGEwJWRTETMBEGA1UEAxMKUFNDUHJvY2VydDCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBANW39KOUM6FGqVVhSQ2oh3NekS1wwQYalNo9 +7BVCwfWMrmoX8Yqt/ICV6oNEolt6Vc5Pp6XVurgfoCfAUFM+jbnADrgV3NZs+J74 +BCXfgI8Qhd19L3uA3VcAZCP4bsm+lU/hdezgfl6VzbHvvnpC2Mks0+saGiKLt38G +ieU89RLAu9MLmV+QfI4tL3czkkohRqipCKzx9hEC2ZUWno0vluYC3XXCFCpa1sl9 +JcLB/KpnheLsvtF8PPqv1W7/U0HU9TI4seJfxPmOEO8GqQKJ/+MMbpfg353bIdD0 +PghpbNjU5Db4g7ayNo+c7zo3Fn2/omnXO1ty0K+qP1xmk6wKImG20qCZyFSTXai2 +0b1dCl53lKItwIKOvMoDKjSuc/HUtQy9vmebVOvh+qBa7Dh+PsHMosdEMXXqP+UH +0quhJZb25uSgXTcYOWEAM11G1ADEtMo88aKjPvM6/2kwLkDd9p+cJsmWN63nOaK/ +6mnbVSKVUyqUtd+tFjiBdWbjxywbk5yqjKPK2Ww8F22c3HxT4CAnQzb5EuE8XL1m +v6JpIzi4mWCZDlZTOpx+FIywBm/xhnaQr/2v/pDGj59/i5IjnOcVdo/Vi5QTcmn7 +K2FjiO/mpF7moxdqWEfLcU8UC17IAggmosvpr2uKGcfLFFb14dq12fy/czja+eev +bqQ34gcnAgMBAAGjggMXMIIDEzASBgNVHRMBAf8ECDAGAQH/AgEBMDcGA1UdEgQw +MC6CD3N1c2NlcnRlLmdvYi52ZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAzNi0w +MB0GA1UdDgQWBBRBDxk4qpl/Qguk1yeYVKIXTC1RVDCCAVAGA1UdIwSCAUcwggFD +gBStuyIdxuDSAaj9dlBSk+2YwU2u06GCASakggEiMIIBHjE+MDwGA1UEAxM1QXV0 +b3JpZGFkIGRlIENlcnRpZmljYWNpb24gUmFpeiBkZWwgRXN0YWRvIFZlbmV6b2xh +bm8xCzAJBgNVBAYTAlZFMRAwDgYDVQQHEwdDYXJhY2FzMRkwFwYDVQQIExBEaXN0 +cml0byBDYXBpdGFsMTYwNAYDVQQKEy1TaXN0ZW1hIE5hY2lvbmFsIGRlIENlcnRp +ZmljYWNpb24gRWxlY3Ryb25pY2ExQzBBBgNVBAsTOlN1cGVyaW50ZW5kZW5jaWEg +ZGUgU2VydmljaW9zIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExJTAjBgkq +hkiG9w0BCQEWFmFjcmFpekBzdXNjZXJ0ZS5nb2IudmWCAQowDgYDVR0PAQH/BAQD +AgEGME0GA1UdEQRGMESCDnByb2NlcnQubmV0LnZloBUGBWCGXgIBoAwMClBTQy0w +MDAwMDKgGwYFYIZeAgKgEgwQUklGLUotMzE2MzUzNzMtNzB2BgNVHR8EbzBtMEag +RKBChkBodHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9sY3IvQ0VSVElGSUNBRE8t +UkFJWi1TSEEzODRDUkxERVIuY3JsMCOgIaAfhh1sZGFwOi8vYWNyYWl6LnN1c2Nl +cnRlLmdvYi52ZTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9v +Y3NwLnN1c2NlcnRlLmdvYi52ZTBBBgNVHSAEOjA4MDYGBmCGXgMBAjAsMCoGCCsG +AQUFBwIBFh5odHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9kcGMwDQYJKoZIhvcN +AQELBQADggIBACtZ6yKZu4SqT96QxtGGcSOeSwORR3C7wJJg7ODU523G0+1ng3dS +1fLld6c2suNUvtm7CpsR72H0xpkzmfWvADmNg7+mvTV+LFwxNG9s2/NkAZiqlCxB +3RWGymspThbASfzXg0gTB1GEMVKIu4YXx2sviiCtxQuPcD4quxtxj7mkoP3Yldmv +Wb8lK5jpY5MvYB7Eqvh39YtsL+1+LrVPQA3uvFd359m21D+VJzog1eWuq2w1n8Gh +HVnchIHuTQfiSLaeS5UtQbHh6N5+LwUeaO6/u5BlOsju6rEYNxxik6SgMexxbJHm +pHmJWhSnFFAFTKQAVzAswbVhltw+HoSvOULP5dAssSS830DD7X9jSr3hTxJkhpXz +sOfIt+FTvZLm8wyWuevo5pLtp4EJFAv8lXrPj9Y0TzYS3F7RNHXGRoAvlQSMx4bE +qCaJqD8Zm4G7UaRKhqsLEQ+xrmNTbSjq3TNWOByyrYDT13K9mmyZY+gAu0F2Bbdb +mRiKw7gSXFbPVgx96OLP7bx0R/vu0xdOIk9W/1DzLuY5poLWccret9W6aAjtmcz9 +opLLabid+Qqkpj5PkygqYWwHJgD/ll9ohri4zspV4KuxPX+Y1zMOWj3YeMLEYC/H +YvBhkdI4sPaeVdtAgAUSM84dkpvRabP/v/GSCmE1P93+hvS84Bpxs2Km +-----END CERTIFICATE----- + +# Issuer: CN=China Internet Network Information Center EV Certificates Root O=China Internet Network Information Center +# Subject: CN=China Internet Network Information Center EV Certificates Root O=China Internet Network Information Center +# Label: "China Internet Network Information Center EV Certificates Root" +# Serial: 1218379777 +# MD5 Fingerprint: 55:5d:63:00:97:bd:6a:97:f5:67:ab:4b:fb:6e:63:15 +# SHA1 Fingerprint: 4f:99:aa:93:fb:2b:d1:37:26:a1:99:4a:ce:7f:f0:05:f2:93:5d:1e +# SHA256 Fingerprint: 1c:01:c6:f4:db:b2:fe:fc:22:55:8b:2b:ca:32:56:3f:49:84:4a:cf:c3:2b:7b:e4:b0:ff:59:9f:9e:8c:7a:f7 +-----BEGIN CERTIFICATE----- +MIID9zCCAt+gAwIBAgIESJ8AATANBgkqhkiG9w0BAQUFADCBijELMAkGA1UEBhMC +Q04xMjAwBgNVBAoMKUNoaW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24g +Q2VudGVyMUcwRQYDVQQDDD5DaGluYSBJbnRlcm5ldCBOZXR3b3JrIEluZm9ybWF0 +aW9uIENlbnRlciBFViBDZXJ0aWZpY2F0ZXMgUm9vdDAeFw0xMDA4MzEwNzExMjVa +Fw0zMDA4MzEwNzExMjVaMIGKMQswCQYDVQQGEwJDTjEyMDAGA1UECgwpQ2hpbmEg +SW50ZXJuZXQgTmV0d29yayBJbmZvcm1hdGlvbiBDZW50ZXIxRzBFBgNVBAMMPkNo +aW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24gQ2VudGVyIEVWIENlcnRp +ZmljYXRlcyBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm35z +7r07eKpkQ0H1UN+U8i6yjUqORlTSIRLIOTJCBumD1Z9S7eVnAztUwYyZmczpwA// +DdmEEbK40ctb3B75aDFk4Zv6dOtouSCV98YPjUesWgbdYavi7NifFy2cyjw1l1Vx +zUOFsUcW9SxTgHbP0wBkvUCZ3czY28Sf1hNfQYOL+Q2HklY0bBoQCxfVWhyXWIQ8 +hBouXJE0bhlffxdpxWXvayHG1VA6v2G5BY3vbzQ6sm8UY78WO5upKv23KzhmBsUs +4qpnHkWnjQRmQvaPK++IIGmPMowUc9orhpFjIpryp9vOiYurXccUwVswah+xt54u +gQEC7c+WXmPbqOY4twIDAQABo2MwYTAfBgNVHSMEGDAWgBR8cks5x8DbYqVPm6oY +NJKiyoOCWTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E +FgQUfHJLOcfA22KlT5uqGDSSosqDglkwDQYJKoZIhvcNAQEFBQADggEBACrDx0M3 +j92tpLIM7twUbY8opJhJywyA6vPtI2Z1fcXTIWd50XPFtQO3WKwMVC/GVhMPMdoG +52U7HW8228gd+f2ABsqjPWYWqJ1MFn3AlUa1UeTiH9fqBk1jjZaM7+czV0I664zB +echNdn3e9rG3geCg+aF4RhcaVpjwTj2rHO3sOdwHSPdj/gauwqRcalsyiMXHM4Ws +ZkJHwlgkmeHlPuV1LI5D1l08eB6olYIpUNHRFrrvwb562bTYzB5MRuF3sTGrvSrI +zo9uoV1/A3U05K2JRVRevq4opbs/eHnrc7MKDf2+yfdWrPa37S+bISnHOLaVxATy +wy39FCqQmbkHzJ8= +-----END CERTIFICATE----- + +# Issuer: CN=Swisscom Root CA 2 O=Swisscom OU=Digital Certificate Services +# Subject: CN=Swisscom Root CA 2 O=Swisscom OU=Digital Certificate Services +# Label: "Swisscom Root CA 2" +# Serial: 40698052477090394928831521023204026294 +# MD5 Fingerprint: 5b:04:69:ec:a5:83:94:63:18:a7:86:d0:e4:f2:6e:19 +# SHA1 Fingerprint: 77:47:4f:c6:30:e4:0f:4c:47:64:3f:84:ba:b8:c6:95:4a:8a:41:ec +# SHA256 Fingerprint: f0:9b:12:2c:71:14:f4:a0:9b:d4:ea:4f:4a:99:d5:58:b4:6e:4c:25:cd:81:14:0d:29:c0:56:13:91:4c:38:41 +-----BEGIN CERTIFICATE----- +MIIF2TCCA8GgAwIBAgIQHp4o6Ejy5e/DfEoeWhhntjANBgkqhkiG9w0BAQsFADBk +MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 +YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg +Q0EgMjAeFw0xMTA2MjQwODM4MTRaFw0zMTA2MjUwNzM4MTRaMGQxCzAJBgNVBAYT +AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp +Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAyMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlUJOhJ1R5tMJ6HJaI2nbeHCOFvEr +jw0DzpPMLgAIe6szjPTpQOYXTKueuEcUMncy3SgM3hhLX3af+Dk7/E6J2HzFZ++r +0rk0X2s682Q2zsKwzxNoysjL67XiPS4h3+os1OD5cJZM/2pYmLcX5BtS5X4HAB1f +2uY+lQS3aYg5oUFgJWFLlTloYhyxCwWJwDaCFCE/rtuh/bxvHGCGtlOUSbkrRsVP +ACu/obvLP+DHVxxX6NZp+MEkUp2IVd3Chy50I9AU/SpHWrumnf2U5NGKpV+GY3aF +y6//SSj8gO1MedK75MDvAe5QQQg1I3ArqRa0jG6F6bYRzzHdUyYb3y1aSgJA/MTA +tukxGggo5WDDH8SQjhBiYEQN7Aq+VRhxLKX0srwVYv8c474d2h5Xszx+zYIdkeNL +6yxSNLCK/RJOlrDrcH+eOfdmQrGrrFLadkBXeyq96G4DsguAhYidDMfCd7Camlf0 +uPoTXGiTOmekl9AbmbeGMktg2M7v0Ax/lZ9vh0+Hio5fCHyqW/xavqGRn1V9TrAL +acywlKinh/LTSlDcX3KwFnUey7QYYpqwpzmqm59m2I2mbJYV4+by+PGDYmy7Velh +k6M99bFXi08jsJvllGov34zflVEpYKELKeRcVVi3qPyZ7iVNTA6z00yPhOgpD/0Q +VAKFyPnlw4vP5w8CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw +FDASBgdghXQBUwIBBgdghXQBUwIBMBIGA1UdEwEB/wQIMAYBAf8CAQcwHQYDVR0O +BBYEFE0mICKJS9PVpAqhb97iEoHF8TwuMB8GA1UdIwQYMBaAFE0mICKJS9PVpAqh +b97iEoHF8TwuMA0GCSqGSIb3DQEBCwUAA4ICAQAyCrKkG8t9voJXiblqf/P0wS4R +fbgZPnm3qKhyN2abGu2sEzsOv2LwnN+ee6FTSA5BesogpxcbtnjsQJHzQq0Qw1zv +/2BZf82Fo4s9SBwlAjxnffUy6S8w5X2lejjQ82YqZh6NM4OKb3xuqFp1mrjX2lhI +REeoTPpMSQpKwhI3qEAMw8jh0FcNlzKVxzqfl9NX+Ave5XLzo9v/tdhZsnPdTSpx +srpJ9csc1fV5yJmz/MFMdOO0vSk3FQQoHt5FRnDsr7p4DooqzgB53MBfGWcsa0vv +aGgLQ+OswWIJ76bdZWGgr4RVSJFSHMYlkSrQwSIjYVmvRRGFHQEkNI/Ps/8XciAT +woCqISxxOQ7Qj1zB09GOInJGTB2Wrk9xseEFKZZZ9LuedT3PDTcNYtsmjGOpI99n +Bjx8Oto0QuFmtEYE3saWmA9LSHokMnWRn6z3aOkquVVlzl1h0ydw2Df+n7mvoC5W +t6NlUe07qxS/TFED6F+KBZvuim6c779o+sjaC+NCydAXFJy3SuCvkychVSa1ZC+N +8f+mQAWFBVzKBxlcCxMoTFh/wqXvRdpg065lYZ1Tg3TCrvJcwhbtkj6EPnNgiLx2 +9CzP0H1907he0ZESEOnN3col49XtmS++dYFLJPlFRpTJKSFTnCZFqhMX5OfNeOI5 +wSsSnqaeG8XmDtkx2Q== +-----END CERTIFICATE----- + +# Issuer: CN=Swisscom Root EV CA 2 O=Swisscom OU=Digital Certificate Services +# Subject: CN=Swisscom Root EV CA 2 O=Swisscom OU=Digital Certificate Services +# Label: "Swisscom Root EV CA 2" +# Serial: 322973295377129385374608406479535262296 +# MD5 Fingerprint: 7b:30:34:9f:dd:0a:4b:6b:35:ca:31:51:28:5d:ae:ec +# SHA1 Fingerprint: e7:a1:90:29:d3:d5:52:dc:0d:0f:c6:92:d3:ea:88:0d:15:2e:1a:6b +# SHA256 Fingerprint: d9:5f:ea:3c:a4:ee:dc:e7:4c:d7:6e:75:fc:6d:1f:f6:2c:44:1f:0f:a8:bc:77:f0:34:b1:9e:5d:b2:58:01:5d +-----BEGIN CERTIFICATE----- +MIIF4DCCA8igAwIBAgIRAPL6ZOJ0Y9ON/RAdBB92ylgwDQYJKoZIhvcNAQELBQAw +ZzELMAkGA1UEBhMCY2gxETAPBgNVBAoTCFN3aXNzY29tMSUwIwYDVQQLExxEaWdp +dGFsIENlcnRpZmljYXRlIFNlcnZpY2VzMR4wHAYDVQQDExVTd2lzc2NvbSBSb290 +IEVWIENBIDIwHhcNMTEwNjI0MDk0NTA4WhcNMzEwNjI1MDg0NTA4WjBnMQswCQYD +VQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2Vy +dGlmaWNhdGUgU2VydmljZXMxHjAcBgNVBAMTFVN3aXNzY29tIFJvb3QgRVYgQ0Eg +MjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMT3HS9X6lds93BdY7Bx +UglgRCgzo3pOCvrY6myLURYaVa5UJsTMRQdBTxB5f3HSek4/OE6zAMaVylvNwSqD +1ycfMQ4jFrclyxy0uYAyXhqdk/HoPGAsp15XGVhRXrwsVgu42O+LgrQ8uMIkqBPH +oCE2G3pXKSinLr9xJZDzRINpUKTk4RtiGZQJo/PDvO/0vezbE53PnUgJUmfANykR +HvvSEaeFGHR55E+FFOtSN+KxRdjMDUN/rhPSays/p8LiqG12W0OfvrSdsyaGOx9/ +5fLoZigWJdBLlzin5M8J0TbDC77aO0RYjb7xnglrPvMyxyuHxuxenPaHZa0zKcQv +idm5y8kDnftslFGXEBuGCxobP/YCfnvUxVFkKJ3106yDgYjTdLRZncHrYTNaRdHL +OdAGalNgHa/2+2m8atwBz735j9m9W8E6X47aD0upm50qKGsaCnw8qyIL5XctcfaC +NYGu+HuB5ur+rPQam3Rc6I8k9l2dRsQs0h4rIWqDJ2dVSqTjyDKXZpBy2uPUZC5f +46Fq9mDU5zXNysRojddxyNMkM3OxbPlq4SjbX8Y96L5V5jcb7STZDxmPX2MYWFCB +UWVv8p9+agTnNCRxunZLWB4ZvRVgRaoMEkABnRDixzgHcgplwLa7JSnaFp6LNYth +7eVxV4O1PHGf40+/fh6Bn0GXAgMBAAGjgYYwgYMwDgYDVR0PAQH/BAQDAgGGMB0G +A1UdIQQWMBQwEgYHYIV0AVMCAgYHYIV0AVMCAjASBgNVHRMBAf8ECDAGAQH/AgED +MB0GA1UdDgQWBBRF2aWBbj2ITY1x0kbBbkUe88SAnTAfBgNVHSMEGDAWgBRF2aWB +bj2ITY1x0kbBbkUe88SAnTANBgkqhkiG9w0BAQsFAAOCAgEAlDpzBp9SSzBc1P6x +XCX5145v9Ydkn+0UjrgEjihLj6p7jjm02Vj2e6E1CqGdivdj5eu9OYLU43otb98T +PLr+flaYC/NUn81ETm484T4VvwYmneTwkLbUwp4wLh/vx3rEUMfqe9pQy3omywC0 +Wqu1kx+AiYQElY2NfwmTv9SoqORjbdlk5LgpWgi/UOGED1V7XwgiG/W9mR4U9s70 +WBCCswo9GcG/W6uqmdjyMb3lOGbcWAXH7WMaLgqXfIeTK7KK4/HsGOV1timH59yL +Gn602MnTihdsfSlEvoqq9X46Lmgxk7lq2prg2+kupYTNHAq4Sgj5nPFhJpiTt3tm +7JFe3VE/23MPrQRYCd0EApUKPtN236YQHoA96M2kZNEzx5LH4k5E4wnJTsJdhw4S +nr8PyQUQ3nqjsTzyP6WqJ3mtMX0f/fwZacXduT98zca0wjAefm6S139hdlqP65VN +vBFuIXxZN5nQBrz5Bm0yFqXZaajh3DyAHmBR3NdUIR7KYndP+tiPsys6DXhyyWhB +WkdKwqPrGtcKqzwyVcgKEZzfdNbwQBUdyLmPtTbFr/giuMod89a2GQ+fYWVq6nTI +fI/DT11lgh/ZDYnadXL77/FHZxOzyNEZiCcmmpl5fx7kLD977vHeTYuWl8PVP3wb +I+2ksx0WckNLIOFZfsLorSa/ovc= +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R1 O=Disig a.s. +# Subject: CN=CA Disig Root R1 O=Disig a.s. +# Label: "CA Disig Root R1" +# Serial: 14052245610670616104 +# MD5 Fingerprint: be:ec:11:93:9a:f5:69:21:bc:d7:c1:c0:67:89:cc:2a +# SHA1 Fingerprint: 8e:1c:74:f8:a6:20:b9:e5:8a:f4:61:fa:ec:2b:47:56:51:1a:52:c6 +# SHA256 Fingerprint: f9:6f:23:f4:c3:e7:9c:07:7a:46:98:8d:5a:f5:90:06:76:a0:f0:39:cb:64:5d:d1:75:49:b2:16:c8:24:40:ce +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAMMDmu5QkG4oMA0GCSqGSIb3DQEBBQUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIxMB4XDTEyMDcxOTA5MDY1NloXDTQy +MDcxOTA5MDY1NlowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjEw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqw3j33Jijp1pedxiy3QRk +D2P9m5YJgNXoqqXinCaUOuiZc4yd39ffg/N4T0Dhf9Kn0uXKE5Pn7cZ3Xza1lK/o +OI7bm+V8u8yN63Vz4STN5qctGS7Y1oprFOsIYgrY3LMATcMjfF9DCCMyEtztDK3A +fQ+lekLZWnDZv6fXARz2m6uOt0qGeKAeVjGu74IKgEH3G8muqzIm1Cxr7X1r5OJe +IgpFy4QxTaz+29FHuvlglzmxZcfe+5nkCiKxLU3lSCZpq+Kq8/v8kiky6bM+TR8n +oc2OuRf7JT7JbvN32g0S9l3HuzYQ1VTW8+DiR0jm3hTaYVKvJrT1cU/J19IG32PK +/yHoWQbgCNWEFVP3Q+V8xaCJmGtzxmjOZd69fwX3se72V6FglcXM6pM6vpmumwKj +rckWtc7dXpl4fho5frLABaTAgqWjR56M6ly2vGfb5ipN0gTco65F97yLnByn1tUD +3AjLLhbKXEAz6GfDLuemROoRRRw1ZS0eRWEkG4IupZ0zXWX4Qfkuy5Q/H6MMMSRE +7cderVC6xkGbrPAXZcD4XW9boAo0PO7X6oifmPmvTiT6l7Jkdtqr9O3jw2Dv1fkC +yC2fg69naQanMVXVz0tv/wQFx1isXxYb5dKj6zHbHzMVTdDypVP1y+E9Tmgt2BLd +qvLmTZtJ5cUoobqwWsagtQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUiQq0OJMa5qvum5EY+fU8PjXQ04IwDQYJKoZI +hvcNAQEFBQADggIBADKL9p1Kyb4U5YysOMo6CdQbzoaz3evUuii+Eq5FLAR0rBNR +xVgYZk2C2tXck8An4b58n1KeElb21Zyp9HWc+jcSjxyT7Ff+Bw+r1RL3D65hXlaA +SfX8MPWbTx9BLxyE04nH4toCdu0Jz2zBuByDHBb6lM19oMgY0sidbvW9adRtPTXo +HqJPYNcHKfyyo6SdbhWSVhlMCrDpfNIZTUJG7L399ldb3Zh+pE3McgODWF3vkzpB +emOqfDqo9ayk0d2iLbYq/J8BjuIQscTK5GfbVSUZP/3oNn6z4eGBrxEWi1CXYBmC +AMBrTXO40RMHPuq2MU/wQppt4hF05ZSsjYSVPCGvxdpHyN85YmLLW1AL14FABZyb +7bq2ix4Eb5YgOe2kfSnbSM6C3NQCjR0EMVrHS/BsYVLXtFHCgWzN4funodKSds+x +DzdYpPJScWc/DIh4gInByLUfkmO+p3qKViwaqKactV2zY9ATIKHrkWzQjX2v3wvk +F7mGnjixlAxYjOBVqjtjbZqJYLhkKpLGN/R+Q0O3c+gB53+XD9fyexn9GtePyfqF +a3qdnom2piiZk4hA9z7NUaPK6u95RyG1/jLix8NRb76AdPCkwzryT+lf3xkK8jsT +Q6wxpLPn6/wY1gGp8yqPNg7rtLG8t0zJa7+h89n07eLw4+1knj0vllJPgFOL +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R2 O=Disig a.s. +# Subject: CN=CA Disig Root R2 O=Disig a.s. +# Label: "CA Disig Root R2" +# Serial: 10572350602393338211 +# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 +# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 +# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Label: "ACCVRAIZ1" +# Serial: 6828503384748696800 +# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 +# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 +# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA Global Root CA" +# Serial: 3262 +# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 +# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 +# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# Issuer: CN=TeliaSonera Root CA v1 O=TeliaSonera +# Subject: CN=TeliaSonera Root CA v1 O=TeliaSonera +# Label: "TeliaSonera Root CA v1" +# Serial: 199041966741090107964904287217786801558 +# MD5 Fingerprint: 37:41:49:1b:18:56:9a:26:f5:ad:c2:66:fb:40:a5:4c +# SHA1 Fingerprint: 43:13:bb:96:f1:d5:86:9b:c1:4e:6a:92:f6:cf:f6:34:69:87:82:37 +# SHA256 Fingerprint: dd:69:36:fe:21:f8:f0:77:c1:23:a1:a5:21:c1:22:24:f7:22:55:b7:3e:03:a7:26:06:93:e8:a2:4b:0f:a3:89 +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +# Issuer: CN=E-Tugra Certification Authority O=E-Tuğra EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. OU=E-Tugra Sertifikasyon Merkezi +# Subject: CN=E-Tugra Certification Authority O=E-Tuğra EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. OU=E-Tugra Sertifikasyon Merkezi +# Label: "E-Tugra Certification Authority" +# Serial: 7667447206703254355 +# MD5 Fingerprint: b8:a1:03:63:b0:bd:21:71:70:8a:6f:13:3a:bb:79:49 +# SHA1 Fingerprint: 51:c6:e7:08:49:06:6e:f3:92:d4:5c:a0:0d:6d:a3:62:8f:c3:52:39 +# SHA256 Fingerprint: b0:bf:d5:2b:b0:d7:d9:bd:92:bf:5d:4d:c1:3d:a2:55:c0:2c:54:2f:37:83:65:ea:89:39:11:f5:5e:55:f2:3c +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV +BAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC +aWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV +BAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1 +Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz +MDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+ +BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp +em1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY +B4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH +D5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF +Q9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo +q1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D +k14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH +fC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut +dEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM +ti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8 +zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX +U8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6 +Jyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5 +XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF +Nzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR +HTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY +GwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c +77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3 ++GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK +vJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6 +FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl +yb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P +AJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD +y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d +NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 2" +# Serial: 1 +# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a +# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 +# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot 2011 O=Atos +# Subject: CN=Atos TrustedRoot 2011 O=Atos +# Label: "Atos TrustedRoot 2011" +# Serial: 6643877497813316402 +# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 +# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 +# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 1 G3" +# Serial: 687049649626669250736271037606554624078720034195 +# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab +# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 +# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2 G3" +# Serial: 390156079458959257446133169266079962026824725800 +# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 +# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 +# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3 G3" +# Serial: 268090761170461462463995952157327242137089239581 +# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 +# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d +# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G2" +# Serial: 15385348160840213938643033620894905419 +# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d +# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f +# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G3" +# Serial: 15459312981008553731928384953135426796 +# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb +# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 +# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G2" +# Serial: 4293743540046975378534879503202253541 +# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 +# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 +# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G3" +# Serial: 7089244469030293291760083333884364146 +# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca +# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e +# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Trusted Root G4" +# Serial: 7451500558977370777930084869016614236 +# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 +# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 +# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# Issuer: CN=Certification Authority of WoSign O=WoSign CA Limited +# Subject: CN=Certification Authority of WoSign O=WoSign CA Limited +# Label: "WoSign" +# Serial: 125491772294754854453622855443212256657 +# MD5 Fingerprint: a1:f2:f9:b5:d2:c8:7a:74:b8:f3:05:f1:d7:e1:84:8d +# SHA1 Fingerprint: b9:42:94:bf:91:ea:8f:b6:4b:e6:10:97:c7:fb:00:13:59:b6:76:cb +# SHA256 Fingerprint: 4b:22:d5:a6:ae:c9:9f:3c:db:79:aa:5e:c0:68:38:47:9c:d5:ec:ba:71:64:f7:f2:2d:c1:d6:5f:63:d8:57:08 +-----BEGIN CERTIFICATE----- +MIIFdjCCA16gAwIBAgIQXmjWEXGUY1BWAGjzPsnFkTANBgkqhkiG9w0BAQUFADBV +MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNV +BAMTIUNlcnRpZmljYXRpb24gQXV0aG9yaXR5IG9mIFdvU2lnbjAeFw0wOTA4MDgw +MTAwMDFaFw0zOTA4MDgwMTAwMDFaMFUxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFX +b1NpZ24gQ0EgTGltaXRlZDEqMCgGA1UEAxMhQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgb2YgV29TaWduMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvcqN +rLiRFVaXe2tcesLea9mhsMMQI/qnobLMMfo+2aYpbxY94Gv4uEBf2zmoAHqLoE1U +fcIiePyOCbiohdfMlZdLdNiefvAA5A6JrkkoRBoQmTIPJYhTpA2zDxIIFgsDcScc +f+Hb0v1naMQFXQoOXXDX2JegvFNBmpGN9J42Znp+VsGQX+axaCA2pIwkLCxHC1l2 +ZjC1vt7tj/id07sBMOby8w7gLJKA84X5KIq0VC6a7fd2/BVoFutKbOsuEo/Uz/4M +x1wdC34FMr5esAkqQtXJTpCzWQ27en7N1QhatH/YHGkR+ScPewavVIMYe+HdVHpR +aG53/Ma/UkpmRqGyZxq7o093oL5d//xWC0Nyd5DKnvnyOfUNqfTq1+ezEC8wQjch +zDBwyYaYD8xYTYO7feUapTeNtqwylwA6Y3EkHp43xP901DfA4v6IRmAR3Qg/UDar +uHqklWJqbrDKaiFaafPz+x1wOZXzp26mgYmhiMU7ccqjUu6Du/2gd/Tkb+dC221K +mYo0SLwX3OSACCK28jHAPwQ+658geda4BmRkAjHXqc1S+4RFaQkAKtxVi8QGRkvA +Sh0JWzko/amrzgD5LkhLJuYwTKVYyrREgk/nkR4zw7CT/xH8gdLKH3Ep3XZPkiWv +HYG3Dy+MwwbMLyejSuQOmbp8HkUff6oZRZb9/D0CAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOFmzw7R8bNLtwYgFP6H +EtX2/vs+MA0GCSqGSIb3DQEBBQUAA4ICAQCoy3JAsnbBfnv8rWTjMnvMPLZdRtP1 +LOJwXcgu2AZ9mNELIaCJWSQBnfmvCX0KI4I01fx8cpm5o9dU9OpScA7F9dY74ToJ +MuYhOZO9sxXqT2r09Ys/L3yNWC7F4TmgPsc9SnOeQHrAK2GpZ8nzJLmzbVUsWh2e +JXLOC62qx1ViC777Y7NhRCOjy+EaDveaBk3e1CNOIZZbOVtXHS9dCF4Jef98l7VN +g64N1uajeeAz0JmWAjCnPv/So0M/BVoG6kQC2nz4SNAzqfkHx5Xh9T71XXG68pWp +dIhhWeO/yloTunK0jF02h+mmxTwTv97QRCbut+wucPrXnbes5cVAWubXbHssw1ab +R80LzvobtCHXt2a49CUwi1wNuepnsvRtrtWhnk/Yn+knArAdBtaP4/tIEp9/EaEQ +PkxROpaw0RPxx9gmrjrKkcRpnd8BKWRRb2jaFOwIQZeQjdCygPLPwj2/kWjFgGce +xGATVdVhmVd8upUPYUk6ynW8yQqTP2cOEvIo4jEbwFcW3wh8GcF+Dx+FHgo2fFt+ +J7x6v+Db9NpSvd4MVHAxkUOVyLzwPt0JfjBkUO1/AaQzZ01oT74V77D2AhGiGxMl +OtzCWfHjXEa7ZywCRuoeSKbmW9m1vFGikpbbqsY3Iqb+zCB0oy2pLmvLwIIRIbWT +ee5Ehr7XHuQe+w== +-----END CERTIFICATE----- + +# Issuer: CN=CA 沃通根证书 O=WoSign CA Limited +# Subject: CN=CA 沃通根证书 O=WoSign CA Limited +# Label: "WoSign China" +# Serial: 106921963437422998931660691310149453965 +# MD5 Fingerprint: 78:83:5b:52:16:76:c4:24:3b:83:78:e8:ac:da:9a:93 +# SHA1 Fingerprint: 16:32:47:8d:89:f9:21:3a:92:00:85:63:f5:a4:a7:d3:12:40:8a:d6 +# SHA256 Fingerprint: d6:f0:34:bd:94:aa:23:3f:02:97:ec:a4:24:5b:28:39:73:e4:47:aa:59:0f:31:0c:77:f4:8f:df:83:11:22:54 +-----BEGIN CERTIFICATE----- +MIIFWDCCA0CgAwIBAgIQUHBrzdgT/BtOOzNy0hFIjTANBgkqhkiG9w0BAQsFADBG +MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxGzAZBgNV +BAMMEkNBIOayg+mAmuagueivgeS5pjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgw +MTAwMDFaMEYxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFXb1NpZ24gQ0EgTGltaXRl +ZDEbMBkGA1UEAwwSQ0Eg5rKD6YCa5qC56K+B5LmmMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA0EkhHiX8h8EqwqzbdoYGTufQdDTc7WU1/FDWiD+k8H/r +D195L4mx/bxjWDeTmzj4t1up+thxx7S8gJeNbEvxUNUqKaqoGXqW5pWOdO2XCld1 +9AXbbQs5uQF/qvbW2mzmBeCkTVL829B0txGMe41P/4eDrv8FAxNXUDf+jJZSEExf +v5RxadmWPgxDT74wwJ85dE8GRV2j1lY5aAfMh09Qd5Nx2UQIsYo06Yms25tO4dnk +UkWMLhQfkWsZHWgpLFbE4h4TV2TwYeO5Ed+w4VegG63XX9Gv2ystP9Bojg/qnw+L +NVgbExz03jWhCl3W6t8Sb8D7aQdGctyB9gQjF+BNdeFyb7Ao65vh4YOhn0pdr8yb ++gIgthhid5E7o9Vlrdx8kHccREGkSovrlXLp9glk3Kgtn3R46MGiCWOc76DbT52V +qyBPt7D3h1ymoOQ3OMdc4zUPLK2jgKLsLl3Az+2LBcLmc272idX10kaO6m1jGx6K +yX2m+Jzr5dVjhU1zZmkR/sgO9MHHZklTfuQZa/HpelmjbX7FF+Ynxu8b22/8DU0G +AbQOXDBGVWCvOGU6yke6rCzMRh+yRpY/8+0mBe53oWprfi1tWFxK1I5nuPHa1UaK +J/kR8slC/k7e3x9cxKSGhxYzoacXGKUN5AXlK8IrC6KVkLn9YDxOiT7nnO4fuwEC +AwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFOBNv9ybQV0T6GTwp+kVpOGBwboxMA0GCSqGSIb3DQEBCwUAA4ICAQBqinA4 +WbbaixjIvirTthnVZil6Xc1bL3McJk6jfW+rtylNpumlEYOnOXOvEESS5iVdT2H6 +yAa+Tkvv/vMx/sZ8cApBWNromUuWyXi8mHwCKe0JgOYKOoICKuLJL8hWGSbueBwj +/feTZU7n85iYr83d2Z5AiDEoOqsuC7CsDCT6eiaY8xJhEPRdF/d+4niXVOKM6Cm6 +jBAyvd0zaziGfjk9DgNyp115j0WKWa5bIW4xRtVZjc8VX90xJc/bYNaBRHIpAlf2 +ltTW/+op2znFuCyKGo3Oy+dCMYYFaA6eFN0AkLppRQjbbpCBhqcqBT/mhDn4t/lX +X0ykeVoQDF7Va/81XwVRHmyjdanPUIPTfPRm94KNPQx96N97qA4bLJyuQHCH2u2n +FoJavjVsIE4iYdm8UXrNemHcSxH5/mc0zy4EZmFcV5cjjPOGG0jfKq+nwf/Yjj4D +u9gqsPoUJbJRa4ZDhS4HIxaAjUz7tGM7zMN07RujHv41D198HRaG9Q7DlfEvr10l +O1Hm13ZBONFLAzkopR6RctR9q5czxNM+4Gm2KHmgCY0c0f9BckgG/Jou5yD5m6Le +ie2uPAmvylezkolwQOQvT8Jwg0DXJCxr5wkf09XHwQj02w47HAcLQxGEIYbpgNR1 +2KvxAmLBsX5VYc8T1yaw15zLKYs4SgsOkI26oQ== +-----END CERTIFICATE----- + +` diff --git a/vendor/github.com/camlistore/camlistore/pkg/httputil/certs_test.go b/vendor/github.com/camlistore/camlistore/pkg/httputil/certs_test.go new file mode 100644 index 00000000..40836577 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/httputil/certs_test.go @@ -0,0 +1,23 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package httputil + +import "testing" + +func TestSystemCARootsAvailable(t *testing.T) { + t.Logf("Roots available = %v", SystemCARootsAvailable()) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/httputil/faketransport.go b/vendor/github.com/camlistore/camlistore/pkg/httputil/faketransport.go new file mode 100644 index 00000000..fac64eb5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/httputil/faketransport.go @@ -0,0 +1,110 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package httputil + +import ( + "bufio" + "fmt" + "net/http" + "os" + "regexp" + "strings" + + "camlistore.org/pkg/types" +) + +// NewFakeTransport takes a map of URL to function generating a response +// and returns an http.RoundTripper that does HTTP requests out of that. +func NewFakeTransport(urls map[string]func() *http.Response) http.RoundTripper { + return fakeTransport(urls) +} + +type fakeTransport map[string]func() *http.Response + +func (m fakeTransport) RoundTrip(req *http.Request) (res *http.Response, err error) { + urls := req.URL.String() + fn, ok := m[urls] + if !ok { + return nil, fmt.Errorf("Unexpected FakeTransport URL requested: %s", urls) + } + return fn(), nil +} + +// Matcher describes a regular expression and the function that will +// be used if that regular expression is matched. +type Matcher struct { + URLRegex string // will be compiled and matched against URLs + Fn func() *http.Response // function that will be run if URLRegex matches +} + +// NewRegexpFakeTransport takes a slice of Matchers and returns an +// http.RoundTripper that will apply the function associated with the +// first UrlRegex that matches. +func NewRegexpFakeTransport(allMatchers []*Matcher) (http.RoundTripper, error) { + var result regexpFakeTransport = []*regexPair{} + for _, matcher := range allMatchers { + r, err := regexp.Compile(matcher.URLRegex) + if err != nil { + return nil, err + } + pair := regexPair{r, matcher.Fn} + result = append(result, &pair) + } + return result, nil +} + +type regexPair struct { + r *regexp.Regexp + fn func() *http.Response +} + +type regexpFakeTransport []*regexPair + +func (rft regexpFakeTransport) RoundTrip(req *http.Request) (*http.Response, error) { + s := req.URL.String() + for _, p := range rft { + if p.r.MatchString(s) { + return p.fn(), nil + } + } + return nil, fmt.Errorf("Unexpected RegexpFakeTransport URL requested: %s", s) +} + +// FileResponder returns an HTTP response generator that returns the +// contents of the named file. +func FileResponder(filename string) func() *http.Response { + return func() *http.Response { + f, err := os.Open(filename) + if err != nil { + return &http.Response{StatusCode: 404, Status: "404 Not Found", Body: types.EmptyBody} + } + return &http.Response{StatusCode: 200, Status: "200 OK", Body: f} + } +} + +// StaticResponder returns an HTTP response generator that parses res +// for an entire HTTP response, including headers and body. +func StaticResponder(res string) func() *http.Response { + _, err := http.ReadResponse(bufio.NewReader(strings.NewReader(res)), nil) + if err != nil { + panic("Invalid response given to StaticResponder: " + err.Error()) + } + return func() *http.Response { + res, _ := http.ReadResponse(bufio.NewReader(strings.NewReader(res)), nil) + return res + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/httputil/httputil.go b/vendor/github.com/camlistore/camlistore/pkg/httputil/httputil.go new file mode 100644 index 00000000..6702d13f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/httputil/httputil.go @@ -0,0 +1,337 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package httputil contains a bunch of HTTP utility code, some generic, +// and some Camlistore-specific. +package httputil + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net" + "net/http" + "net/url" + "path" + "strconv" + "strings" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/env" +) + +// IsGet reports whether r.Method is a GET or HEAD request. +func IsGet(r *http.Request) bool { + return r.Method == "GET" || r.Method == "HEAD" +} + +func ErrorRouting(rw http.ResponseWriter, req *http.Request) { + http.Error(rw, "Handlers wired up wrong; this path shouldn't be hit", 500) + log.Printf("Internal routing error on %q", req.URL.Path) +} + +func BadRequestError(rw http.ResponseWriter, errorMessage string, args ...interface{}) { + rw.WriteHeader(http.StatusBadRequest) + log.Printf("Bad request: %s", fmt.Sprintf(errorMessage, args...)) + fmt.Fprintf(rw, "

    Bad Request

    ") +} + +func ForbiddenError(rw http.ResponseWriter, errorMessage string, args ...interface{}) { + rw.WriteHeader(http.StatusForbidden) + log.Printf("Forbidden: %s", fmt.Sprintf(errorMessage, args...)) + fmt.Fprintf(rw, "

    Forbidden

    ") +} + +func RequestEntityTooLargeError(rw http.ResponseWriter) { + rw.WriteHeader(http.StatusRequestEntityTooLarge) + fmt.Fprintf(rw, "

    Request entity is too large

    ") +} + +func ServeError(rw http.ResponseWriter, req *http.Request, err error) { + rw.WriteHeader(http.StatusInternalServerError) + if IsLocalhost(req) || env.IsDev() { + fmt.Fprintf(rw, "Server error: %s\n", err) + return + } + fmt.Fprintf(rw, "An internal error occured, sorry.") +} + +func ReturnJSON(rw http.ResponseWriter, data interface{}) { + ReturnJSONCode(rw, 200, data) +} + +func ReturnJSONCode(rw http.ResponseWriter, code int, data interface{}) { + js, err := json.MarshalIndent(data, "", " ") + if err != nil { + BadRequestError(rw, fmt.Sprintf("JSON serialization error: %v", err)) + return + } + rw.Header().Set("Content-Type", "text/javascript") + rw.Header().Set("Content-Length", strconv.Itoa(len(js)+1)) + rw.WriteHeader(code) + rw.Write(js) + rw.Write([]byte("\n")) +} + +// PrefixHandler wraps another Handler and verifies that all requests' +// Path begin with Prefix. If they don't, a 500 error is returned. +// If they do, the headers PathBaseHeader and PathSuffixHeader are set +// on the request before proxying to Handler. +// PathBaseHeader is just the value of Prefix. +// PathSuffixHeader is the part of the path that follows Prefix. +type PrefixHandler struct { + Prefix string + Handler http.Handler +} + +const ( + PathBaseHeader = "X-Prefixhandler-Pathbase" + PathSuffixHeader = "X-Prefixhandler-Pathsuffix" +) + +func (p *PrefixHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if !strings.HasPrefix(req.URL.Path, p.Prefix) { + http.Error(rw, "Inconfigured PrefixHandler", 500) + return + } + req.Header.Set(PathBaseHeader, p.Prefix) + req.Header.Set(PathSuffixHeader, strings.TrimPrefix(req.URL.Path, p.Prefix)) + p.Handler.ServeHTTP(rw, req) +} + +// PathBase returns a Request's base path, if it went via a PrefixHandler. +func PathBase(req *http.Request) string { return req.Header.Get(PathBaseHeader) } + +// PathSuffix returns a Request's suffix path, if it went via a PrefixHandler. +func PathSuffix(req *http.Request) string { return req.Header.Get(PathSuffixHeader) } + +// BaseURL returns the base URL (scheme + host and optional port + +// blobserver prefix) that should be used for requests (and responses) +// subsequent to req. The returned URL does not end in a trailing slash. +// The scheme and host:port are taken from urlStr if present, +// or derived from req otherwise. +// The prefix part comes from urlStr. +func BaseURL(urlStr string, req *http.Request) (string, error) { + var baseURL string + defaultURL, err := url.Parse(urlStr) + if err != nil { + return baseURL, err + } + prefix := path.Clean(defaultURL.Path) + scheme := "http" + if req.TLS != nil { + scheme = "https" + } + host := req.Host + if defaultURL.Host != "" { + host = defaultURL.Host + } + if defaultURL.Scheme != "" { + scheme = defaultURL.Scheme + } + baseURL = scheme + "://" + host + prefix + return baseURL, nil +} + +// RequestTargetPort returns the port targetted by the client +// in req. If not present, it returns 80, or 443 if TLS is used. +func RequestTargetPort(req *http.Request) int { + _, portStr, err := net.SplitHostPort(req.Host) + if err == nil && portStr != "" { + port, err := strconv.ParseInt(portStr, 0, 64) + if err == nil { + return int(port) + } + } + if req.TLS != nil { + return 443 + } + return 80 +} + +// Recover is meant to be used at the top of handlers with "defer" +// to catch errors from MustGet, etc: +// +// func handler(rw http.ResponseWriter, req *http.Request) { +// defer httputil.Recover(rw, req) +// id := req.MustGet("id") +// .... +// +// Recover will send the proper HTTP error type and message (e.g. +// a 400 Bad Request for MustGet) +func Recover(rw http.ResponseWriter, req *http.Request) { + RecoverJSON(rw, req) // TODO: for now. alternate format? +} + +// RecoverJSON is like Recover but returns with a JSON response. +func RecoverJSON(rw http.ResponseWriter, req *http.Request) { + e := recover() + if e == nil { + return + } + ServeJSONError(rw, e) +} + +type httpCoder interface { + HTTPCode() int +} + +// An InvalidMethodError is returned when an HTTP handler is invoked +// with an unsupported method. +type InvalidMethodError struct{} + +func (InvalidMethodError) Error() string { return "invalid method" } +func (InvalidMethodError) HTTPCode() int { return http.StatusMethodNotAllowed } + +// A MissingParameterError represents a missing HTTP parameter. +// The underlying string is the missing parameter name. +type MissingParameterError string + +func (p MissingParameterError) Error() string { return fmt.Sprintf("Missing parameter %q", string(p)) } +func (MissingParameterError) HTTPCode() int { return http.StatusBadRequest } + +// An InvalidParameterError represents an invalid HTTP parameter. +// The underlying string is the invalid parameter name, not value. +type InvalidParameterError string + +func (p InvalidParameterError) Error() string { return fmt.Sprintf("Invalid parameter %q", string(p)) } +func (InvalidParameterError) HTTPCode() int { return http.StatusBadRequest } + +// A ServerError is a generic 500 error. +type ServerError string + +func (e ServerError) Error() string { return string(e) } +func (ServerError) HTTPCode() int { return http.StatusInternalServerError } + +// MustGet returns a non-empty GET (or HEAD) parameter param and panics +// with a special error as caught by a deferred httputil.Recover. +func MustGet(req *http.Request, param string) string { + if !IsGet(req) { + panic(InvalidMethodError{}) + } + v := req.FormValue(param) + if v == "" { + panic(MissingParameterError(param)) + } + return v +} + +// MustGetBlobRef returns a non-nil BlobRef from req, as given by param. +// If it doesn't, it panics with a value understood by Recover or RecoverJSON. +func MustGetBlobRef(req *http.Request, param string) blob.Ref { + br, ok := blob.Parse(MustGet(req, param)) + if !ok { + panic(InvalidParameterError(param)) + } + return br +} + +// OptionalInt returns the integer in req given by param, or 0 if not present. +// If the form value is not an integer, it panics with a a value understood by Recover or RecoverJSON. +func OptionalInt(req *http.Request, param string) int { + v := req.FormValue(param) + if v == "" { + return 0 + } + i, err := strconv.Atoi(v) + if err != nil { + panic(InvalidParameterError(param)) + } + return i +} + +// ServeJSONError sends a JSON error response to rw for the provided +// error value. +func ServeJSONError(rw http.ResponseWriter, err interface{}) { + code := 500 + if i, ok := err.(httpCoder); ok { + code = i.HTTPCode() + } + msg := fmt.Sprint(err) + log.Printf("Sending error %v to client for: %v", code, msg) + ReturnJSONCode(rw, code, map[string]interface{}{ + "error": msg, + "errorType": http.StatusText(code), + }) +} + +// TODO: use a sync.Pool if/when Go 1.3 includes it and Camlistore depends on that. +var freeBuf = make(chan *bytes.Buffer, 2) + +func getBuf() *bytes.Buffer { + select { + case b := <-freeBuf: + b.Reset() + return b + default: + return new(bytes.Buffer) + } +} + +func putBuf(b *bytes.Buffer) { + select { + case freeBuf <- b: + default: + } +} + +// DecodeJSON decodes the JSON in res.Body into dest and then closes +// res.Body. +// It defensively caps the JSON at 8 MB for now. +func DecodeJSON(res *http.Response, dest interface{}) error { + defer CloseBody(res.Body) + buf := getBuf() + defer putBuf(buf) + if err := json.NewDecoder(io.TeeReader(io.LimitReader(res.Body, 8<<20), buf)).Decode(dest); err != nil { + return fmt.Errorf("httputil.DecodeJSON: %v, on input: %s", err, buf.Bytes()) + } + return nil +} + +// CloseBody should be used to close an http.Response.Body. +// +// It does a final little Read to maybe see EOF (to trigger connection +// re-use) before calling Close. +func CloseBody(rc io.ReadCloser) { + // Go 1.2 pseudo-bug: the NewDecoder(res.Body).Decode never + // sees an EOF, so we have to do this 0-byte copy here to + // force the http Transport to see its own EOF and recycle the + // connection. In Go 1.1 at least, the Close would cause it to + // read to EOF and recycle the connection, but in Go 1.2, a + // Close before EOF kills the underlying TCP connection. + // + // Will hopefully be fixed in Go 1.3, at least for bodies with + // Content-Length. Or maybe Go 1.3's Close itself would look + // to see if we're at EOF even if it hasn't been Read. + + // TODO: use a bytepool package somewhere for this byte? + // Justification for 3 byte reads: two for up to "\r\n" after + // a JSON/XML document, and then 1 to see EOF if we haven't yet. + buf := make([]byte, 1) + for i := 0; i < 3; i++ { + _, err := rc.Read(buf) + if err != nil { + break + } + } + rc.Close() +} + +func IsWebsocketUpgrade(req *http.Request) bool { + return req.Method == "GET" && req.Header.Get("Upgrade") == "websocket" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/httputil/httputil_test.go b/vendor/github.com/camlistore/camlistore/pkg/httputil/httputil_test.go new file mode 100644 index 00000000..da865c37 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/httputil/httputil_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package httputil + +import ( + "io" + "net/http" + "net/http/httptest" + "strconv" + "testing" +) + +func TestCloseBody(t *testing.T) { + const msg = "{\"foo\":\"bar\"}\r\n" + addrSeen := make(map[string]int) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + addrSeen[r.RemoteAddr]++ + w.Header().Set("Content-Length", strconv.Itoa(len(msg))) + w.WriteHeader(200) + w.Write([]byte(msg)) + })) + defer ts.Close() + + buf := make([]byte, len(msg)) + + for _, trim := range []int{0, 2} { + for i := 0; i < 3; i++ { + res, err := http.Get(ts.URL) + if err != nil { + t.Errorf("Get: %v", err) + continue + } + want := len(buf) - trim + n, err := res.Body.Read(buf[:want]) + CloseBody(res.Body) + if n != want { + t.Errorf("Read = %v; want %v", n, want) + } + if err != nil && err != io.EOF { + t.Errorf("Read = %v", err) + } + } + } + if len(addrSeen) != 1 { + t.Errorf("server saw %d distinct client addresses; want 1", len(addrSeen)) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/httputil/transport.go b/vendor/github.com/camlistore/camlistore/pkg/httputil/transport.go new file mode 100644 index 00000000..6fac04f0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/httputil/transport.go @@ -0,0 +1,97 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package httputil + +import ( + "io" + "log" + "net/http" + "sync" + "time" +) + +// StatsTransport wraps another RoundTripper (or uses the default one) and +// counts the number of HTTP requests performed. +type StatsTransport struct { + mu sync.Mutex + reqs int + + // Transport optionally specifies the transport to use. + // If nil, http.DefaultTransport is used. + Transport http.RoundTripper + + // If VerboseLog is true, HTTP request summaries are logged. + VerboseLog bool +} + +func (t *StatsTransport) Requests() int { + t.mu.Lock() + defer t.mu.Unlock() + return t.reqs +} + +func (t *StatsTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + t.mu.Lock() + t.reqs++ + n := t.reqs + t.mu.Unlock() + + rt := t.Transport + if rt == nil { + rt = http.DefaultTransport + } + var t0 time.Time + if t.VerboseLog { + t0 = time.Now() + log.Printf("(%d) %s %s ...", n, req.Method, req.URL) + } + resp, err = rt.RoundTrip(req) + if t.VerboseLog { + t1 := time.Now() + td := t1.Sub(t1) + if err == nil { + log.Printf("(%d) %s %s = status %d (in %v)", n, req.Method, req.URL, resp.StatusCode, td) + resp.Body = &logBody{body: resp.Body, n: n, t0: t0, t1: t1} + } else { + log.Printf("(%d) %s %s = error: %v (in %v)", n, req.Method, req.URL, err, td) + } + } + return +} + +type logBody struct { + body io.ReadCloser + n int + t0, t1 time.Time + readOnce sync.Once + closeOnce sync.Once +} + +func (b *logBody) Read(p []byte) (n int, err error) { + b.readOnce.Do(func() { + log.Printf("(%d) Read body", b.n) + }) + return b.body.Read(p) +} + +func (b *logBody) Close() error { + b.closeOnce.Do(func() { + t := time.Now() + log.Printf("(%d) Close body (%v tot, %v post-header)", b.n, t.Sub(b.t0), t.Sub(b.t1)) + }) + return b.body.Close() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/bench_test.go b/vendor/github.com/camlistore/camlistore/pkg/images/bench_test.go new file mode 100644 index 00000000..b0faedc0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/images/bench_test.go @@ -0,0 +1,136 @@ +/* +Copyright 2013 The Camlistore AUTHORS + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package images + +import ( + "image" + "testing" +) + +func benchRescale(b *testing.B, w, h, thumbW, thumbH int) { + // Most JPEGs are YCbCr, so bench with that. + im := image.NewYCbCr(image.Rect(0, 0, w, h), image.YCbCrSubsampleRatio422) + o := &DecodeOpts{MaxWidth: thumbW, MaxHeight: thumbH} + sw, sh, needRescale := o.rescaleDimensions(im.Bounds(), false) + if !needRescale { + b.Fatal("opts.rescaleDimensions failed to indicate image needs rescale") + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = rescale(im, sw, sh) + } +} + +func BenchmarkRescale1000To50(b *testing.B) { + orig, thumb := 1000, 50 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale1000To100(b *testing.B) { + orig, thumb := 1000, 100 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale1000To200(b *testing.B) { + orig, thumb := 1000, 200 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale1000To400(b *testing.B) { + orig, thumb := 1000, 400 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale1000To800(b *testing.B) { + orig, thumb := 1000, 800 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale2000To50(b *testing.B) { + orig, thumb := 2000, 50 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale2000To100(b *testing.B) { + orig, thumb := 2000, 100 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale2000To200(b *testing.B) { + orig, thumb := 2000, 200 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale2000To400(b *testing.B) { + orig, thumb := 2000, 400 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale2000To800(b *testing.B) { + orig, thumb := 2000, 800 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale4000To50(b *testing.B) { + orig, thumb := 4000, 50 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale4000To100(b *testing.B) { + orig, thumb := 4000, 100 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale4000To200(b *testing.B) { + orig, thumb := 4000, 200 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale4000To400(b *testing.B) { + orig, thumb := 4000, 400 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale4000To800(b *testing.B) { + orig, thumb := 4000, 800 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale8000To50(b *testing.B) { + orig, thumb := 8000, 50 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale8000To100(b *testing.B) { + orig, thumb := 8000, 100 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale8000To200(b *testing.B) { + orig, thumb := 8000, 200 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale8000To400(b *testing.B) { + orig, thumb := 8000, 400 + benchRescale(b, orig, orig, thumb, thumb) +} + +func BenchmarkRescale8000To800(b *testing.B) { + orig, thumb := 8000, 800 + benchRescale(b, orig, orig, thumb, thumb) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/benchfastjpeg_test.go b/vendor/github.com/camlistore/camlistore/pkg/images/benchfastjpeg_test.go new file mode 100644 index 00000000..0138cb1b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/images/benchfastjpeg_test.go @@ -0,0 +1,138 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package images + +import ( + "bytes" + "image" + "io" + "io/ioutil" + "testing" + + "camlistore.org/pkg/images/fastjpeg" + "camlistore.org/pkg/images/resize" + "camlistore.org/pkg/types" + "camlistore.org/third_party/go/pkg/image/jpeg" +) + +// The decode routines being benchmarked in this file will use these bytes for +// their in-memory io.Readers. +var jpegBytes []byte + +func init() { + // Create image with non-uniform color to make decoding more realistic. + // Solid color jpeg images decode faster than non-uniform images. + b := new(bytes.Buffer) + w, h := 4000, 4000 + im := image.NewNRGBA(image.Rect(0, 0, w, h)) + for i := range im.Pix { + switch { + case i%4 == 3: + im.Pix[i] = 255 + default: + im.Pix[i] = uint8(i) + } + } + if err := jpeg.Encode(b, im, nil); err != nil { + panic(err) + } + jpegBytes = b.Bytes() +} + +type decodeFunc func(r io.Reader) (image.Image, string, error) + +func BenchmarkStdlib(b *testing.B) { + common(b, image.Decode) +} + +func decodeDownsample(factor int) decodeFunc { + return func(r io.Reader) (image.Image, string, error) { + im, err := fastjpeg.DecodeDownsample(r, factor) + return im, "jpeg", err + } +} + +func BenchmarkDjpeg1(b *testing.B) { + if !fastjpeg.Available() { + b.Skip("Skipping benchmark, djpeg unavailable.") + } + common(b, decodeDownsample(1)) +} + +func BenchmarkDjpeg2(b *testing.B) { + if !fastjpeg.Available() { + b.Skip("Skipping benchmark, djpeg unavailable.") + } + common(b, decodeDownsample(2)) +} + +func BenchmarkDjpeg4(b *testing.B) { + if !fastjpeg.Available() { + b.Skip("Skipping benchmark, djpeg unavailable.") + } + common(b, decodeDownsample(4)) +} + +func BenchmarkDjpeg8(b *testing.B) { + if !fastjpeg.Available() { + b.Skip("Skipping benchmark, djpeg unavailable.") + } + common(b, decodeDownsample(8)) +} + +func testRun(b types.TB, decode decodeFunc) { + if !fastjpeg.Available() { + b.Skip("Skipping benchmark, djpeg unavailable.") + } + im, _, err := decode(bytes.NewReader(jpegBytes)) + if err != nil { + b.Fatal(err) + } + rect := im.Bounds() + w, h := 128, 128 + im = resize.Resize(im, rect, w, h) + err = jpeg.Encode(ioutil.Discard, im, nil) + if err != nil { + b.Fatal(err) + } +} + +func common(b *testing.B, decode decodeFunc) { + for i := 0; i < b.N; i++ { + testRun(b, decode) + } +} + +func TestStdlib(t *testing.T) { + testRun(t, decodeDownsample(1)) +} + +func TestDjpeg1(t *testing.T) { + testRun(t, decodeDownsample(1)) +} + +func TestDjpeg2(t *testing.T) { + testRun(t, decodeDownsample(2)) +} + +func TestDjpeg4(t *testing.T) { + testRun(t, decodeDownsample(4)) +} + +func TestDjpeg8(t *testing.T) { + testRun(t, decodeDownsample(8)) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/fastjpeg/fastjpeg.go b/vendor/github.com/camlistore/camlistore/pkg/images/fastjpeg/fastjpeg.go new file mode 100644 index 00000000..42550a7d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/images/fastjpeg/fastjpeg.go @@ -0,0 +1,230 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package fastjpeg uses djpeg(1), from the Independent JPEG Group's +// (www.ijg.org) jpeg package, to quickly down-sample images on load. It can +// sample images by a factor of 1, 2, 4 or 8. +// This reduces the amount of data that must be decompressed into memory when +// the full resolution image isn't required, i.e. in the case of generating +// thumbnails. +package fastjpeg + +import ( + "bytes" + "errors" + "expvar" + "fmt" + "image" + "image/color" + "io" + "log" + "os" + "os/exec" + "strconv" + "sync" + + "camlistore.org/pkg/buildinfo" + "camlistore.org/pkg/types" + _ "camlistore.org/third_party/go/pkg/image/jpeg" +) + +var ( + ErrDjpegNotFound = errors.New("fastjpeg: djpeg not found in path") +) + +// DjpegFailedError wraps errors returned when calling djpeg and handling its +// response. Used for type asserting and retrying with other jpeg decoders, +// i.e. the standard library's jpeg.Decode. +type DjpegFailedError struct { + Err error +} + +func (dfe DjpegFailedError) Error() string { + return dfe.Err.Error() +} + +// TODO(wathiede): do we need to conditionally add ".exe" on Windows? I have +// no access to test on Windows. +const djpegBin = "djpeg" + +var ( + checkAvailability sync.Once + available bool +) + +var ( + djpegSuccessVar = expvar.NewInt("fastjpeg-djpeg-success") + djpegFailureVar = expvar.NewInt("fastjpeg-djpeg-failure") + // Bytes read from djpeg subprocess + djpegBytesReadVar = expvar.NewInt("fastjpeg-djpeg-bytes-read") + // Bytes written to djpeg subprocess + djpegBytesWrittenVar = expvar.NewInt("fastjpeg-djpeg-bytes-written") +) + +func Available() bool { + checkAvailability.Do(func() { + if ok, _ := strconv.ParseBool(os.Getenv("CAMLI_DISABLE_DJPEG")); ok { + log.Println("CAMLI_DISABLE_DJPEG set in environment. Disabling fastjpeg.") + return + } + + if p, err := exec.LookPath(djpegBin); p != "" && err == nil { + available = true + log.Printf("fastjpeg enabled with %s.", p) + } + if !available { + log.Printf("%s not found in PATH, disabling fastjpeg.", djpegBin) + } + }) + + return available +} + +func init() { + buildinfo.RegisterDjpegStatusFunc(djpegStatus) +} + +func djpegStatus() string { + // TODO: more info: its path, whether it works, its version, etc. + if Available() { + return "djpeg available" + } + return "djpeg optimizaton unavailable" +} + +func readPNM(buf *bytes.Buffer) (image.Image, error) { + var imgType, w, h int + nTokens, err := fmt.Fscanf(buf, "P%d\n%d %d\n255\n", &imgType, &w, &h) + if err != nil { + return nil, err + } + if nTokens != 3 { + hdr := buf.Bytes() + if len(hdr) > 100 { + hdr = hdr[:100] + } + return nil, fmt.Errorf("fastjpeg: Invalid PNM header: %q", hdr) + } + + switch imgType { + case 5: // Gray + src := buf.Bytes() + if len(src) != w*h { + return nil, fmt.Errorf("fastjpeg: grayscale source buffer not sized w*h") + } + im := &image.Gray{ + Pix: src, + Stride: w, + Rect: image.Rect(0, 0, w, h), + } + return im, nil + case 6: // RGB + src := buf.Bytes() + if len(src) != w*h*3 { + return nil, fmt.Errorf("fastjpeg: RGB source buffer not sized w*h*3") + } + im := image.NewRGBA(image.Rect(0, 0, w, h)) + dst := im.Pix + for i := 0; i < len(src)/3; i++ { + dst[4*i+0] = src[3*i+0] // R + dst[4*i+1] = src[3*i+1] // G + dst[4*i+2] = src[3*i+2] // B + dst[4*i+3] = 255 // Alpha + } + return im, nil + default: + return nil, fmt.Errorf("fastjpeg: Unsupported PNM type P%d", imgType) + } +} + +// Factor returns the sample factor DecodeSample should use to generate a +// sampled image greater than or equal to sw x sh pixels given a source image +// of w x h pixels. +func Factor(w, h, sw, sh int) int { + switch { + case w>>3 >= sw && h>>3 >= sh: + return 8 + case w>>2 >= sw && h>>2 >= sh: + return 4 + case w>>1 >= sw && h>>1 >= sh: + return 2 + } + return 1 +} + +// DecodeDownsample decodes JPEG data in r, down-sampling it by factor. +// If djpeg is not found, err is ErrDjpegNotFound and r is not read from. +// If the execution of djpeg, or decoding the resulting PNM fails, error will +// be of type DjpegFailedError. +func DecodeDownsample(r io.Reader, factor int) (image.Image, error) { + if !Available() { + return nil, ErrDjpegNotFound + } + switch factor { + case 1, 2, 4, 8: + default: + return nil, fmt.Errorf("fastjpeg: unsupported sample factor %d", factor) + } + + buf := new(bytes.Buffer) + tr := io.TeeReader(r, buf) + ic, format, err := image.DecodeConfig(tr) + if err != nil { + return nil, err + } + if format != "jpeg" { + return nil, fmt.Errorf("fastjpeg: Unsupported format %q", format) + } + var bpp int + switch ic.ColorModel { + case color.YCbCrModel: + bpp = 4 // JPEG will decode to RGB, and we'll expand inplace to RGBA. + case color.GrayModel: + bpp = 1 + default: + return nil, fmt.Errorf("fastjpeg: Unsupported thumnbnail color model %T", ic.ColorModel) + } + args := []string{djpegBin, "-scale", fmt.Sprintf("1/%d", factor)} + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdin = types.NewStatsReader(djpegBytesWrittenVar, io.MultiReader(buf, r)) + + // Allocate space for the RGBA / Gray pixel data plus some extra for PNM + // header info. Explicitly allocate all the memory upfront to prevent + // many smaller allocations. + pixSize := ic.Width*ic.Height*bpp/factor/factor + 128 + w := bytes.NewBuffer(make([]byte, 0, pixSize)) + cmd.Stdout = w + + stderrW := new(bytes.Buffer) + cmd.Stderr = stderrW + if err := cmd.Run(); err != nil { + // cmd.ProcessState == nil happens if /lib/*/ld-x.yz.so is missing, which gives you the ever useful: + // "fork/exec /usr/bin/djpeg: no such file or directory" error message. + // So of course it only happens on broken systems and this check is probably overkill. + if cmd.ProcessState == nil || !cmd.ProcessState.Success() { + djpegFailureVar.Add(1) + return nil, DjpegFailedError{Err: fmt.Errorf("%v: %s", err, stderrW)} + } + // false alarm, so proceed. See http://camlistore.org/issue/550 + } + djpegSuccessVar.Add(1) + djpegBytesReadVar.Add(int64(w.Len())) + m, err := readPNM(w) + if err != nil { + return m, DjpegFailedError{Err: err} + } + return m, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/fastjpeg/fastjpeg_test.go b/vendor/github.com/camlistore/camlistore/pkg/images/fastjpeg/fastjpeg_test.go new file mode 100644 index 00000000..6a1deadf --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/images/fastjpeg/fastjpeg_test.go @@ -0,0 +1,214 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fastjpeg + +import ( + "bytes" + "image" + "os" + "os/exec" + "path/filepath" + "reflect" + "runtime" + "strconv" + "sync" + "testing" + + "camlistore.org/third_party/go/pkg/image/jpeg" +) + +const ( + width = 3840 + height = 1280 +) + +// testImage hold an image and the encoded jpeg bytes for the image. +type testImage struct { + im image.Image + buf []byte +} + +// makeTestImages generates an RGBA and a grayscale image and returns +// testImages containing the JPEG encoded form as bytes and the expected color +// model of the image when decoded. +func makeTestImages() ([]testImage, error) { + var ims []testImage + w := bytes.NewBuffer(nil) + im1 := image.NewRGBA(image.Rect(0, 0, width, height)) + for i := range im1.Pix { + switch { + case i%4 == 3: + im1.Pix[i] = 255 + default: + im1.Pix[i] = uint8(i) + } + } + if err := jpeg.Encode(w, im1, nil); err != nil { + return nil, err + } + ims = append(ims, testImage{im: im1, buf: w.Bytes()}) + + w = bytes.NewBuffer(nil) + im2 := image.NewGray(image.Rect(0, 0, width, height)) + for i := range im2.Pix { + im2.Pix[i] = uint8(i) + } + if err := jpeg.Encode(w, im2, nil); err != nil { + return nil, err + } + ims = append(ims, testImage{im: im2, buf: w.Bytes()}) + return ims, nil + +} + +func TestDecodeDownsample(t *testing.T) { + checkAvailability = sync.Once{} + if !Available() { + t.Skip("djpeg isn't available.") + } + + tis, err := makeTestImages() + if err != nil { + t.Fatal(err) + } + + if _, err := DecodeDownsample(bytes.NewReader(tis[0].buf), 0); err == nil { + t.Errorf("Expect error for invalid sample factor 0") + } + for i, ti := range tis { + for factor := 1; factor <= 8; factor *= 2 { + im, err := DecodeDownsample(bytes.NewReader(ti.buf), factor) + if err != nil { + t.Errorf("%d: Sample factor %d failed: %v", i, factor, err) + continue + } + wantW := width / factor + wantH := height / factor + b := im.Bounds() + gotW := b.Dx() + gotH := b.Dy() + + if wantW != gotW || wantH != gotH || reflect.TypeOf(im) != reflect.TypeOf(ti.im) { + t.Errorf("%d: Sample factor %d want image %dx%d %T got %dx%d %T", i, factor, wantW, wantH, ti.im, gotW, gotH, im) + } + } + } +} + +// TestUnavailable verifies the behavior of Available and DecodeDownsample +// when djpeg is not available. +// It sets the environment variable CAMLI_DISABLE_DJPEG and spawns +// a subprocess to simulate unavailability. +func TestUnavailable(t *testing.T) { + checkAvailability = sync.Once{} + defer os.Setenv("CAMLI_DISABLE_DJPEG", "0") + if ok, _ := strconv.ParseBool(os.Getenv("CAMLI_DISABLE_DJPEG")); !ok { + os.Setenv("CAMLI_DISABLE_DJPEG", "1") + out, err := exec.Command(os.Args[0], "-test.v", + "-test.run=TestUnavailable$").CombinedOutput() + if err != nil { + t.Fatalf("%v: %s", err, out) + } + return + } + + if Available() { + t.Fatal("djpeg shouldn't be available when run with CAMLI_DISABLE_DJPEG set.") + } + + tis, err := makeTestImages() + if err != nil { + t.Fatal(err) + } + if _, err := DecodeDownsample(bytes.NewReader(tis[0].buf), 2); err != ErrDjpegNotFound { + t.Errorf("Wanted ErrDjpegNotFound, got %v", err) + } +} + +func TestFailed(t *testing.T) { + switch runtime.GOOS { + case "darwin", "freebsd", "linux": + default: + t.Skip("test only runs on UNIX") + } + checkAvailability = sync.Once{} + if !Available() { + t.Skip("djpeg isn't available.") + } + + oldPath := os.Getenv("PATH") + defer os.Setenv("PATH", oldPath) + // Use djpeg that exits after calling false. + newPath, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + os.Setenv("PATH", newPath) + t.Log("PATH", os.Getenv("PATH")) + t.Log(exec.LookPath("djpeg")) + + tis, err := makeTestImages() + if err != nil { + t.Fatal(err) + } + _, err = DecodeDownsample(bytes.NewReader(tis[0].buf), 2) + if _, ok := err.(DjpegFailedError); !ok { + t.Errorf("Got err type %T want ErrDjpegFailed: %v", err, err) + } +} + +func TestFactor(t *testing.T) { + checkAvailability = sync.Once{} + if !Available() { + t.Skip("djpeg isn't available.") + } + + const ( + width = 3840 + height = 1280 + ) + testCases := []struct { + w, h int + want int + }{ + {width + 1, height, 1}, + {width, height + 1, 1}, + {width, height, 1}, + {width - 1, height, 1}, + {width, height - 1, 1}, + {width/2 + 1, height / 2, 1}, + {width / 2, height/2 + 1, 1}, + + {width / 2, height / 2, 2}, + {width/2 - 1, height / 2, 2}, + {width / 2, height/2 - 1, 2}, + + {width / 8, height/8 + 1, 4}, + {width/8 + 1, height / 8, 4}, + + {width / 8, height / 8, 8}, + {width / 8, height/8 - 1, 8}, + {width/8 - 1, height / 8, 8}, + {width/8 - 1, height/8 - 1, 8}, + } + for _, tc := range testCases { + if got := Factor(width, height, tc.w, tc.h); got != tc.want { + t.Errorf("%dx%d -> %dx%d got %d want %d", width, height, + tc.w, tc.h, got, tc.want) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/fastjpeg/testdata/djpeg b/vendor/github.com/camlistore/camlistore/pkg/images/fastjpeg/testdata/djpeg new file mode 100755 index 00000000..11a0e977 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/images/fastjpeg/testdata/djpeg @@ -0,0 +1,2 @@ +#!/bin/sh +/usr/bin/false diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/images.go b/vendor/github.com/camlistore/camlistore/pkg/images/images.go new file mode 100644 index 00000000..551f2297 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/images/images.go @@ -0,0 +1,564 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package images + +import ( + "bytes" + "fmt" + "image" + "image/draw" + "image/jpeg" + "io" + "log" + "os" + "strconv" + "time" + + _ "image/gif" + _ "image/png" + + "camlistore.org/pkg/images/fastjpeg" + "camlistore.org/pkg/images/resize" + "camlistore.org/third_party/github.com/nf/cr2" + "camlistore.org/third_party/github.com/rwcarlsen/goexif/exif" + + // TODO(mpl, wathiede): add test(s) to check we can decode both tiff and cr2, + // so we don't mess up the import order again. + // See https://camlistore-review.googlesource.com/5196 comments. + + // tiff package must be imported after any image packages that decode + // tiff-like formats, i.e. CR2 or DNG + _ "camlistore.org/third_party/golang.org/x/image/tiff" +) + +var disableThumbCache, _ = strconv.ParseBool(os.Getenv("CAMLI_DISABLE_THUMB_CACHE")) + +// thumbnailVersion should be incremented whenever we want to +// invalidate the cache of previous thumbnails on the server's +// cache and in browsers. +const thumbnailVersion = "2" + +// ThumbnailVersion returns a string safe for URL query components +// which is a generation number. Whenever the thumbnailing code is +// updated, so will this string. It should be placed in some URL +// component (typically "tv"). +func ThumbnailVersion() string { + if disableThumbCache { + return fmt.Sprintf("nocache%d", time.Now().UnixNano()) + } + return thumbnailVersion +} + +// Exif Orientation Tag values +// http://sylvana.net/jpegcrop/exif_orientation.html +const ( + topLeftSide = 1 + topRightSide = 2 + bottomRightSide = 3 + bottomLeftSide = 4 + leftSideTop = 5 + rightSideTop = 6 + rightSideBottom = 7 + leftSideBottom = 8 +) + +// The FlipDirection type is used by the Flip option in DecodeOpts +// to indicate in which direction to flip an image. +type FlipDirection int + +// FlipVertical and FlipHorizontal are two possible FlipDirections +// values to indicate in which direction an image will be flipped. +const ( + FlipVertical FlipDirection = 1 << iota + FlipHorizontal +) + +type DecodeOpts struct { + // Rotate specifies how to rotate the image. + // If nil, the image is rotated automatically based on EXIF metadata. + // If an int, Rotate is the number of degrees to rotate + // counter clockwise and must be one of 0, 90, -90, 180, or + // -180. + Rotate interface{} + + // Flip specifies how to flip the image. + // If nil, the image is flipped automatically based on EXIF metadata. + // Otherwise, Flip is a FlipDirection bitfield indicating how to flip. + Flip interface{} + + // MaxWidgth and MaxHeight optionally specify bounds on the + // image's size. Rescaling is done before flipping or rotating. + // Proportions are conserved, so the smallest of the two is used + // as the decisive one if needed. + MaxWidth, MaxHeight int + + // ScaleWidth and ScaleHeight optionally specify how to rescale the + // image's dimensions. Rescaling is done before flipping or rotating. + // Proportions are conserved, so the smallest of the two is used + // as the decisive one if needed. + // They overrule MaxWidth and MaxHeight. + ScaleWidth, ScaleHeight float32 + + // TODO: consider alternate options if scaled ratio doesn't + // match original ratio: + // Crop bool + // Stretch bool +} + +// Config is like the standard library's image.Config as used by DecodeConfig. +type Config struct { + Width, Height int + Format string + Modified bool // true if Decode actually rotated or flipped the image. +} + +func (c *Config) setBounds(im image.Image) { + if im != nil { + c.Width = im.Bounds().Dx() + c.Height = im.Bounds().Dy() + } +} + +func rotate(im image.Image, angle int) image.Image { + var rotated *image.NRGBA + // trigonometric (i.e counter clock-wise) + switch angle { + case 90: + newH, newW := im.Bounds().Dx(), im.Bounds().Dy() + rotated = image.NewNRGBA(image.Rect(0, 0, newW, newH)) + for y := 0; y < newH; y++ { + for x := 0; x < newW; x++ { + rotated.Set(x, y, im.At(newH-1-y, x)) + } + } + case -90: + newH, newW := im.Bounds().Dx(), im.Bounds().Dy() + rotated = image.NewNRGBA(image.Rect(0, 0, newW, newH)) + for y := 0; y < newH; y++ { + for x := 0; x < newW; x++ { + rotated.Set(x, y, im.At(y, newW-1-x)) + } + } + case 180, -180: + newW, newH := im.Bounds().Dx(), im.Bounds().Dy() + rotated = image.NewNRGBA(image.Rect(0, 0, newW, newH)) + for y := 0; y < newH; y++ { + for x := 0; x < newW; x++ { + rotated.Set(x, y, im.At(newW-1-x, newH-1-y)) + } + } + default: + return im + } + return rotated +} + +// flip returns a flipped version of the image im, according to +// the direction(s) in dir. +// It may flip the input im in place and return it, or it may allocate a +// new NRGBA (if im is an *image.YCbCr). +func flip(im image.Image, dir FlipDirection) image.Image { + if dir == 0 { + return im + } + ycbcr := false + var nrgba image.Image + dx, dy := im.Bounds().Dx(), im.Bounds().Dy() + di, ok := im.(draw.Image) + if !ok { + if _, ok := im.(*image.YCbCr); !ok { + log.Printf("failed to flip image: input does not satisfy draw.Image") + return im + } + // because YCbCr does not implement Set, we replace it with a new NRGBA + ycbcr = true + nrgba = image.NewNRGBA(image.Rect(0, 0, dx, dy)) + di, ok = nrgba.(draw.Image) + if !ok { + log.Print("failed to flip image: could not cast an NRGBA to a draw.Image") + return im + } + } + if dir&FlipHorizontal != 0 { + for y := 0; y < dy; y++ { + for x := 0; x < dx/2; x++ { + old := im.At(x, y) + di.Set(x, y, im.At(dx-1-x, y)) + di.Set(dx-1-x, y, old) + } + } + } + if dir&FlipVertical != 0 { + for y := 0; y < dy/2; y++ { + for x := 0; x < dx; x++ { + old := im.At(x, y) + di.Set(x, y, im.At(x, dy-1-y)) + di.Set(x, dy-1-y, old) + } + } + } + if ycbcr { + return nrgba + } + return im +} + +// ScaledDimensions returns the newWidth and newHeight obtained +// when an image of dimensions w x h has to be rescaled under +// mw x mh, while conserving the proportions. +// It returns 1,1 if any of the parameter is 0. +func ScaledDimensions(w, h, mw, mh int) (newWidth int, newHeight int) { + if w == 0 || h == 0 || mw == 0 || mh == 0 { + imageDebug("ScaledDimensions was given as 0; returning 1x1 as dimensions.") + return 1, 1 + } + newWidth, newHeight = mw, mh + if float32(h)/float32(mh) > float32(w)/float32(mw) { + newWidth = w * mh / h + } else { + newHeight = h * mw / w + } + return +} + +// rescaleDimensions computes the width & height in the pre-rotated +// orientation needed to meet the post-rotation constraints of opts. +// The image bound by b represents the pre-rotated dimensions of the image. +// needRescale is true if the image requires a resize. +func (opts *DecodeOpts) rescaleDimensions(b image.Rectangle, swapDimensions bool) (width, height int, needRescale bool) { + w, h := b.Dx(), b.Dy() + mw, mh := opts.MaxWidth, opts.MaxHeight + mwf, mhf := opts.ScaleWidth, opts.ScaleHeight + if mw == 0 && mh == 0 && mwf == 0 && mhf == 0 { + return w, h, false + } + + // Floating point compares probably only allow this to work if the values + // were specified as the literal 1 or 1.0, computed values will likely be + // off. If Scale{Width,Height} end up being 1.0-epsilon we'll rescale + // when it probably wouldn't even be noticeable but that's okay. + if opts.ScaleWidth == 1.0 && opts.ScaleHeight == 1.0 { + return w, h, false + } + + if swapDimensions { + w, h = h, w + } + + // ScaleWidth and ScaleHeight overrule MaxWidth and MaxHeight + if mwf > 0.0 && mwf <= 1 { + mw = int(mwf * float32(w)) + } + if mhf > 0.0 && mhf <= 1 { + mh = int(mhf * float32(h)) + } + + neww, newh := ScaledDimensions(w, h, mw, mh) + if neww > w || newh > h { + // Don't scale up. + return w, h, false + } + + needRescale = neww != w || newh != h + if swapDimensions { + return newh, neww, needRescale + } + return neww, newh, needRescale +} + +// rescale resizes im in-place to the dimensions sw x sh, overwriting the +// existing pixel data. It is up to the caller to ensure sw & sh maintain the +// aspect ratio of im. +func rescale(im image.Image, sw, sh int) image.Image { + b := im.Bounds() + w, h := b.Dx(), b.Dy() + if sw == w && sh == h { + return im + } + + // If it's gigantic, it's more efficient to downsample first + // and then resize; resizing will smooth out the roughness. + // (trusting the moustachio guys on that one). + if w > sw*2 && h > sh*2 { + im = resize.ResampleInplace(im, b, sw*2, sh*2) + return resize.HalveInplace(im) + } + return resize.Resize(im, b, sw, sh) +} + +// forcedRotate checks if the values in opts explicitly set a rotation. +func (opts *DecodeOpts) forcedRotate() bool { + return opts != nil && opts.Rotate != nil +} + +// forcedRotate checks if the values in opts explicitly set a flip. +func (opts *DecodeOpts) forcedFlip() bool { + return opts != nil && opts.Flip != nil +} + +// useEXIF checks if the values in opts imply EXIF data should be used for +// orientation. +func (opts *DecodeOpts) useEXIF() bool { + return !(opts.forcedRotate() || opts.forcedFlip()) +} + +// forcedOrientation returns the rotation and flip values stored in opts. The +// values are asserted to their proper type, and err is non-nil if an invalid +// value is found. This function ignores the orientation stored in EXIF. +// If auto-correction of the image's orientation is desired, it is the +// caller's responsibility to check via useEXIF first. +func (opts *DecodeOpts) forcedOrientation() (angle int, flipMode FlipDirection, err error) { + var ( + ok bool + ) + if opts.forcedRotate() { + if angle, ok = opts.Rotate.(int); !ok { + return 0, 0, fmt.Errorf("Rotate should be an int, not a %T", opts.Rotate) + } + } + if opts.forcedFlip() { + if flipMode, ok = opts.Flip.(FlipDirection); !ok { + return 0, 0, fmt.Errorf("Flip should be a FlipDirection, not a %T", opts.Flip) + } + } + return angle, flipMode, nil +} + +var debug, _ = strconv.ParseBool(os.Getenv("CAMLI_DEBUG_IMAGES")) + +func imageDebug(msg string) { + if debug { + log.Print(msg) + } +} + +// DecodeConfig returns the image Config similarly to +// the standard library's image.DecodeConfig with the +// addition that it also checks for an EXIF orientation, +// and sets the Width and Height as they would visibly +// be after correcting for that orientation. +func DecodeConfig(r io.Reader) (Config, error) { + var c Config + var buf bytes.Buffer + tr := io.TeeReader(io.LimitReader(r, 2<<20), &buf) + swapDimensions := false + + ex, err := exif.Decode(tr) + // trigger a retry when there isn't enough data for reading exif data from a tiff file + if exif.IsShortReadTagValueError(err) { + return c, io.ErrUnexpectedEOF + } + if err != nil { + imageDebug(fmt.Sprintf("No valid EXIF, error: %v.", err)) + } else { + tag, err := ex.Get(exif.Orientation) + if err != nil { + imageDebug(`No "Orientation" tag in EXIF.`) + } else { + orient, err := tag.Int(0) + if err == nil { + switch orient { + // those are the orientations that require + // a rotation of ±90 + case leftSideTop, rightSideTop, rightSideBottom, leftSideBottom: + swapDimensions = true + } + } else { + imageDebug(fmt.Sprintf("EXIF Error: %v", err)) + } + } + } + conf, format, err := image.DecodeConfig(io.MultiReader(&buf, r)) + if err != nil { + imageDebug(fmt.Sprintf("Image Decoding failed: %v", err)) + return c, err + } + c.Format = format + if swapDimensions { + c.Width, c.Height = conf.Height, conf.Width + } else { + c.Width, c.Height = conf.Width, conf.Height + } + return c, err +} + +// decoder reads an image from r and modifies the image as defined by opts. +// swapDimensions indicates the decoded image will be rotated after being +// returned, and when interpreting opts, the post-rotation dimensions should +// be considered. +// The decoded image is returned in im. The registered name of the decoder +// used is returned in format. If the image was not successfully decoded, err +// will be non-nil. If the decoded image was made smaller, needRescale will +// be true. +func decode(r io.Reader, opts *DecodeOpts, swapDimensions bool) (im image.Image, format string, err error, needRescale bool) { + if opts == nil { + // Fall-back to normal decode. + im, format, err = image.Decode(r) + return im, format, err, false + } + + var buf bytes.Buffer + tr := io.TeeReader(r, &buf) + ic, format, err := image.DecodeConfig(tr) + if err != nil { + return nil, "", err, false + } + + mr := io.MultiReader(&buf, r) + b := image.Rect(0, 0, ic.Width, ic.Height) + sw, sh, needRescale := opts.rescaleDimensions(b, swapDimensions) + if !needRescale { + im, format, err = image.Decode(mr) + return im, format, err, false + } + + imageDebug(fmt.Sprintf("Resizing from %dx%d -> %dx%d", ic.Width, ic.Height, sw, sh)) + if format == "cr2" { + // Replace mr with an io.Reader to the JPEG thumbnail embedded in a + // CR2 image. + if mr, err = cr2.NewReader(mr); err != nil { + return nil, "", err, false + } + format = "jpeg" + } + + if format == "jpeg" && fastjpeg.Available() { + factor := fastjpeg.Factor(ic.Width, ic.Height, sw, sh) + if factor > 1 { + var buf bytes.Buffer + tr := io.TeeReader(mr, &buf) + im, err = fastjpeg.DecodeDownsample(tr, factor) + switch err.(type) { + case fastjpeg.DjpegFailedError: + log.Printf("Retrying with jpeg.Decode, because djpeg failed with: %v", err) + im, err = jpeg.Decode(io.MultiReader(&buf, mr)) + case nil: + // fallthrough to rescale() below. + default: + return nil, format, err, false + } + return rescale(im, sw, sh), format, err, true + } + } + + // Fall-back to normal decode. + im, format, err = image.Decode(mr) + if err != nil { + return nil, "", err, false + } + return rescale(im, sw, sh), format, err, needRescale +} + +// exifOrientation parses the EXIF data in r and returns the stored +// orientation as the angle and flip necessary to transform the image. +func exifOrientation(r io.Reader) (int, FlipDirection) { + var ( + angle int + flipMode FlipDirection + ) + ex, err := exif.Decode(r) + if err != nil { + imageDebug("No valid EXIF; will not rotate or flip.") + return 0, 0 + } + tag, err := ex.Get(exif.Orientation) + if err != nil { + imageDebug(`No "Orientation" tag in EXIF; will not rotate or flip.`) + return 0, 0 + } + orient, err := tag.Int(0) + if err != nil { + imageDebug(fmt.Sprintf("EXIF error: %v", err)) + return 0, 0 + } + switch orient { + case topLeftSide: + // do nothing + case topRightSide: + flipMode = 2 + case bottomRightSide: + angle = 180 + case bottomLeftSide: + angle = 180 + flipMode = 2 + case leftSideTop: + angle = -90 + flipMode = 2 + case rightSideTop: + angle = -90 + case rightSideBottom: + angle = 90 + flipMode = 2 + case leftSideBottom: + angle = 90 + } + return angle, flipMode +} + +// Decode decodes an image from r using the provided decoding options. +// The Config returned is similar to the one from the image package, +// with the addition of the Modified field which indicates if the +// image was actually flipped, rotated, or scaled. +// If opts is nil, the defaults are used. +func Decode(r io.Reader, opts *DecodeOpts) (image.Image, Config, error) { + var ( + angle int + buf bytes.Buffer + c Config + flipMode FlipDirection + ) + + tr := io.TeeReader(io.LimitReader(r, 2<<20), &buf) + if opts.useEXIF() { + angle, flipMode = exifOrientation(tr) + } else { + var err error + angle, flipMode, err = opts.forcedOrientation() + if err != nil { + return nil, c, err + } + } + + // Orientation changing rotations should have their dimensions swapped + // when scaling. + var swapDimensions bool + switch angle { + case 90, -90: + swapDimensions = true + } + + mr := io.MultiReader(&buf, r) + im, format, err, rescaled := decode(mr, opts, swapDimensions) + if err != nil { + return nil, c, err + } + c.Modified = rescaled + + if angle != 0 { + im = rotate(im, angle) + c.Modified = true + } + + if flipMode != 0 { + im = flip(im, flipMode) + c.Modified = true + } + + c.Format = format + c.setBounds(im) + return im, c, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/images_test.go b/vendor/github.com/camlistore/camlistore/pkg/images/images_test.go new file mode 100644 index 00000000..0f51d063 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/images/images_test.go @@ -0,0 +1,374 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package images + +import ( + "bytes" + "image" + "os" + "path/filepath" + "sort" + "strings" + "testing" + "time" + + "camlistore.org/third_party/github.com/rwcarlsen/goexif/exif" + "camlistore.org/third_party/go/pkg/image/jpeg" +) + +const datadir = "testdata" + +func equals(im1, im2 image.Image) bool { + if !im1.Bounds().Eq(im2.Bounds()) { + return false + } + for y := 0; y < im1.Bounds().Dy(); y++ { + for x := 0; x < im1.Bounds().Dx(); x++ { + r1, g1, b1, a1 := im1.At(x, y).RGBA() + r2, g2, b2, a2 := im2.At(x, y).RGBA() + if r1 != r2 || g1 != g2 || b1 != b2 || a1 != a2 { + return false + } + } + } + return true +} + +func straightFImage(t *testing.T) image.Image { + g, err := os.Open(filepath.Join(datadir, "f1.jpg")) + if err != nil { + t.Fatal(err) + } + defer g.Close() + straightF, err := jpeg.Decode(g) + if err != nil { + t.Fatal(err) + } + return straightF +} + +func smallStraightFImage(t *testing.T) image.Image { + g, err := os.Open(filepath.Join(datadir, "f1-s.jpg")) + if err != nil { + t.Fatal(err) + } + defer g.Close() + straightF, err := jpeg.Decode(g) + if err != nil { + t.Fatal(err) + } + return straightF +} + +func sampleNames(t *testing.T) []string { + dir, err := os.Open(datadir) + if err != nil { + t.Fatal(err) + } + defer dir.Close() + samples, err := dir.Readdirnames(-1) + if err != nil { + t.Fatal(err) + } + sort.Strings(samples) + return samples +} + +// TestEXIFCorrection tests that the input files with EXIF metadata +// are correctly automatically rotated/flipped when decoded. +func TestEXIFCorrection(t *testing.T) { + samples := sampleNames(t) + straightF := straightFImage(t) + for _, v := range samples { + if !strings.Contains(v, "exif") || strings.HasSuffix(v, "-s.jpg") { + continue + } + name := filepath.Join(datadir, v) + t.Logf("correcting %s with EXIF Orientation", name) + f, err := os.Open(name) + if err != nil { + t.Fatal(err) + } + defer f.Close() + im, _, err := Decode(f, nil) + if err != nil { + t.Fatal(err) + } + if !equals(im, straightF) { + t.Fatalf("%v not properly corrected with exif", name) + } + } +} + +// TestForcedCorrection tests that manually specifying the +// rotation/flipping to be applied when decoding works as +// expected. +func TestForcedCorrection(t *testing.T) { + samples := sampleNames(t) + straightF := straightFImage(t) + for _, v := range samples { + if strings.HasSuffix(v, "-s.jpg") { + continue + } + name := filepath.Join(datadir, v) + t.Logf("forced correction of %s", name) + f, err := os.Open(name) + if err != nil { + t.Fatal(err) + } + defer f.Close() + num := name[10] + angle, flipMode := 0, 0 + switch num { + case '1': + // nothing to do + case '2': + flipMode = 2 + case '3': + angle = 180 + case '4': + angle = 180 + flipMode = 2 + case '5': + angle = -90 + flipMode = 2 + case '6': + angle = -90 + case '7': + angle = 90 + flipMode = 2 + case '8': + angle = 90 + } + im, _, err := Decode(f, &DecodeOpts{Rotate: angle, Flip: FlipDirection(flipMode)}) + if err != nil { + t.Fatal(err) + } + if !equals(im, straightF) { + t.Fatalf("%v not properly corrected", name) + } + } +} + +// TestRescale verifies that rescaling an image, without +// any rotation/flipping, produces the expected image. +func TestRescale(t *testing.T) { + name := filepath.Join(datadir, "f1.jpg") + t.Logf("rescaling %s with half-width and half-height", name) + f, err := os.Open(name) + if err != nil { + t.Fatal(err) + } + defer f.Close() + rescaledIm, _, err := Decode(f, &DecodeOpts{ScaleWidth: 0.5, ScaleHeight: 0.5}) + if err != nil { + t.Fatal(err) + } + + smallIm := smallStraightFImage(t) + + gotB, wantB := rescaledIm.Bounds(), smallIm.Bounds() + if !gotB.Eq(wantB) { + t.Errorf("(scale) %v bounds not equal, got %v want %v", name, gotB, wantB) + } + if !equals(rescaledIm, smallIm) { + t.Errorf("(scale) %v pixels not equal", name) + } + + _, err = f.Seek(0, os.SEEK_SET) + if err != nil { + t.Fatal(err) + } + + rescaledIm, _, err = Decode(f, &DecodeOpts{MaxWidth: 2000, MaxHeight: 40}) + if err != nil { + t.Fatal(err) + } + gotB = rescaledIm.Bounds() + if !gotB.Eq(wantB) { + t.Errorf("(max) %v bounds not equal, got %v want %v", name, gotB, wantB) + } + if !equals(rescaledIm, smallIm) { + t.Errorf("(max) %v pixels not equal", name) + } +} + +// TestRescaleEXIF verifies that rescaling an image, followed +// by the automatic EXIF correction (rotation/flipping), +// produces the expected image. All the possible correction +// modes are tested. +func TestRescaleEXIF(t *testing.T) { + smallStraightF := smallStraightFImage(t) + samples := sampleNames(t) + for _, v := range samples { + if !strings.Contains(v, "exif") { + continue + } + name := filepath.Join(datadir, v) + t.Logf("rescaling %s with half-width and half-height", name) + f, err := os.Open(name) + if err != nil { + t.Fatal(err) + } + defer f.Close() + rescaledIm, _, err := Decode(f, &DecodeOpts{ScaleWidth: 0.5, ScaleHeight: 0.5}) + if err != nil { + t.Fatal(err) + } + + gotB, wantB := rescaledIm.Bounds(), smallStraightF.Bounds() + if !gotB.Eq(wantB) { + t.Errorf("(scale) %v bounds not equal, got %v want %v", name, gotB, wantB) + } + if !equals(rescaledIm, smallStraightF) { + t.Errorf("(scale) %v pixels not equal", name) + } + + _, err = f.Seek(0, os.SEEK_SET) + if err != nil { + t.Fatal(err) + } + rescaledIm, _, err = Decode(f, &DecodeOpts{MaxWidth: 2000, MaxHeight: 40}) + if err != nil { + t.Fatal(err) + } + + gotB = rescaledIm.Bounds() + if !gotB.Eq(wantB) { + t.Errorf("(max) %v bounds not equal, got %v want %v", name, gotB, wantB) + } + if !equals(rescaledIm, smallStraightF) { + t.Errorf("(max) %v pixels not equal", name) + } + } +} + +// TestUpscale verifies we don't resize up. +func TestUpscale(t *testing.T) { + b := new(bytes.Buffer) + w, h := 64, 48 + if err := jpeg.Encode(b, image.NewNRGBA(image.Rect(0, 0, w, h)), nil); err != nil { + t.Fatal(err) + } + sizes := []struct { + mw, mh int + wantW, wantH int + }{ + {wantW: w, wantH: h}, + {mw: w, mh: h, wantW: w, wantH: h}, + {mw: w, mh: 2 * h, wantW: w, wantH: h}, + {mw: 2 * w, mh: w, wantW: w, wantH: h}, + {mw: 2 * w, mh: 2 * h, wantW: w, wantH: h}, + {mw: w / 2, mh: h / 2, wantW: w / 2, wantH: h / 2}, + {mw: w / 2, mh: 2 * h, wantW: w / 2, wantH: h / 2}, + {mw: 2 * w, mh: h / 2, wantW: w / 2, wantH: h / 2}, + } + for i, size := range sizes { + var opts DecodeOpts + switch { + case size.mw != 0 && size.mh != 0: + opts = DecodeOpts{MaxWidth: size.mw, MaxHeight: size.mh} + case size.mw != 0: + opts = DecodeOpts{MaxWidth: size.mw} + case size.mh != 0: + opts = DecodeOpts{MaxHeight: size.mh} + } + im, _, err := Decode(bytes.NewReader(b.Bytes()), &opts) + if err != nil { + t.Error(i, err) + } + gotW := im.Bounds().Dx() + gotH := im.Bounds().Dy() + if gotW != size.wantW || gotH != size.wantH { + t.Errorf("%d got %dx%d want %dx%d", i, gotW, gotH, size.wantW, size.wantH) + } + } +} + +// TODO(mpl): move this test to the goexif lib if/when we contribute +// back the DateTime stuff to upstream. +func TestDateTime(t *testing.T) { + f, err := os.Open(filepath.Join(datadir, "f1-exif.jpg")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + ex, err := exif.Decode(f) + if err != nil { + t.Fatal(err) + } + got, err := ex.DateTime() + if err != nil { + t.Fatal(err) + } + exifTimeLayout := "2006:01:02 15:04:05" + want, err := time.ParseInLocation(exifTimeLayout, "2012:11:04 05:42:02", time.Local) + if err != nil { + t.Fatal(err) + } + if got != want { + t.Fatalf("Creation times differ; got %v, want: %v\n", got, want) + } +} + +var issue513tests = []image.Rectangle{ + // These test image bounds give a fastjpeg.Factor() result of 1 since + // they give dim/max == 1, but require rescaling. + image.Rect(0, 0, 500, 500), // The file, bug.jpeg, in issue 315 is a black 500x500. + image.Rect(0, 0, 1, 257), + image.Rect(0, 0, 1, 511), + image.Rect(0, 0, 2001, 1), + image.Rect(0, 0, 3999, 1), + + // These test image bounds give either a fastjpeg.Factor() > 1 or + // do not require rescaling. + image.Rect(0, 0, 1, 256), + image.Rect(0, 0, 1, 512), + image.Rect(0, 0, 2000, 1), + image.Rect(0, 0, 4000, 1), +} + +// Test that decode does not hand off a nil image when using +// fastjpeg, and fastjpeg.Factor() == 1. +// See https://camlistore.org/issue/513 +func TestIssue513(t *testing.T) { + opts := &DecodeOpts{MaxWidth: 2000, MaxHeight: 256} + for _, rect := range issue513tests { + buf := &bytes.Buffer{} + err := jpeg.Encode(buf, image.NewRGBA(rect), nil) + if err != nil { + t.Fatalf("Failed to encode test image: %v", err) + } + func() { + defer func() { + if r := recover(); r != nil { + t.Errorf("Unexpected panic for image size %dx%d: %v", rect.Dx(), rect.Dy(), r) + } + }() + _, format, err, needsRescale := decode(buf, opts, false) + if err != nil { + t.Errorf("Unexpected error for image size %dx%d: %v", rect.Dx(), rect.Dy(), err) + } + if format != "jpeg" { + t.Errorf("Unexpected format for image size %dx%d: got %q want %q", rect.Dx(), rect.Dy(), format, "jpeg") + } + if needsRescale != (rect.Dx() > opts.MaxWidth || rect.Dy() > opts.MaxHeight) { + t.Errorf("Unexpected rescale for image size %dx%d: needsRescale = %t", rect.Dx(), rect.Dy(), needsRescale) + } + }() + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/resize/bench_test.go b/vendor/github.com/camlistore/camlistore/pkg/images/resize/bench_test.go new file mode 100644 index 00000000..875290f1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/images/resize/bench_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resize + +import ( + "image" + "testing" +) + +func resize(m image.Image) { + s := m.Bounds().Size().Div(2) + Resize(m, m.Bounds(), s.X, s.Y) +} + +func halve(m image.Image) { + HalveInplace(m) +} + +func BenchmarkResizeRGBA(b *testing.B) { + m := image.NewRGBA(orig) + b.ResetTimer() + for i := 0; i < b.N; i++ { + resize(m) + } +} + +func BenchmarkHalveRGBA(b *testing.B) { + m := image.NewRGBA(orig) + b.ResetTimer() + for i := 0; i < b.N; i++ { + halve(m) + } +} + +func BenchmarkResizeYCrCb(b *testing.B) { + m := image.NewYCbCr(orig, image.YCbCrSubsampleRatio422) + b.ResetTimer() + for i := 0; i < b.N; i++ { + resize(m) + } +} + +func BenchmarkHalveYCrCb(b *testing.B) { + m := image.NewYCbCr(orig, image.YCbCrSubsampleRatio422) + b.ResetTimer() + for i := 0; i < b.N; i++ { + halve(m) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/resize/resize.go b/vendor/github.com/camlistore/camlistore/pkg/images/resize/resize.go new file mode 100644 index 00000000..d9240408 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/images/resize/resize.go @@ -0,0 +1,320 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package resize resizes images. +package resize + +import ( + "image" + "image/color" + "image/draw" + + xdraw "golang.org/x/image/draw" +) + +// Resize returns a scaled copy of the image slice r of m. +// The returned image has width w and height h. +func Resize(m image.Image, r image.Rectangle, w, h int) image.Image { + if w < 0 || h < 0 { + return nil + } + if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 { + return image.NewRGBA64(image.Rect(0, 0, w, h)) + } + switch m := m.(type) { + case *image.RGBA: + return resizeRGBA(m, r, w, h) + case *image.YCbCr: + if m, ok := resizeYCbCr(m, r, w, h); ok { + return m + } + } + ww, hh := uint64(w), uint64(h) + dx, dy := uint64(r.Dx()), uint64(r.Dy()) + // The scaling algorithm is to nearest-neighbor magnify the dx * dy source + // to a (ww*dx) * (hh*dy) intermediate image and then minify the intermediate + // image back down to a ww * hh destination with a simple box filter. + // The intermediate image is implied, we do not physically allocate a slice + // of length ww*dx*hh*dy. + // For example, consider a 4*3 source image. Label its pixels from a-l: + // abcd + // efgh + // ijkl + // To resize this to a 3*2 destination image, the intermediate is 12*6. + // Whitespace has been added to delineate the destination pixels: + // aaab bbcc cddd + // aaab bbcc cddd + // eeef ffgg ghhh + // + // eeef ffgg ghhh + // iiij jjkk klll + // iiij jjkk klll + // Thus, the 'b' source pixel contributes one third of its value to the + // (0, 0) destination pixel and two thirds to (1, 0). + // The implementation is a two-step process. First, the source pixels are + // iterated over and each source pixel's contribution to 1 or more + // destination pixels are summed. Second, the sums are divided by a scaling + // factor to yield the destination pixels. + // TODO: By interleaving the two steps, instead of doing all of + // step 1 first and all of step 2 second, we could allocate a smaller sum + // slice of length 4*w*2 instead of 4*w*h, although the resultant code + // would become more complicated. + n, sum := dx*dy, make([]uint64, 4*w*h) + for y := r.Min.Y; y < r.Max.Y; y++ { + for x := r.Min.X; x < r.Max.X; x++ { + // Get the source pixel. + r32, g32, b32, a32 := m.At(x, y).RGBA() + r64 := uint64(r32) + g64 := uint64(g32) + b64 := uint64(b32) + a64 := uint64(a32) + // Spread the source pixel over 1 or more destination rows. + py := uint64(y-r.Min.Y) * hh + for remy := hh; remy > 0; { + qy := dy - (py % dy) + if qy > remy { + qy = remy + } + // Spread the source pixel over 1 or more destination columns. + px := uint64(x-r.Min.X) * ww + index := 4 * ((py/dy)*ww + (px / dx)) + for remx := ww; remx > 0; { + qx := dx - (px % dx) + if qx > remx { + qx = remx + } + sum[index+0] += r64 * qx * qy + sum[index+1] += g64 * qx * qy + sum[index+2] += b64 * qx * qy + sum[index+3] += a64 * qx * qy + index += 4 + px += qx + remx -= qx + } + py += qy + remy -= qy + } + } + } + return average(sum, w, h, n*0x0101) +} + +// average convert the sums to averages and returns the result. +func average(sum []uint64, w, h int, n uint64) image.Image { + ret := image.NewRGBA(image.Rect(0, 0, w, h)) + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + i := y*ret.Stride + x*4 + j := 4 * (y*w + x) + ret.Pix[i+0] = uint8(sum[j+0] / n) + ret.Pix[i+1] = uint8(sum[j+1] / n) + ret.Pix[i+2] = uint8(sum[j+2] / n) + ret.Pix[i+3] = uint8(sum[j+3] / n) + } + } + return ret +} + +// resizeYCbCr returns a scaled copy of the YCbCr image slice r of m. +// The returned image has width w and height h. +func resizeYCbCr(m *image.YCbCr, r image.Rectangle, w, h int) (image.Image, bool) { + dst := image.NewRGBA(image.Rect(0, 0, w, h)) + xdraw.ApproxBiLinear.Scale(dst, dst.Bounds(), m, m.Bounds(), xdraw.Src, nil) + return dst, true +} + +// resizeRGBA returns a scaled copy of the RGBA image slice r of m. +// The returned image has width w and height h. +func resizeRGBA(m *image.RGBA, r image.Rectangle, w, h int) image.Image { + ww, hh := uint64(w), uint64(h) + dx, dy := uint64(r.Dx()), uint64(r.Dy()) + // See comment in Resize. + n, sum := dx*dy, make([]uint64, 4*w*h) + for y := r.Min.Y; y < r.Max.Y; y++ { + pix := m.Pix[(y-m.Rect.Min.Y)*m.Stride:] + for x := r.Min.X; x < r.Max.X; x++ { + // Get the source pixel. + p := pix[(x-m.Rect.Min.X)*4:] + r64 := uint64(p[0]) + g64 := uint64(p[1]) + b64 := uint64(p[2]) + a64 := uint64(p[3]) + // Spread the source pixel over 1 or more destination rows. + py := uint64(y-r.Min.Y) * hh + for remy := hh; remy > 0; { + qy := dy - (py % dy) + if qy > remy { + qy = remy + } + // Spread the source pixel over 1 or more destination columns. + px := uint64(x-r.Min.X) * ww + index := 4 * ((py/dy)*ww + (px / dx)) + for remx := ww; remx > 0; { + qx := dx - (px % dx) + if qx > remx { + qx = remx + } + qxy := qx * qy + sum[index+0] += r64 * qxy + sum[index+1] += g64 * qxy + sum[index+2] += b64 * qxy + sum[index+3] += a64 * qxy + index += 4 + px += qx + remx -= qx + } + py += qy + remy -= qy + } + } + } + return average(sum, w, h, n) +} + +// HalveInplace downsamples the image by 50% using averaging interpolation. +func HalveInplace(m image.Image) image.Image { + b := m.Bounds() + switch m := m.(type) { + case *image.YCbCr: + for y := b.Min.Y; y < b.Max.Y/2; y++ { + for x := b.Min.X; x < b.Max.X/2; x++ { + y00 := uint32(m.Y[m.YOffset(2*x, 2*y)]) + y10 := uint32(m.Y[m.YOffset(2*x+1, 2*y)]) + y01 := uint32(m.Y[m.YOffset(2*x, 2*y+1)]) + y11 := uint32(m.Y[m.YOffset(2*x+1, 2*y+1)]) + // Add before divide with uint32 or we get errors in the least + // significant bits. + m.Y[m.YOffset(x, y)] = uint8((y00 + y10 + y01 + y11) >> 2) + + cb00 := uint32(m.Cb[m.COffset(2*x, 2*y)]) + cb10 := uint32(m.Cb[m.COffset(2*x+1, 2*y)]) + cb01 := uint32(m.Cb[m.COffset(2*x, 2*y+1)]) + cb11 := uint32(m.Cb[m.COffset(2*x+1, 2*y+1)]) + m.Cb[m.COffset(x, y)] = uint8((cb00 + cb10 + cb01 + cb11) >> 2) + + cr00 := uint32(m.Cr[m.COffset(2*x, 2*y)]) + cr10 := uint32(m.Cr[m.COffset(2*x+1, 2*y)]) + cr01 := uint32(m.Cr[m.COffset(2*x, 2*y+1)]) + cr11 := uint32(m.Cr[m.COffset(2*x+1, 2*y+1)]) + m.Cr[m.COffset(x, y)] = uint8((cr00 + cr10 + cr01 + cr11) >> 2) + } + } + b.Max = b.Min.Add(b.Size().Div(2)) + return subImage(m, b) + case draw.Image: + for y := b.Min.Y; y < b.Max.Y/2; y++ { + for x := b.Min.X; x < b.Max.X/2; x++ { + r00, g00, b00, a00 := m.At(2*x, 2*y).RGBA() + r10, g10, b10, a10 := m.At(2*x+1, 2*y).RGBA() + r01, g01, b01, a01 := m.At(2*x, 2*y+1).RGBA() + r11, g11, b11, a11 := m.At(2*x+1, 2*y+1).RGBA() + + // Add before divide with uint32 or we get errors in the least + // significant bits. + r := (r00 + r10 + r01 + r11) >> 2 + g := (g00 + g10 + g01 + g11) >> 2 + b := (b00 + b10 + b01 + b11) >> 2 + a := (a00 + a10 + a01 + a11) >> 2 + + m.Set(x, y, color.RGBA{ + R: uint8(r >> 8), + G: uint8(g >> 8), + B: uint8(b >> 8), + A: uint8(a >> 8), + }) + } + } + b.Max = b.Min.Add(b.Size().Div(2)) + return subImage(m, b) + default: + // TODO(wathiede): fallback to generic Resample somehow? + panic("Unhandled image type") + } +} + +// ResampleInplace will resample m inplace, overwritting existing pixel data, +// and return a subimage of m sized to w and h. +func ResampleInplace(m image.Image, r image.Rectangle, w, h int) image.Image { + // We don't support scaling up. + if r.Dx() < w || r.Dy() < h { + return m + } + + switch m := m.(type) { + case *image.YCbCr: + xStep := float64(r.Dx()) / float64(w) + yStep := float64(r.Dy()) / float64(h) + for y := r.Min.Y; y < r.Min.Y+h; y++ { + for x := r.Min.X; x < r.Min.X+w; x++ { + xSrc := int(float64(x) * xStep) + ySrc := int(float64(y) * yStep) + cSrc := m.COffset(xSrc, ySrc) + cDst := m.COffset(x, y) + m.Y[m.YOffset(x, y)] = m.Y[m.YOffset(xSrc, ySrc)] + m.Cb[cDst] = m.Cb[cSrc] + m.Cr[cDst] = m.Cr[cSrc] + } + } + case draw.Image: + xStep := float64(r.Dx()) / float64(w) + yStep := float64(r.Dy()) / float64(h) + for y := r.Min.Y; y < r.Min.Y+h; y++ { + for x := r.Min.X; x < r.Min.X+w; x++ { + xSrc := int(float64(x) * xStep) + ySrc := int(float64(y) * yStep) + r, g, b, a := m.At(xSrc, ySrc).RGBA() + m.Set(x, y, color.RGBA{ + R: uint8(r >> 8), + G: uint8(g >> 8), + B: uint8(b >> 8), + A: uint8(a >> 8), + }) + } + } + default: + // TODO fallback to generic Resample somehow? + panic("Unhandled image type") + } + r.Max.X = r.Min.X + w + r.Max.Y = r.Min.Y + h + return subImage(m, r) +} + +func subImage(m image.Image, r image.Rectangle) image.Image { + type subImager interface { + SubImage(image.Rectangle) image.Image + } + if si, ok := m.(subImager); ok { + return si.SubImage(r) + } + panic("Image type doesn't support SubImage") +} + +// Resample returns a resampled copy of the image slice r of m. +// The returned image has width w and height h. +func Resample(m image.Image, r image.Rectangle, w, h int) image.Image { + if w < 0 || h < 0 { + return nil + } + if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 { + return image.NewRGBA64(image.Rect(0, 0, w, h)) + } + img := image.NewRGBA(image.Rect(0, 0, w, h)) + xStep := float64(r.Dx()) / float64(w) + yStep := float64(r.Dy()) / float64(h) + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + xSrc := int(float64(r.Min.X) + float64(x)*xStep) + ySrc := int(float64(r.Min.Y) + float64(y)*yStep) + r, g, b, a := m.At(xSrc, ySrc).RGBA() + img.SetRGBA(x, y, color.RGBA{ + R: uint8(r >> 8), + G: uint8(g >> 8), + B: uint8(b >> 8), + A: uint8(a >> 8), + }) + } + } + return img +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/resize/resize_test.go b/vendor/github.com/camlistore/camlistore/pkg/images/resize/resize_test.go new file mode 100644 index 00000000..1d4358ad --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/images/resize/resize_test.go @@ -0,0 +1,366 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resize + +import ( + "flag" + "fmt" + "image" + "image/color" + "image/draw" + "image/png" + "io" + "math" + "os" + "path/filepath" + "strings" + "testing" +) + +const ( + // psnrThreshold is the threshold over which images must match to consider + // HalveInplace equivalent to Resize. It is in terms of dB and 60-80 is + // good for RGB. + psnrThreshold = 50.0 + + // TODO(wathiede, mpl): figure out why we got an increase from ~3% to ~16% for + // YCbCr images in Go 1.5. That is, for halving vs resizing. + maxPixelDiffPercentage = 10 +) + +var ( + output = flag.String("output", "", "If non-empty, the directory to save comparison images.") + + orig = image.Rect(0, 0, 1024, 1024) + thumb = image.Rect(0, 0, 64, 64) +) + +var somePalette = []color.Color{ + color.RGBA{0x00, 0x00, 0x00, 0xff}, + color.RGBA{0x00, 0x00, 0x44, 0xff}, + color.RGBA{0x00, 0x00, 0x88, 0xff}, + color.RGBA{0x00, 0x00, 0xcc, 0xff}, + color.RGBA{0x00, 0x44, 0x00, 0xff}, + color.RGBA{0x00, 0x44, 0x44, 0xff}, + color.RGBA{0x00, 0x44, 0x88, 0xff}, + color.RGBA{0x00, 0x44, 0xcc, 0xff}, +} + +func makeImages(r image.Rectangle) []image.Image { + return []image.Image{ + image.NewGray(r), + image.NewGray16(r), + image.NewNRGBA(r), + image.NewNRGBA64(r), + image.NewPaletted(r, somePalette), + image.NewRGBA(r), + image.NewRGBA64(r), + image.NewYCbCr(r, image.YCbCrSubsampleRatio444), + image.NewYCbCr(r, image.YCbCrSubsampleRatio422), + image.NewYCbCr(r, image.YCbCrSubsampleRatio420), + image.NewYCbCr(r, image.YCbCrSubsampleRatio440), + image.NewYCbCr(r, image.YCbCrSubsampleRatio410), + image.NewYCbCr(r, image.YCbCrSubsampleRatio411), + } +} + +func TestResize(t *testing.T) { + for i, im := range makeImages(orig) { + m := Resize(im, orig, thumb.Dx(), thumb.Dy()) + got, want := m.Bounds(), thumb + if !got.Eq(want) { + t.Error(i, "Want bounds", want, "got", got) + } + } +} + +func TestResampleInplace(t *testing.T) { + for i, im := range makeImages(orig) { + m := ResampleInplace(im, orig, thumb.Dx(), thumb.Dy()) + got, want := m.Bounds(), thumb + if !got.Eq(want) { + t.Error(i, "Want bounds", want, "got", got) + } + } +} + +func TestResample(t *testing.T) { + for i, im := range makeImages(orig) { + m := Resample(im, orig, thumb.Dx(), thumb.Dy()) + got, want := m.Bounds(), thumb + if !got.Eq(want) { + t.Error(i, "Want bounds", want, "got", got) + } + } + + for _, d := range []struct { + wantFn string + r image.Rectangle + w, h int + }{ + { + // Generated with imagemagick: + // $ convert -crop 128x128+320+160 -resize 64x64 -filter point \ + // testdata/test.png testdata/test-resample-128x128-64x64.png + wantFn: "test-resample-128x128-64x64.png", + r: image.Rect(320, 160, 320+128, 160+128), + w: 64, + h: 64, + }, + { + // Generated with imagemagick: + // $ convert -resize 128x128 -filter point testdata/test.png \ + // testdata/test-resample-768x576-128x96.png + wantFn: "test-resample-768x576-128x96.png", + r: image.Rect(0, 0, 768, 576), + w: 128, + h: 96, + }, + } { + m := image.NewRGBA(testIm.Bounds()) + fillTestImage(m) + r, err := os.Open(filepath.Join("testdata", d.wantFn)) + if err != nil { + t.Fatal(err) + } + defer r.Close() + want, err := png.Decode(r) + if err != nil { + t.Fatal(err) + } + got := Resample(m, d.r, d.w, d.h) + res := compareImages(got, want) + t.Logf("PSNR %.4f", res.psnr) + s := got.Bounds().Size() + tot := s.X * s.Y + per := float32(100*res.diffCnt) / float32(tot) + t.Logf("Resample not the same %d pixels different %.2f%%", res.diffCnt, per) + if *output != "" { + err = savePng(t, want, fmt.Sprintf("Resample.%s->%dx%d.want.png", + d.r, d.w, d.h)) + if err != nil { + t.Fatal(err) + } + err = savePng(t, got, fmt.Sprintf("Resample.%s->%dx%d.got.png", + d.r, d.w, d.h)) + if err != nil { + t.Fatal(err) + } + err = savePng(t, res.diffIm, + fmt.Sprintf("Resample.%s->%dx%d.diff.png", d.r, d.w, d.h)) + if err != nil { + t.Fatal(err) + } + } + } +} + +func TestHalveInplace(t *testing.T) { + for i, im := range makeImages(orig) { + m := HalveInplace(im) + b := im.Bounds() + got, want := m.Bounds(), image.Rectangle{ + Min: b.Min, + Max: b.Min.Add(b.Max.Div(2)), + } + if !got.Eq(want) { + t.Error(i, "Want bounds", want, "got", got) + } + } +} + +type results struct { + diffCnt int + psnr float64 + diffIm *image.Gray +} + +func compareImages(m1, m2 image.Image) results { + b := m1.Bounds() + s := b.Size() + res := results{} + mse := uint32(0) + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + r1, g1, b1, a1 := m1.At(x, y).RGBA() + r2, g2, b2, a2 := m2.At(x, y).RGBA() + + mse += ((r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2)) / 3 + if r1 != r2 || g1 != g2 || b1 != b2 || a1 != a2 { + if res.diffIm == nil { + res.diffIm = image.NewGray(m1.Bounds()) + } + res.diffCnt++ + res.diffIm.Set(x, y, color.White) + } + } + } + mse = mse / uint32(s.X*s.Y) + res.psnr = 20*math.Log10(1<<16) - 10*math.Log10(float64(mse)) + return res +} + +var testIm image.Image + +func init() { + r, err := os.Open(filepath.Join("testdata", "test.png")) + if err != nil { + panic(err) + } + defer r.Close() + testIm, err = png.Decode(r) +} + +func fillTestImage(im image.Image) { + b := im.Bounds() + if !b.Eq(testIm.Bounds()) { + panic("Requested target image dimensions not equal reference image.") + } + src := testIm + if dst, ok := im.(*image.YCbCr); ok { + b := testIm.Bounds() + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + r, g, b, _ := src.At(x, y).RGBA() + yp, cb, cr := color.RGBToYCbCr(uint8(r), uint8(g), uint8(b)) + + dst.Y[dst.YOffset(x, y)] = yp + off := dst.COffset(x, y) + dst.Cb[off] = cb + dst.Cr[off] = cr + } + } + return + } + draw.Draw(im.(draw.Image), b, testIm, b.Min, draw.Src) +} + +func savePng(t *testing.T, m image.Image, fn string) error { + fn = filepath.Join(*output, fn) + t.Log("Saving", fn) + f, err := os.Create(fn) + if err != nil { + return err + } + defer f.Close() + + return png.Encode(f, m) +} + +func getFilename(im image.Image, method string) string { + imgType := fmt.Sprintf("%T", im) + imgType = imgType[strings.Index(imgType, ".")+1:] + if m, ok := im.(*image.YCbCr); ok { + imgType += "." + m.SubsampleRatio.String() + } + return fmt.Sprintf("%s.%s.png", imgType, method) +} + +func TestCompareResizeToHalveInplace(t *testing.T) { + if testing.Short() { + t.Skip("Skipping TestCompareNewResizeToHalveInplace in short mode.") + } + testCompareResizeMethods(t, "resize", "halveInPlace") +} + +var resizeMethods = map[string]func(image.Image) image.Image{ + "resize": func(im image.Image) image.Image { + s := im.Bounds().Size() + return Resize(im, im.Bounds(), s.X/2, s.Y/2) + }, + "halveInPlace": func(im image.Image) image.Image { + return HalveInplace(im) + }, +} + +func testCompareResizeMethods(t *testing.T, method1, method2 string) { + images1, images2 := []image.Image{}, []image.Image{} + var imTypes []string + for _, im := range makeImages(testIm.Bounds()) { + // keeping track of the types for the final output + imTypes = append(imTypes, fmt.Sprintf("%T", im)) + fillTestImage(im) + images1 = append(images1, resizeMethods[method1](im)) + } + for _, im := range makeImages(testIm.Bounds()) { + fillTestImage(im) + images2 = append(images2, resizeMethods[method2](im)) + } + var ( + f io.WriteCloser + err error + ) + if *output != "" { + os.Mkdir(*output, os.FileMode(0777)) + f, err = os.Create(filepath.Join(*output, "index.html")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + fmt.Fprintf(f, ` + + + + Image comparison for `+method1+` vs `+method2+` + + + +`) + } + for i, im1 := range images1 { + im2 := images2[i] + res := compareImages(im1, im2) + if *output != "" { + fmt.Fprintf(f, "") + fn := getFilename(im1, "halve") + err := savePng(t, im1, fn) + if err != nil { + t.Fatal(err) + } + fmt.Fprintf(f, `

    %s`, fn, fn) + + fn = getFilename(im1, "resize") + err = savePng(t, im2, fn) + if err != nil { + t.Fatal(err) + } + fmt.Fprintf(f, `

    %s`, fn, fn) + + if res.diffIm != nil { + fn = getFilename(im1, "diff") + err = savePng(t, res.diffIm, fn) + if err != nil { + t.Fatal(err) + } + fmt.Fprintf(f, `

    %s`, fn, fn) + } + fmt.Fprintln(f) + } + + if res.psnr < psnrThreshold { + t.Errorf("%v PSNR too low %.4f", imTypes[i], res.psnr) + } else { + t.Logf("%v PSNR %.4f", imTypes[i], res.psnr) + } + s := im1.Bounds().Size() + tot := s.X * s.Y + if per := float32(100*res.diffCnt) / float32(tot); per > maxPixelDiffPercentage { + t.Errorf("%v not the same %d pixels different %.2f%%", imTypes[i], res.diffCnt, per) + } + } + +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/resize/testdata/test-resample-128x128-64x64.png b/vendor/github.com/camlistore/camlistore/pkg/images/resize/testdata/test-resample-128x128-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..7663ba5f8e89886cb789fd6503816b3075f703e4 GIT binary patch literal 437 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 ? z%I`b<-7UoV`M9b7R9BS66Z@OUVz!DKgG6`HQLbMUz4}s|9z5G^cF64lAd8p-$~`&A zH2c7drI*Uf+U_oW>fW`-tEYNr^6u-k5oVUXtyT#M32N*sBwdLgtfqz}7|R{< zDv@2XP_6Os>9|E4g6q@jHlT!ssm>n|@gL(hn>o8}-+=?^B=%iOhA&E%P9D83tZd;w z^S8l&q9yGz&$_;jFx4;TnAn@f`OD2wMdT%bOYqXQU_|x#PG&m#Ri`BnZhIOpIz^Y3b~UuBjwlj7~s6>G2PD63GK%2J;_0`^A6Q{hc_8^d}h8=QDqb z$)fY-q?bR2`J@3Agg+>o_c?r~E8@zPD@OkQ&sFBej6cS!#yhB}v7?$cw$A^RBj+tl zWQ$j0hTZ3T)48;1so$+-R!GMWRHiz48oqpq31ut)iD@<1xO%mEn9|6li%SGjQB#2K z{D)pJ`(T0J30%x)VrG`MS9Q)LK43ay(PF-Y;f{*_bL^7Ua}%w(2VI`;Cj_#pYpW1X z&;>(^5?m)MyFZ*1cwn`NsHpx|oT~L9A_3_mBkr$yDM6=}A}@=l&T%B*Sb5g=;N8BY z=-isU+duWUjS8~CCA1yC74}CSbT$Og7t1IuwIo6H#P;3NNMXj!&d3WsBk$Hl;0`$7 zzO(XXcuKQN*s)#z3 z)?1fW)#cthQYB?};J_W{qTaRF1>_9ac{f*=oEcqDPpzla()+&T++AxBo4)h>BAsy5 zbXmJkf+YD#l?A6heQ`a%s1C)^<5Njt_BUsIy3sq;YZ1t`OkD* zKDgMW-lm7iBr%h0hYSy7J>A?opISzn8^kq?Ei7VsFH!Ug7+ z4H)5Raj%jfG4$(0bTVl8BV#$PbL5it%>_Bm&$@W$I@hV*tm-n-)Vx3r;Z&WyK}T_Q zk|*}~rS~yzhZzt^F-f%uh{ODH&aprIM)f5Q7nv+svLw}p-hNd(All*{I^VILcAEX0 z{7axjk~)1~AE(jJa)*RH!O*Dpa`wEQ|2Pi6SqIsHWwsOP5%#3n~~zSp_^h zBpK!Cv4rvd;ncuVz{C9g*`-Cr`*|sc0QLCeGsz zy&#B-Tev4qDeUA1)9>Q9{p>zR)$~n?IUPT7*$>nNaBp6=JWNm_DLg#fxM)*UWaO{1 z7~40Eghzh|YWolIpA=Twfoh5YzGeH-ua-0S2#ANc+}j;-9s{oOgwE$XH5!1dIi5IS za_rcB5QmhaBE^m=J^O~t2!N7*gxcuIFwa_-u-#Y8zGX$8s=#F~81UL&26+-)ai_bQLy zhN2M>7nk$>`yt3L(nnr0>c`0Fc>MZcXkcKV78VD>Qv9b#cZH>QVoItAe{RXhdxw@U zdv$f|s-()Y@mwv)l5`(etpEQ@C31bXSYS(+H|@P? z+csk|8j!^>8YdM$>Bx3_UkwR5E82Fdcp;R&7rGG+2EyX*?oKd5flR#R)t9KOR`E%#!_3v)i2b643p7@>|FgJ}~Eo+%Y{1Px;b>Ve` zR4k-W>urXHhB5MPNu-`!xNyM%l!E|2@%Yn}bzqihPHl{=ljo}?ay@CPa~Y(bkeu|` z?(=aAE^FguRQYbnqMp@rv)!s@D9k|g$#OruccFGTKhCMAqry$_m$KO|sp*+k+5?oD z!NLo?$3Mm#dlzwonw7`>87^PGJU#U}@WHBG7q!E`y}5oA0< z>Qdj`T1RFFk#~h;q~{YqdCRhw$I2Nv)4|nZ6g*O)rlsMHJ8o|mX0*Oo=Sfi&9*S|(y?X;Q z0hJA*({9|nxn3%8O84WPE$3gTQdM6IPTs{hs5~EjD**fzBYotYrQfh7jqMnf9fGIO zc67`<{wX$tEH_|NQe_UGZt=3%F-5PYo0Xf}DBx0(h>_g8u?oV;DHg6Y`#fKWd zRfw~kwv&$9Tzn)*RI`%dWs`{nkse$zw4$+?hBb28)Z~6R+w&R;+V6lKI)rg?U zLdDyMY+Lm6$|f=B09CGAMP&?u)|c|~rV@BycGxRIEF(Q#8=UlM>Eus#JWawBpw7bj zgJix?#X(sVX|tnC)-kAaGEw$)$nZXYo^@l*%Vi*TjOq+xkXR7IYE>4Gk6*X+{Vl6w zWNVvpFJRh1KUV%GbMRSS4LU03E7YvjY4aRawm& zAq@K(bK}YSz@oepx`)kcV1hE~qTLX`-TAw{A}`f+57Gs=TPA??Q|N|8eVJKf zppKaUehBdbEjsE`(5v=pglf7KZMtz@Tm>m66D%H!NHjhBz(aLTrJ*3ylZN8cF3*Qv zvSUc)gxK?BA>zhB1s>~34s-nZ`f_-RndO-`ffw-d-hf+Q2qVcYo43I8)C~sGd;zqg zLz$Y;-jH7c!sqk%s~w?nSS}lO>^O*QEIB$ymk>>KvxgPJb)*OYiz$y(u9^P&Ibdzb zt0Rj1);pF_ZOag}B+ZeUg2x(1rnARQQf=teD(&s=9sOL@Ki&t`=Sy;!N+vFZ zw>bzxlqQP&yv^Yx#T(x6aaylEf7Y)aa;yV2rKM z>FQ@q9yg|&pwXjlbI-G>A+UfeoofJ!QhsgYr$h62$O z=x;2W&wo((lTAq3$AmW5O3MMKj=>t3UH#AV3Cor<-Eo_F*kV!UCas7lgJoNE zdhh{2ao_yRVB@E_9f)lz_99T;WPBsPYCs9esaUM1gKUOFDKVm+VSN*6G*oEELg`9R zuk_rEfS(+z+dDql5mwbP+>@F0zzDewu$(-nJcpn>S9KcyLDC=?2U8(kM$y)tmnYBeJYO zOeQAv4m7{8%a?~+W%GXauX79i9NDnY;BiO6ew59M775pCLGXM{HTH5$)@@);tGPZi ztZ4SmM24faDLO^deOa=n{~UTg`e_GgE&}tG&5!5q$3!Eht36|H`Jk?e>p*?p8rwO5 z6t)Lr<}NRonVoIo<~AHAIojK>SVyGbgxdL$2L02DmfM7qkI5R`9J}7TOyc-JNTYq< zVyF3Q4w6gwofXxu#$+&$B|g|9l+4tn8{=84+6%GuN5ITUqJSPOI8mc7*OG#`$Ujj+ z9enPS56V48n{TBVgR28z15Y|tC+zg;ys7o%SQYOvJ3G=wEgk*>Vzpw(CWLvqKplWT zCAE3EY03ScRVL=zld<=|k+Xec^$ zC;RQ5V>H2tUwZd1MwiMb`&VVfu5uOyaAG5O_eTgi^m}N(+e85Vm^lQi(+!i!qeHEC z8vgmF5405F!M%(MzTMPxfN0%At z0PCp}1+gJ<7c(t_cAl9tCX|t~KN7+rREq>Xj$Ta`FqH*_q$1DWql&!MN$a7OLpYmP zprH;Nnqm&(IyE_Tgs3u*L=6FxRHl2@0D9cPQE@3%o(GP9 z7@c!cf{RZ5CS_`qg2GqN%?X+SG+6=YrcK_E4fZir)ec`(1h7{NB@6Ho$y8c_c zMW`Vg4rmI{yd05)NK#dq9omJe`VEJE#UpqEt{&*)pdVhEj6($2w0iYR&&{H3 z7&v}FN2vJdDJr^mUdnmu%}m{S=+dfkedV^@%$&klagpFJ3t)|xYu_@8@d6M932SuR z&`$gUxw^ioi4?i=i>C&E5Jo*|#yb5sGTR!8}dOnc{vYHGLWDd)TnJjtnxN^Z3A>q1MmJt8Q*VaCSbrw zu`qEekKN#vvdl(n83J1?&4t@q4oB{Rs0(#KO!oJ;1BQl20D>U2R}|LI!cW|=uT$C9O}AUD$a`Tex@ z!NrSau_km}K^UA`fiDj!+8bZO_QVli!X8QJ06(+G_YsRX^#?Lw@mrWt4s%e8pk0$`VQ`>D|qF!LKnt6o79 zP)A#P-@l+TA7-Yk1q9@r>(S})(5VHePQzegT4M!q*`K&<|2S#HEi?^qS!2Yv(p)=d z;68Nf=Rtfok(Dpj#<+|+up57(CAK2Dz3&+2BQM$OJ3Cx44kEK&TwId?)t4_{>S@u3 z?xG74mya_LU$<_wT8b+FOTaQDfGqU4h&%?abQ}>a$VP2m1qTiIN@hkz4VmN$jBhq@ zbHAPHOf(k7IKjz89PiZ*5@Dj=qIsNwF3kKH+Q1k)#j=PtS)odnU}djoAH;D(Fh&H5 z7zX(O=m>;JDIM?*+cYamZQq&ssqTFHn$<**g3UsSlhRF~%K6gjdRqswva|KHLIt;w zkqv@WRZr+3ltBB+8>@WAYP4|C^^|lPn8k+~W{UAS=ofg7A^4?FPhyDYJMpMHp}|}$ z?#0CkRWls7wbc#>90UYkQEkKg63WCzofx>7e8zf?EU0-_yLVrt_jk?~+rHh@&CQM6 z5P{zWaK=>+aRvsay}g&$-K*Y|Tat8DvPc^l+Rp#`z`%*F?rvt$Bi1Nxumbhc?8k>q zJv!MZ;u6%Hpn%5oH=R{i*@kY^T>spP<;yeRMsQtCQ8BF_qPG$E$Kg|7kNFJ0S>XlA zBX*oA5BTcfeEZR?Fgx`vR+G?%mRB=OTf7I_uzt6Lg34wDnVEFC|HpXM^UWzYLX2tp zJLCJWGKL|q+T$mz%*`{v&#AQxRCy7ZYNU8a5Ho&R$J=Z5V^SdKk7tKYeL3i2KEe^y zP;n6Sl3&3+MXW#UY0Mg{$@dYPV1NPn1`D!>mK3>}Li4#A0^bw#J{EjDfRd!rVYxiE zEqzQDBoJB_G;y@EQaj+(NJ8%O0`Ro|^ZnWMN0)$P!WkPM9Jcc-iP&Uj@rZ(Zs%zL= z#=TR!>(?)ft*sAZ)`)P@CBMw1w>T#1SiF!sT;YuhU8F`r{^Q@H?eVY$_cTq zx=0_y+YUVw{O16;OZ_D!;cbtP%7YQrgSZ|$v#ZImyO4J%? zscQk3BqFoNgfdV;CxP0_YLTtH8uptxz)F+aAzBHv!v)!#AkAI7&T|MW*(-9Ob8l*D z`pb%gbY}MhzAav-E}e)3Csh24hVxN=tWh+nzj6Tm5>};$NU0#n2Dc`Mks0|1SWMB+ zAR)wfJ;OLH6V2*d>-L&{zNd^MZzSMIn60uf`p6@;eVi0U_ZMZ4l+hbiUA5ajE7>S= zoCC$Odo$7I0@D-1Mh}Trv`1<5!!}~yAl*9v&u^GKIyUN4(?iaAVMWhMPeF=0X9TMh z2`QagFxMQ!$*arOr85{&3};N#;ZxDQd^kb|{ZfFQ1z(j^+O2j$1J2eh^2dnk zis)N7M5&d{4=wBZOWbP~r=4xt2eFax+|kdfR*-U2!MlTOzN6S<&86Cu?_N($KEXCbiZGbb-l^aN^b>Vz@4A#akhh;S zL!N%d9%Rg`;#i;l&y^Z3178ZvV&bHU(c0(>Seo_EYdU3FhQ1DJO>8JkRRq5i)C)`w zDthf0-g})KPDM@B4F|ZD%5BUdO<8yykaf4j=sx|Cc) z-KhQ&a)F+f{T9nP(^{pvEfyany&A8FK1;QElKpmv*h%TQq&Kq_CGmX^TU8ttcVBg^ zrs}vY>g!@s&p>tOldz9U5zq^<-$DaIBI;J%7Hi}7t1F~9dg%Nm?xQ~s>`834?M=MP zdvcYzh!qP9L+95Bp_MBRxt;}c@~e#q%>Hqkj$&C3ZaCgbcOwN=ht%jy2;uNVkOUzX z^{-&Ue0AeL9X3?_cm(`_Ad;E{(!T)7Be*5+yE!3r?L2$~O zg|A|zylVo{YOo+;J%^BWvQI)YCr}2Cfll{ldKG7x3@O)Gbf>k$#hYOYot%IR?g`Q? zsE#H46LLb#P>F+DpLO5MLI;PmMW)e(CLoZq>p`RIrr326RZD|)uapJD23aPj9?XA6 z2Ny=mh44%zVGXJTwVbH(8lQ~`8fKaikzjr6NK<5IZFGg9kK zMk1va?)f{v@jWmjlz+H)<4A68+ni(mcsI;YMEFH(p;tZ+4K1()udWC)0A7fdDqBen zWmE&1A~wQ>RN*4OBs-X%BEBm0nh3$q%*-U|m7*Jg8!fA(=@1)UO`c-tlM8+2u}ft<};wLTTiV5+R#M~C$@R8lZE;GIB->}bRWu?w&V&od?+(CW)>yeU&Gr^wGMX&W-_<1Ls3&cbTRdKsN2*g5WecA3cgb%S*(YdoQ4_;z_>7z5(8)hqeT`$d^2!b*ST3!OrozRg{RGn zJTux+U&@04sK}@T^6_s!ai>>aI*6W2o!EPj!C>VwLuZ=YklHmrKT)O|9x!q%9;3yB z(Rw|xcH((29i?Uu%k5n(k=ys~JwURkbE|%dpI;(Ou_~e<#-r2gNMMEZ2RWfT%5><> zh-w^ieZfSGnT=L~Ibg>m8wVY!iXz9u^&1&$a4|SB__5IHi<}~oI>h<4cpYNX)D^N2 zF;W5p8Drf6NU$eS!9kF_iDfN6Kc86qNYs_DMvqxihlElrqr_!;E_!0cL!O!&ji;G- zdFB5*h_sSOXol-b!heCNlL*G4emOrHM{ql_40NSoe2W0Kf|^6_mPj!wpspY_6CGbp zu92)$>n)|-4E~?lS5F#J=e~niZjP$DPwYg5dy~<-Y7QY1atz_e>FMh+MBrsR|DeAY zDMk!d7s;NMO#mE3fB+i2vuGKB@XpPOiWUsa9Q81m=b!p*u`o!Qgq(+aqHoM^a4r(z zfAsvKCE)-}sC-7&gG9}VKM1 z?#fgs^6~$aHP!iHPE;zrCo{QMt}Xt)+Y$Qpk|xf9UwLv$`)NXQco?U#Jt3UTrwlS) zwX^gL_ZibdEJE%x47=&%AN2%qZGz8 z;LV{0{IZGRREgTbhet2e)&W`v=_|m%yt10!^@Q$*&6^iP0AFn~f_#UZoQdrhmM+$` zvm?i@UBAAh$L)v^mSbN{GBB9^T38$EV5Q1lBCy>aNGi1>A^IBM7IpN%OUcg=#tiK0 z^!})Y+QBmc?S`~u-oM`_rIxt$hoeF1wU0NYW(%6>eKl>#1|oRiVI2K0Plsng2G$Vl zqafYi|0wWvX?x8qE$^WX=(D>*d+**xpuJp(YQ3;%YX<_+jw0L2|8b!u=Bi_ ztVAz1w76!L(&l}Ixt%c=hy6x<7TDau96@J2>S$Z=?5-U&TVB~y&t zR=N}Ur|^2)-pC}hY0|MQC9__a7KYUhP3i>6dwB}2*XUapGca>h!ML_39Jt0#o|XPOJ(rkYl_!$iJMvy!i^+BfcpxES!cO@DT!)KER6F+ck2B#Z zKoM^j4%+Oug7+&{Hik9`HZs5fcwb2MA@8FGuEn^HO^r)_;=9N4l`Au_Cnzg5^(`57 zF0u903uH&eJ2XI{m|f@`!xKf?0f_1tydv`5S?osAURo7M4~?m+s_K>)up6JjIkiNp zDHb#rH-nWfnw(GX44@kxlPb7Iejjy065<*<1q276vM!XMhPV2fr27`3b;(Z70Hug3(~)U4MlL9LUMcm`SU^qGCqUuq8pCPd0d|L z6E9PFDs~dVD^EZUw1$6E)(n?{*cPVsEO^T+B>WP@rz91pCDmV+uBWy)5NB&GRBAgJ zgft73nI+R`Ji-;83T^Ce==sNLba{l*>gb=uEGLLhN-9lDYP`IMb2U-eD2bCGZVNko z@Xn8S(&Z(BB4f(?{wvkuvcW8^7qnV0XgBdNm0v8J#{B9bkeJzmmP|PpPdUHBb)E#z z%}iurW>6cl8Ev7%3;aaDQ_?TcGX-6as_V5C7Zvar0Gr!#gLzb-of6NpxI?1OZ{=Nxbq}S~5@frvf$+~1vP7La z$w(2Ci)i7`hV}gXW;giXYpFPxDyvmG)a+a=k-!`a8xr<2q`~rT+6rfrT9^0X>h4SJnIbViLD+uXf7q*=HHe2mad z6OgNV0Y@T9dy-hobbslBWu=Jt&k+kNu#k|F_l=Ms5HvT8wS>w&!rd1(aHvk71~^X_u3Wk# zEyQdWL~p(AI|-z`(wL=IJb;&X*sD^dznWafE=HpnY8eA(L;#^H#70eS8A` z6uz+ikf7c4k?XtsI+iwJ#4uH>GHS)>vkWc-VT z!u6MOa5msNjr()Ns;;wNY zq!m;$Jdhg%{F`D#(QsWFD~XIzh%Kr|2{YZC2rqH?G64Z6c~*lJOyz6g_NL1B6f#&_ zEu-!ZW%`BxhR+J`yp(O>xn55E8hQQ|f6yB_e0%J0N-%ks)oa)84mUY` z_#_#KAYuZ&Y9@Y^L0oWmqBB6^j)^*frIT2#8N+3bh;k_>-K`d-_Tta}j z{m;A6ZFe&$U!=?6T=SV2oD$@)uzL_KOT4Qh-u1;YQ50rbY%2`M784|317^I$9eNCL z_C=uENEuL$7O=9+ z@O53kj%7^@c2#SeSoyGvc*zWfL`4q%zdMQLVX>&=t7HfT$qgsj_2XSR=8FoTkz){g%45Lp&slP8_)9A&@EZ+-Pa62S*XBp<0y-;?ETdG2B8*v+;RHp$V z6Xs8lTuMqxI*4kjO+qvwomkv&=jVyKhOGDvJ%5NigEarFu&2b%d*g=ayO^mI5zAM< zD!U#&8o5%-!@3EqM= zuq=o=P~!cthQ69?Dubf|yTS+or%mLj{D3Z6gPnWWFy-_PChd-#1!Dw8ON}Lw4BEfA z0P73KwtW_YGN~9On^HHn0hIbnm+LA3+ZMhHz=O>f2*qXpE1>=-T>tlpJV+7YDOd{s zt|-k^N$RHCSdXEO5!w6=;!Noyd5GzNI$pfh=83hHgd!LY`DlTr+P_?g+@|1)j+pB-OyU(|0_8lU6DM3REeZ@894wxN_hYO zl8d|@=E@_94LcSOg`=anuryh`pwNb6XT#S2WU-&V=cj(S;_%c-N$qLLeJ@ooK-dw6 zMO%X=mrgA%)fJ92vIZt_t2dO znrL=|pE_dXM^%FYmtJCgnJ9Yc$`xw~N2T7Rbni`9#t&sRzw$c#{hMjdekbKU%?;B| zzgKpMeu#T#fxg( zdR9H8@>an>Oi@-=Rxfn$wTQ}_D=SvNeaQNimzUZuwrv}a&vRiF6_p!R&wBBz_F`VD z;CiyQ0JPt8J5K*8|N1q$k&cU-`=<0}>VyF9_^J-_#U*VSRaI3=4Pd%WY;64js)>)6 z($LTl<7cOu-_n=A{a)n7PVLLJRQ`8Q!{KG33xCg}MDdt4Fl2I4g=a=n|9f@^^T){- zYw%O1&b@p0_GOsh9WU%o=EiH%;ievQ(_WMs*4lqwvp!)bdQ{*cjO!A$x&)D}YZf&) z%2J{mH?rZc1@_I6)`wcr>|Kqmsje}Z*XGhpvyM17%PT1C%x!$t+A4xQMlxeQ{`y*C zxanm~m^z5db;)gFVks|2KHi-6&Vph`1?A=CIX3Rxn0<3^WK>i|w;WGIa)aZ|*x1?f z(Wmw`^lTTB-g{o(eICtE(>vHSqO75*c?2EC17!jJ5ffw(zJC3>JhdU$N(8TUJZesQ zw13$i9n=(|eVMgL_x`VLfW= z>oZY|jErjcjiP^e%+H?b8TXl-o_^Wf+$=MPb}+6RTwLsX!DSe`Wbw5OrVHw46B0!0 z$QwaMf~Dr%IlZ~8x?Web4eG{1{3W%av23xeu>EmlZ0t4It5xd@FzQ9-`xF&^yog0#I{_O~l5DiYU*rgnE&0a% z2BthbJhEaHu`hO$Aodb}Q$l4cNAqT*UUQMxCRhTpKFokfbiBUI(@=111N4*Ek3Xxh zQ}W(JqJ%MTj|vHaR|HBfAM|7gANa9c@!;146_$c(Zqt|hd#p{l zFH4P8wD3G`uotlPyL`8MpIiLUrQ*6k2zGGlggweP?z}}^Z9`hSOL=|z7>dfc6A+rB7LCX z#7ZC>jfxEQ9Md&;W%%0DotyPyWLTjuzC=F%D6Cnq-B*f2vO4i5N%N{>2}S%z)pg=30k@Q)h=}`S$eL4V*}58nW-?=cPlnD*xtD zc{`(|tQJvq8R`o^B!c=QQ5E{nY}*(Ter?bf=9*Kf{Jrw zGa2^uwL`OkGcW1OFGA~ULvkA$8BxdVXjG6=wWA?P6WwaqyxYWVS#kZ?;q&&h)(f|^ z(AH+5+TasEck6~Y2Hp+G6m61^ci9cd*#FgDdn*xkNol^tvx~|N$N_erV(BPsS2l88 zO5R5ck%ewO^8>50P&i&{+$!lYGKEkP6AKIZcJADnzCSCnM;_O*wY$c=@;bB7yT5pD zH^n#(d`IJ)dZOCCeK-F5d)i8Yy*t)6SJ0Pp#438RQPrSF|H|5r^ae*t4NWJ!0vVT} zVFriUpoLdBhr81=Fx-ra3W2VH2T;cYoJB4}{Yge|2=!LyGOF>`dPmt#cLhXuVq?E% z#VYo8`x=hU?(Q|RvdjPc`NOh!@iL(h4CX-a@YvCg-nX_SE-ph|qtrkG85gR)?-4;W zbP5-KVqK`iwmQzv&hXf+ga+u^{*npzKS$1ku@SfcPhDxdwZ;vXt$aI-IeJ51ZeVL? zw?Dtg)YvBtRY(&6W9vh6aPsF(?9TBv;F=tJF z5>e%#?6I}7O^l3dRmt<`+E@N(!b9Jf6aZm%2f zY~=%Hp;^MydoQ3apl7MkI7@~+kgMQj#lDN42zLcP*_P-y;}AM;TL(u(L+`^pr6mtHR+abeH>tusZpZr$oV^|EUsZbX5ep1!TK z)8xq9^%=b>FS)-tVPRnoF(DyZ5nkJ!KaVsI7)BnRpZjvCASxPw<&J*I%{zB4;V2dN9r&C|`RxtHq*1u}P+|2s*0WH6EuJW^epG^) zFaP*)33k6EC3D?ejwZb@lwDqjs;^&H0^6wT=&*uMvtQpmT6t2(zXY<@MeG4zrJ^E? zu5ePvonwM7=04dRL#wTez2JEV`18cK0lpcVT&g7VunLl;=i=3mRzY7!1$hZmhltbA z-r9biiu*4f#43Ck5EA{4^CC>B#`X{r5>l4n@1V&(Wmh^Ch4Y5|l!I;si%=Qc4D z9$ftyT%^=`-}QXl3it4h3e!Sn6>h_0?#~3_S~zQ&TY=9IX;viilO%vP@rp&~$(OB^ zmRzc&+pvsnv9Qtsj6}F}zQqz0YO%FzSEIG=DVRxzbZ`ij_0v6N_Q1fv;NW09ng;!l zvzmsd6z<4KK`AzNT}q0gU^%=l6oo~$6nR4f2}%wBu2CVnn$`W(solt=hDJs&n*$ZO zcqtBR^aQXDH1Uu2&Q38!4(rJTv^9_@D#WF0*GMU3Y;#c4uoYzHt6=K%K0wLy+1z)@p1oZ85Y?Z22Y^H%WINo|ch#&s0y4qc7mN%&K;mTYl8A}1}&=95|2JGiwLpb9epoXPqnW&o` z?x#;{;j2qG;^V_GznJZFd6cUf!Puszyny#u5fWs!^4i+00~oYB6OaR$QKe3_6fFni zO6s^Ficr{xrHO9ZVmmv#F|_kgzLo~H(sL;YBOQX2zQj8r(c)k=99$1%gr&m0eEA#9 zzaRkWE74o;5I9Nv^$;gu+hT!`U5Mn}BQM6K6~KW#wn7j%hDJwsLFdB|n!Lg%7CwAd zxp^LLAyI0mNA)4)YFjqYaNp$N=Jo=5zajR|6fKum*p69rMj}QKVhV%z++T`WbZq+1O5TLck1I+_ zO7gHTBPA{EYUr+|h~^g#GW;igl&suyKb&*X8h15(qTsB0A{VAM+a9~2sG#`3d2`pL z;cUEAFu)Wrl?^Q9uI}y&*S--J#t9SnD#b8wuJ3CvA-V~rv`o&?a>c3T!Q@?5gH-<4m8pjp|28s_3Y5*FOI>?NSnCO}LlJM;$ zPr-1c0^6l4RxnfE*kBBZl@#+9q+sAG+S~YI$z>o{m8qyqYdW>HScz}2s3DLt;~N2o z5K);s(YbZ?@ZiCqZ0^Lq$4!6$!w`Vgvl_6jm%gG8(nIA%-DV1EMC|d)$;mAQBT_yk zB_*|(PnvwT@Bp?ABCd9Brqa*Bh$w_D?LElxaM!FSah?G7cqvp(gI5}xUDrqKIq~fY zD|+q-UB-8x;#FPpY|qcn&!@)M)z)tQ`b1N@buPnXqqnzrLSA0=fR$j^wxu`cF!3yG zZ0e{858+C~SohFHh)5xNkRf)iy|oG|ZPS}FBfihyuhx&!ohJ2kvy{~5BqmDXMyM4% zwOG2mkY{D!_Ic*I!R55<$Mqi1f5gg~bN7F_|9xbmziHN>@94~FuTgo09lG<@=kMMXu-zKn!`F~$oW^0Y zF#`X_#>%=A(#_|+fg0#G+kY>7*u!U=9E)8jWdN4o*DnF{$k&A6sybFvAhAkOksl4% z3b?xm*0FJ#CL4KZ;K`ymVhIq__-5zY6rJhw*8G{ocRPPiCSjCA&@d zYj4x0UM#9=;P@i!y3eqM8n_quBboA!J!R;y@>lbj^6|39l8ZClB~~*H4~+Al4Js%2 zws~#+ZZ~y2a(LfdU_5<7QTD`pXP=yyfWSL*j1w^vbOrtA9~ei>ebuA04x)Rs@$&mm z>=8Q`?Yb4ed~J$r7%#IS8(XJuVjxXBiXnsAl5WFR5Jn6dprQ|qA) z_!c!pL<=wvs;#f@3cPyd4Gqg5AOE=2$M#@fM-E1z9k#I-(7(8Nc-AO}n!g>6ykF5r zwVmjV>j0D<8XkTDO-Oty;>wmfED^EjTo7|gw1kGQJDr6p?g|L% zYm^I%ijKm(S=~R59n&6+6>HxN5PvMC?#>$5PJ`~+^qih2Ez zN^nGC%Rcy&10O^&RiIrXG$y{dcn8^W41DH?ji)$p30C~X7&#b|LeSp%6u6{N0be z&vnjqu5+DzegE0tZLRlR@B2KT;r`tB{b*`Rpoq}1wM{}WVKB@pp`~oYZKFFul-EG~ za5u*Mh7}s>Z@I1(W=^X}ipf*Gubv#t>PslW{H*vm?yo|cPG>uq6@P9fIk<$4DPDnv=<=(3 z7pBts?n@B!CoVsJyz%n`t5CqjtOy+yghGudFlB(PZAKx0I*UTwq_niPX~fJ>TN<4!y?xyaj;tV`jsn)V!5=LW?U<91E^7Qk=FnHKi*qYLLaXDtOjwa3~Lv z^uemIULm)h-CvF%b9M=_qR@DLER4k%WTd-#PF4w7HaAMsBGoyTScEoS4YlUBSx zU6bdQR)g|$%LowN*-?W7W?clA<27mm4gAP>%U+oFrk|S6Gr!^(L+kU0z3zBa^c$NF zzh3YJka|W&#(x`YD^i$zPvmz+??ft3rftO6M=u?yL=IfcE_i^chj%mthnwLNn;cbq z?VbtGm&`_vs8L_GpEP4qs52bSz(|@6iO;G2A+0=8LPRXMpO2f$x$mSfGTQ=YI4@0s zAycl!_-PJXNyY`KXZ6;tH;kvGGVN$u!s#|Xq7zm!QGZ_QD&$Pd+alEuj;*;p^0{^*v7?Tgp{vr_Si_v~Of&?1NDdG{eGE>p= zopxnH&fbd&n*F?YPb}Px8<4a@tD+4g7Zh_aGbHc>&cX@6BK$&`VeeR)&gTz;Ph0v( z@yQ=8>FMEN3$GN4i800!IPQ|SD~2B??oWGheECH-u>}2G|Gi^rDS}uChE?#g*pqwO zY7VuZ{(YlYwTr?X|7BIFU3vZ`b&cn#u*RZgKe)(a_*iiM9EKRlNlBE1_YjRhG6r*h z{V7g<>!ptc8Bw3cw6b_7P^jUa7h-aBb<3GGbBMOV&Ygk@tQ8!%k8QY*x(n=XPh7;m za8>CZ3Yq(bu~_6V!61zuitD)&cy%;89`vRX7Y&iJ$GL2j<+fg4Paoc5L{Qf2Wg8q8 zVB`^osP#Tk6J@j;hER9k#7-r!Vr;6ly6*(MOG&3)GjhiaCq-0*kBSxeA5nV@H2r$P}D`!$C7V^yJh|6m^o!9B*;)k6XE#@9k^oQ^Fs_x2Tw!Ouad( z8o0@C@7T(Jyq*hQnmKRs@0X{VNGn4AiwbCJ;(K;|q3^8yQxSY2@TGSFk5^_y<`PY& z?+%~*BjMgTDBP@1?~ZivOxHw;WEQ*Ev31Ta`f%d{8|B|V_+wG&H`}2O6={VU`@ah# zIzL}qQRH&YT8p=!?^x&LBB92~$38xHe!r{U`};&gwO4847Q3QEXN9`Y&2>CS!sAYs zzE=;r_d`arFxlydQMQ@ma<8U>V%@TNpPu^GB~IU4pW6~Q@%*sO)Sr9pgH1w>$`71J z2jYIzy=?3{UDz^^`F5w`pomc2tBz&`9--f%Kc^j;;%P2vv)|<&T-3H{7dzLuzEJq9 zSG~(v+@05>8-XAru?D3SyRL@PeEZlGItb1p=@aO)`Q_qc?PwjfE zqob2#BF!j&XaO@$ZEtV&xuPIr55jj5l3%3Ufd{q#ED$`sZ=WT}96c9z`s(fN`7j*| zRdKv;3SvzdAXN*zQdUGnkkJ}{{nTR6out)v3js#aM-NS-@LuB&H@$JCiP^3AS#08( zMAd3`eYKhWB{BJZ{*ni^cK;}HyvT1A?@aGbO-gE-G-6MC$^KAw;EKeDD~D#9Y2zAS z&bQtF8l)cdEhlbPY|QD$jK%O$i+8;4?~Y_nosYZk@%sKJ=kybg_xF}>uk=d1pino{ z{LD73;M?R2x8K3g z-p(={i~&Ukz%^&FT3M%J&Hv&8+*}OOR@G|WnRkt^e70gPD<4&rgK-HY?w9-S3t{Qq zkFNFv(_Z-0hx%lU#0-p#o?rmlhT;P@3hkh?``xK)79b-a(zHMd9P@{I=Pfi^C~>J~ z_dvkf#Hh}vS1gCEoi1|3ka%$!{S){C=t75pYl)z6e{Ap)ZzI-$wi2Kks#oN-V35}g zF|CQfFlLO1|Mh@T0doMCn=B%4Nq>17k5ibiz*zg9XJ!ZR1td= zkN3^@{uwAtDSGD1iE@H#LxPVd*x35!=1Whvz6uAFHGI5Kas@XfaH3pXOyyLqcE-H! z_ip|l_2v@ByM>Dul|k*Wpujp3z9mEPiISKi801yQpKxtAmBDw~eVZ3I7qf*)@Pjs& z6S)ZOZyEUMdfVx2MQlt-twRn)O}yMmGKxv(#ttqOD9ffH!9PQ>Y6maHq2IsXKtVYC z_vThs1j7O4k7WDAE{$yy#t4jUUmZ8A{|R{9Yk*si?1z`CK2R)D8oj@{^-9n5<=Yr` zSJt053Eo%enCueYeskI2-GQ>^%fQWzBgHj_pc2qYuWirDZP7s*!Pf}61n9-&I0Xua z7)lL|ohfN__Ry-*h{fL-> znmn$Q;Kp0BUBX9FO<>Pcye&y7sVlZMzQ1Txc0pG}R5#aLe7-mK zRxUdUz!c1lUL&rXSX#>e_W_4fE!F{>zH_&C&`T36Zr$B`7#kwLDoT z@zw+KT%-NL=)Jn}*kV6_f7js-y?nU+pcP25|MycHBi1glAHL!lRmt3JJ*tx_{O>N{ zR6g}Zne-uG0|3`oQx*-!>*EdNpc-OrRD=(Yt{0VFvf+K`u)fJkXWku{Iv#_M`%)FB zrI_#!A>phBCD&atB^X0Dr>dTJzg*f}_)na#94;>OX**PBIQL^Hh+;gTv%ow=(Raoh zXt+K;vK@=oDZ-jJf3ikQQtC>x_=vx6UEtgB-1*p@-^m`rm~GYN%7?baY)%b1dgLSI zDlz*{7WHqrmUrRtJnf5I`OEi;aB+lU0GN}HD2!|@!4=2`59}h)-m%fWiToId3PU;=jK&T$1Uo=^sGZl7k2v8YX;y_6etpiZL1{(I zR~q}Sjvp4oB4t(V{^xtQRtbm_k>JbNo~VS)J4N$*oU z5?yo4zCjxO5B%Kt;87-tDm4iG#4qbvhXYvm&Go)_`6MpHaQM?aMx~rb2Kv^`U1upE zvFB%cQvQQCL3sm2t3a-U)%h~#&zQIOvysIP8*G0+dBN(vFF=;FQvgtLBO@MI$XtTO zfYB}WWdIrl#6l}N$dxRft`ljUiDiFo8FcRGcyg^x(_H)vxm@XRRpY!0Gy(wn{Xr;g zhhh=S&4QF!JKw!3@Xy<{#YEb{WAhscyJUGbJUn%TTulGz;KCft62>r37em%wbH;wu z)BT@)uN<`-UBBh-ohUo&FioAtM+;WcQ!ZhTp;Y$6B;z&<100Qi90pIyLT!7k3{8Yr`ggn)$fJd=)1dIks)y zo2gUg^U!55Ay8~{_MKert|oT0`c^oy2R?9O9uuKa*7Og#^qfz2E zv1;{SJWvHFo%}4{-Zx_eY;g616Ch>C=?pM0_ z4Elb+i4XA^9o2KNFHc!?k4%Q~g4+fLP65R!&U*y{N-2N}j`WPT^}hesoAI9&noN#d zr{%a6R**uGrWji=(ZieeJiKr@&sUp`Q-8L~8f6Z>(zMC0f91O4&)C$T$H^Vl3qt~j zvU|eT-Befnkwx8abm1%)R2KU3;9--Sr);jXV(g9Trk`QI_Pss**5-)k*Op@ktKG7f zz0EooH)dvew(R%2W19<4@0)n4W4f`i#^i7BuV3T}=Z_t%dpmGr;K0UrDs``(Pboe& zUM8CrS!^_U?#A7tuec^p{`GES-G%3)4|3EuP9HqJHLf*u@acnn6Q7zUW!Jy_c{Vdj zLNT{G*7?`1$;^i8XMd*meFKQN0N_cDlGA?7tJ*p`{4fK}TuZf;9?8_4u=+M> zi>bl>vxGF+3}^d{o?igh2?=42kJ1j8$>>|R)~W8=XWIH^Q=DHTsHQ&st3XR3WIo~n`--FIJEx%bHYmz_T+E{8U+_j!GzZ^jP72ADG$bc zyxzt=7{1t{7=#M}z+5qM&cnv^zs%nNDk3cp&TNr~K3Ddg#|ohMWie7a0nYegA`^-) z1u&ItnxP1rKSN;(gkA>JrzClx?AFDw5}0=Y+32_|VZHXM?gM1oj*~TXy}Z<{J=(!5 zcs~Ij(l!KmVyzj&QG{5i2vM!Pb^cOFm_)?VFX3zqJ1~|2s&IB!CpyO+86O|FotgUM z`0@5a5K%%v3&KQb;9^UrX~sk3ZAWEMp5-d^6y>xPq-1d0h<8S;pbE{2AN z;?@|8Ww5=G*WUm;34wV3Fibna{K&(7srYkHHS7?(2oh&r!hpdAU|icK<_DUQIk+Pa zVMGV|B7M~=8s=0b@ch)nRWW*HiId^A5)O5-EW99w7`s{QBB^d-Djmk*WD(>$JK1U? z8&?m89*OgyN!HY`oX8sqb|3w=FCT`0s;(XL;JzM;O_`~lc7w%wi&meco$(&c*@cw( z3wN1qGvoHiMa7jYCqaC4J7`_Ouo<=kb;?9~i3faPhEWfLFl*tiOsBsjln>yn$zT#z zR9Bb7tuzoA_N53thgx%rPa_5Tac_WF;}E=~a1O(|J&a8OR8c@IoK>+dASkE<%t2if z6TWpeufjmV=|E$PHC@q-8pF#7@LwrJm8khHLYCA<{?x#LMc%K=1e)bJG8l3Tl1=IpiE_d%< zOy1=b1V^B1rKIvV2!Wj=3`ep{@VPvUhV+A@G89~#Fn5PzH# z4DW(fDBE2&(94VVGFc~z04@&0rI&;gadC0jtVHoue4Sb#5N$tx4_PuC=u|RD(hAMB(!N^Uj8`eKo3n^bJ^A<_~RYQ`6I~-S3wnx{IT6#AHh4vV81P$Fo4H^oLrs4Wk2P$Eu#W^-0$T1vz~Y3D zONPp;1)|0FW`6R36UYaQ?7>td<7OZ>dXR=AeT+omCpBEXeOuD#sv?a{Jn?RnpnG=z z{&^8%{4A&w^KS{0juX^B-|6o*y&xALRf6pjK#+2*Dny~0)c`>p{K2k&1bb>61|bHG z59&V+MHu{%u{ci1@OAz6j|`q~#T&v8!h99TRFG*#0PsafAH(4n5Y8#easoO{)5Vm96+nIl*Q zswd#P@gn-UbLY%ooZ6{)^yko!?W=OhPu`|KbWeKg1NS4LZDgQP1TO4sWLSYEV^1nG zj2@pH9AM8q@?P>&wIX%bOqM#b?8t^|r1&i0y)&~c+G|@ymR z4N<%a4oY(ET6+jC(J$v`XzYxdIQDw{941FG4%@UQe56OKAN^CF`PJgzk-Y8j{<+%Q zASM9?vcebJKeA;XPaR_WrLoOkKck2J_O(Z8KY=3QKnm`vsw2lH`bND_%QTNQS!Ik# zxNJ^^^I2ETfsC5$a$+8Wo}@NyR~)CmN`}u|$l`u_Z|HeY-XI{n+^Z zEiEyD!NG?{mNWm4s`=akfWD;6B~N*74z@S&V3xjSUMt?Eh6xkABsVN5%%HL=M9d^h{@f4i5$F=gD#c7>2quem&<2y+C-cf0rI3mp*pl$)^@Ej7650!jiOtmk z=E@sjD)Z7;g9XFJthh*iH_C3(H-Lo{hA5bVO^VKs5bge@LKrZFKf|1ftYxsyC?>+~ zVca;1xqvnYh9nnuC#M-&Oe826hA$$38Vp(@aPDCWtnyL#YBI`Kr<6P7YYd`*3PrMF zlYV)p2k!@t#EV11^cf%{;Z}r}ENMe5E64j+a_=>n8qN0Ao0}~VY9iVgRA}<5Qv4dy zn0Z3kN5T;ZYVb9B3cm*;jxftG^B2NEB z%rq!LnBMagKo6Ye{19YTK~O^)el*lLed;2{&7`BmTGb6#S7osXvELs5*4t9B6%}Q2 zZQn!BUq|m-K99-$DV4&B#lRZei3_2Q#o$d73|jIMNvnn9)HsRfI*H-nmFmIHx%V#P z?Dln62+fVWZu~Jb$GKOK?su z#z;LLWsN<|TV50T2KfoNU^&{ga6*iM^Hn)nxJZTeSl_B{T?)11jM_>^ZRaI}W&q)MM zQ1^_f7Fk(249N|O_!>k>aC2Lfd+QvPNkqkwH4z|N3#0y9hGlCYfa6>qVb_uMiJ3m4 ztQHu{$@?=wOAUkt6#DT_NFZF!cH3A<#$UcHNZBGNfS9Uuvn930@gdA>`&$bo;0@63 zIn9tXoH`K8l3crSqae6yrHHE}k&yuiCQ~>>Oo3S5Tz7_vERa?Tgy#U{OKu~h-&K{B z3(4RXWh!Q&4lm9dV_-nEMX;43eJts_IPv`is4(~lho46{)5xhQkDo|o&{FAd2&|{E(X4p~sse{9F$IH8!>#D~04GLQlq%Ppd;I2pV<(DdKQ4Mt7eE z!(lHrUi9Cja|gOHgvK7zi*`se^a-C7r(mLpTe)&2g*2ge{J0DjwF~stD>S^8ptD|) zRO5ryLi(-b4OM>ZpXAxifTkYWLFperjcLD#n(Pam0^qR>3y z?(`2ICO82^uVH|1i%)8O?gnIl=Jm~@cDQSvv;cXe06~I+FMC7w{!BenQ!=EtpV*I$ zvsdc`o!&?Cl$~>pIz@2|7&5fY78?UzLsBMETWBK}6iw~Q{wf>XWf`5L5h&o*9++A;;b4! z|1~IpTz+;LB8Gfa-j!^UgsBrGQz~MCfRL2%o%{j*fHj1>jv5D$V>Ip(KXpiyhfe24m(Ccu3f?CK zBB5BmY!p6R1CM5TXl$`^(o!1=^2SErc@^ zFnUV4dv^hTD+E8NI&g>Em^rE4afz2!ELnn6E)UoVgw|zUrzIA>)@f2-7dA`VBc<305o|@1H8LLsbzOZ$pNSNLN5ECIErPD7G zWo%sBsMash$oxz*S@Iq{JP7$000IQ?z*NC*_XoEWKZ6!)#ky}MEB6HFMeCCO8YLs? zSdYJeq)206e|l{)o*m*91~T9aF`&P|jJ@v$3x^7#RYUgL+Vzx&!FWalw$b7ElP48| zp`8DYH%NFS;|)T7iB^bi^%Vwqj7hf$k_`E01*;!8!~C5p<}9XE(vk-@I}Tu)y@&<9Gg(f6z2rd9-gK4IZC4X2Rhz_wmMB+fB^!g9|T85%}A zY)k6bHpQ;#sZQM7{LFO6Oku%ReLKZ~V#O)3;!2s&%Lj&M?A`p0)(whX;0mm|Lu>7s zCvmriney`8`?nFU49Bc}XT$poqztcFY)Bk&_+B7KkG(b<@vE$PQ;+=1D_5^c#@+P) zQ7B!tMe3KKak#j>G<}CP?cRl!6Iq6)4QkVUB7Z#Q@xWiiG62p<6qUCC=FKqOiWW)q WPaPFfUUP>0tD)X*#seK&zyAU9K`pib literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f1-exif.jpg b/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f1-exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ff003e394915961ec2576628bf882b6b49a3407d GIT binary patch literal 992 zcmbV~Nl+6(6oy|<&txVu31pHY9z;cv6SzZJj6F#Vr6=@Fzv;hv-s?Bt>()lK33!#)nV;DAL|z(gVP07?KRqyehY@~Cb=&PWVJVpJe+kcfLJ+hT5q2k1y{JDGJWBu;h> z&#J7bZbfm+u0%Q0?NZ&c3Sc+_z>)}BRu#Dmg*Bou1b(k8vK&C+Dw+lmprTbEfEP8{ z1GLv*Db6p-2ZRu*NvdlLV9WK^R{3gc68#=uePTgTNvZY{asgA6K8Vp|FdW0sG{dv3 zfiv+YlfVmtImT)+$A~e4U~yPPn`F1!O|fzD4k_L$*(H4wgh>v~7#YSWnFX`-&!*i1 zk%LljVk813A}k_p7*>#`F$6)Uh#2Vz%@|nD$O|N3&;k^~n4+=%*CcwM-Z5HatVv3? z!B$qqCO1lIYgZ4Kk~1)5FCUpn&8YUa8F@!s{IZ0!m8+bq(_NWs)~;Lc&fSogUr<<7 zym3>-=E^Nwx9zB@t=n1eY1rl4y~p3Qw|W18gY6xM4tE|scD(z<$y2A#^qxI;{=&t+ z{@|s{R|cmZ z=5;P|jA5)O*FOC6GNxpd&)BQI zBfKNkHIX)}qlL(p2G;gpWKpn1u5mD7L=F#&kOe=30jh-uC%L9AS;5YT=x&CEU}%kp hg^DRW-xrR;bCn?x`WQR8eTgZt)4kyuf2L?-zW}!h3XT8( literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f1-s.jpg b/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f1-s.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1fdacd651786ca1181de034aa39324d2ad637cce GIT binary patch literal 960 zcmex=L?6mQqXwbzE zD#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2ZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx# zW#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8+42=DS8dw7W$U)>J9h3mboj{8W5-XN zJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2`tAFVpT8IxnBgG}@eq=K1cClxVqsxs zVF&q#k*OSrnFU!`6%E;h90S=C3x$=88aYIqCNA7~kW<+>=!0ld(M2vX6_bamA3mRh)e{h!nQ7iw)RQyk>_#fkR5Yp8D zxhVd_j{Of!{xh^5{m;N%{BPCsL+5|E+dq^6ibVcr2)zHc{_Xvrt^XNP|1%sp{{8nq zh2;MXYZm=ycs9BIXH4LK1~$Y04Ceyt-<{n5{hjvjKMVLD@MEYE0IInbSpV5`|DSb0 zHH#YTPl*5H02;&slxV;BpW$#|{jtFM!)73KXwrX%S*k$CbpI3f{}B`aVY~gSbu~Kw z8Cr_}37-EEI{&Nezw7_*{%4pG|1JKHPyPP$KNS1_2ps<>v-3X#uj_vXNg(D`hhQ%6 ze;L#NXx{(f^q=9O-+zWfcm6Z1+WFD{pOF0z51>%ye}*gEzm~xI@1lSI1u(jb1h9W#$MEWUpig;#o|pp+jk}ZU-`@%U{?ma~B+xLJ%Z3brsW+o;O z0s6Oj-S z5fuR$!pIEN!@|nR%E~Fi%grl7GWdUhL6CzXfI)+qQILU2kdaxC@&6G9c?JeXR-hL^ zzJLNoCZHSH*f}`4xPc0`3NSD+GBY!=FoRqTR9y>{XJ8Rz6;d>GWD^cdWLGK_F>0K+ zkVDyN<3Z7&iyu^slZu)+xx~aJB&Af<)HO7bPj7;S~%q+;ls%Xe2zfvV;IO#p8*_6Gm} literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f2-exif.jpg b/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f2-exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7e0f170e36674d695e53c5b480e57d59ca2ecab9 GIT binary patch literal 994 zcmbW0OHk8L6o${uO`0@mp=r~CxR9q-R!Jy6x?uo$&M*TgqAr}F0#bHVw8)GzfC|b= zRD7)pzOXw~e62+Bxx*)*3#+2Y3Vg#j#2Z?R!%94JPwqb_=iHO;+=(`#h2V8wleZS2 zqy*9dfCCm;0TYFQB98=M<~4u^EsyGYYh?M8CJOF0r7fq*QwaxqvB3U&Ls#7>;3Rn&Da2 zz?pcHN#F&+9AmYZW5gIiusAHDO|skVrr5Z6hZJv>?2>*6!lZ^~j0|Ix%z{~3vT3(L zPiM2#ZJ?f)!+F3_;K-B1R@cGX|D3@&ZX1umFWHrf95xHObzmPmC5BYm$;} zu$5J@$qka)+SS9Q@wy=Tvzzi_ed zQtxK+R8~eT|NDS#*~cmDZ9rv z%sWzDp|oioZJzAkz}o(cEDE;3H3lY3{v8pEkOjYj0jh-uCj??Adn00edrMZZQ;$(C fS>4Sr7o3+o%vDU_*}h10i6x{5o~c~aMH~GM?`I42 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f2.jpg b/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bc3e1dba386b381d7f7917effecb54ce05d48849 GIT binary patch literal 772 zcmex=LJ%Z3brsW+o;O z0s6Oj-S z5fuR$!pIEN!@|nR%E~Fi%grl7GWdUhL6CzXfI)+qQILU2kdaxC@&6G9c?JeXR-hL^ zzJLNoCZHSH*f}`4xPc0`3NSD+GBY!=FoRqTR9y>{XJ8Rz6;d>GWD^cdWLGK_F>0K+ zkVDyN<3Z7&iyu^slZu)+xx~aJB&Af<)HO7bPj7;S~%q+;ls%Xe2risYLlKV(WCe-L0ObmuBY+M+(4pLRvKx^{Ci$|k ztFof{6vZcdW92lTSM{Z;0G6WwEOF4Xs>oevK_d!-_V-+oWzGCIK)Mc~QQ<0};YCgE z0R1gE3Ucyu03k$blIr?AxUvG(6%Ex@v5o$Q+SuIu!Xo`OWC5lqW00W9U^s@MX@+N6 z3uohPHh~uedxX3WGm$fN7CpRy@ zpmcNDmh!FJw(qQ}uGv-VuiM?QXK!QEzUBi554Cq3KGJ#Y_=)b5r%s@+0RlHT16^FdScFkd!_=lYhWkWp02v4h+W-In literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f3.jpg b/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bca977c644eb6805d1f4f74eece7baaeb61c9ae2 GIT binary patch literal 770 zcmex=LJ%Z3brsW+o;O z0s6Oj-S z5fuR$!pIEN!@|nR%E~Fi%grl7GWdUhL6CzXfI)+qQILU2kdaxC@&6G9c?JeXR-hL^ zzJLNoCZHSH*f}`4xPc0`3NSD+GBY!=FoRqTR9y>{XJ8Rz6;d>GWD^cdWLGK_F>0K+ zkVDyN<3Z7&iyu^slZu)+xx~aJB&Af<)HO7bPj7;S~%q+;ls%Xe2WB#=oa0r4QKMou$BaLI)NWUZnKP((dg1p*RoL=>dTDnJG0 zBr0yFfE(n7irYyP*Bh>Y9z;cv6SzZJjBf&l(v#Tr`prK*{krGtrw{96@G7gpR|QZ| z0LcKr1BdK@sX{>L76WWZ1C*oXVO@jV3potU(SYtCQ};8Dg*XCe^BEOr9e`HqhUHQ> z-a}7KQ8llsdKFKslIrzn-V_bMbOeB-8AQ=kr31}t#9#>gj;o3iK=WO69U#DjyFdUh zYFY#I*I>)f&dUab5ba63>T}@8^jDVERaV5-`|7G=bMgub^_P$dm|=`XjnKvNEK3M0 zaGZ&^2o{Sdh@v&ZZnH*65u#{w*(8VTbUH1OQPD0r+Acd~qX@$E3}MYIYnH8|RbH~` zw?N{d5Zo9^z(@#7NFRh1bZ9I^Feny`PK2-~jyDS;P3X4)gD_?YHvXDs?=vPwB-S3M zrkfl^Wn6r%tTlIZ@d+7y1J2^1sl=3We~VdgMMW=*Nm{wey*k;Gx@PUV_1?@4S=l+c zdHEYRm258EvUS^zipr{;)xMftb-VY}H|%ZPf8bzi+o8klM~@xvJaO{W=`-DD&z-+; zv8Ok9>GGBStJkjIxOw}|;N5%oA3S{Y_{q~}&tD9WjE=o|`)>UGhl!7q)1N+n`TFhq zkC|D6i(cbAR+#KBE;s+1(_78v>Ez+ndsY?S^EU k(mESqE*O#m%#}>w*&Z6Dd3dJu&m%@G^l?c?LOt}6Uy(TsK>z>% literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f4.jpg b/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..395385b8893ec4ce3545c0947d9be56879cf0bbf GIT binary patch literal 772 zcmex=LJ%Z3brsW+o;O z0s6Oj-S z5fuR$!pIEN!@|nR%E~Fi%grl7GWdUhL6CzXfI)+qQILU2kdaxC@&6G9c?JeXR-hL^ zzJLNoCZHSH*f}`4xPc0`3NSD+GBY!=FoRqTR9y>{XJ8Rz6;d>GWD^cdWLGK_F>0K+ zkVDyN<3Z7&iyu^slZu)+xx~aJB&Af<)HO7bPj7;S~%q+;ls%Xe2Zzy zfG2LChzM}F#hN=}fC{o8f@?{N#bNMK2i!qH4>2C=9wXX? znif#~acX+NuLsg}0NZ{59vQ5vYic{0#)P2|{(Y~hYM4w{F${n(6R84WI)gC>7%#w8 zm|Ktwgb=KWRgFpTWQVHC>#Hi`8iMsTad`zr#l|zp2FftzLSu1p0>`l|C-S^aaEK0v zBubJq%I$JS$x)Kz^0{P>;`MqR(eq+_N{n0aDrOQwF^1*r9A{UYl2iF-Gp>UyKnfI7 zA_F56Difn07Gl#llwhi`3_HSdHeRrc5)yh{zz|9qmYRQ!?Coa9SebLjYne7rNjaZT zr|8Y?okC((PoKARU@R%EBGh6Ree+`G$0jdYoU$a4~jg?i^n`(l!o9nl1ZD`!qv~$<)*0w!+_Z>KRsN?XFqsNYSoj7^w^qI5gy3b#@ z*n8>nm8;io-0Z(~`_A2a_a8ib^!Uls!J*-im#<#GdHe4DhtctmpFV&2`tAF~4^tP8 zaavYH*&JQi1j4W^WqDH%yRLX&7E1om)Cg`^kzZHYVYjnvnQsNj(ICW1EMd< zKaxCdN}E#lcfwl!OIbwNjILpDP=dpwGGxHd?l7}M#EH&6CcCG;Gdh~Ug2LJ%Z3brsW+o;O z0s6Oj-S z5fuR$!pIEN!@|nR%E~Fi%grl7GWdUhL6Cz%gCT&KQILU2kdaxC@&6G9c?JeXR-hL^ zzJLNoCZHSH*f}`4xPc0`3NSD+GBY!=FoRqTR9y>{XJ8Rz6;d>GWD^cdWLGK_F>0K+ zkVDyN<3Z7&iyu^slZu)+xx~aJB&Af<)HO7bPj7;S~%q+;ls%Xe2c7CZVZ;T4DeHCIAZW>_Gqk literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f6-exif.jpg b/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f6-exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4e2c86415a65d641722ce33a98f61d5bed9da762 GIT binary patch literal 982 zcmbV}Nl+6(6oy|<&txVu31pHF1b*EtW{J2E~qyUkZ>cSAXQcgDySz> zaXSUvxuN2A62SS&VN;2q{nMtLg5)y8qX2zSqWxF%B>CTl{qZrKOMo z00QvD2^19p3r-2Z&1!&ZvM{0>NP5`AphpLsLBSDVT;Y8J(CG&@oZEq`8AUkoB(&

    ECwfnwI(_Esx$}J& zE?yeAeC6u3>o;!=-M(}8-u(v;A3c8Z^x4Sh*!Zj0Z{EIp|Ka1r)Ths1zJB}uWBRAb zh0hqmije)mg-sv~%Tks%xd_vYgUT$Iq*X2qntIxsAH zQoZA8QzqIh+24V+{})*VY@TZjY?R>RQ5mw~S6?u8qIai=ZyIFgmhA3UnCXk&!E-uY XIcJTMW>%CvN`_M+mxfDZ8>7Dg!SMnV literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f6.jpg b/vendor/github.com/camlistore/camlistore/pkg/images/testdata/f6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..175f40239c7571fa39be2f26f4c9d157a8dc60b9 GIT binary patch literal 760 zcmex=LJ%Z3brsW+o;O z0s6Oj-S z5fuR$!pIEN!@|nR%E~Fi%grl7GWdUhL6Cz%gCT&KQILU2kdaxC@&6G9c?JeXR-hL^ zzJLNoCZHSH*f}`4xPc0`3NSD+GBY!=FoRqTR9y>{XJ8Rz6;d>GWD^cdWLGK_F>0K+ zkVDyN<3Z7&iyu^slZu)+xx~aJB&Af<)HO7bPj7;S~%q+;ls%Xe2F1b*EtW{L8h}*#`B0<89h=NvG1*o8$ z#9dFp6}X||b`r()hAW^4qoU{u+@(s!9teS=JgBewb-(KVyI+6r=|g%5UgkFXs{x9O zAq@a{;E)|KjSz5jiUW2!2JoWAk+>G6hBXY;Xh3I>sRtNGcpm|@`GJJ*w$jzG0_yZ+ zX4Mo`^QfvvamOne9=GO6*8ofh!N?0l(Nv{l`VT-5gCRJ(uPRCq&1BJafFKje0zo_% z(>FkW1-7ER!aP6-(VR59J_U}PfUmONR~6sjudj*EFDx$6UqB9EhA{>;LI=yUEFrAG zaVFj(SS+F-iq|iK5MAlN_?s>9j=0#Jc2IyX=&WM-ZlW2y13pvuqWu@;{q? z6C@r|p#&od7ztqs=>xEY7LBC{21VFtMF?x+c(WiO9M=d~Frv-F5@f=!@*vYB(2mY7(W9H2GJD$R?HK1% Y6=Qg^>kpa*@_9NMMjnl%$kc~_19kTSLJ%Z3brsW+o;O z0s6Oj-S z5fuR$!pIEN!@|nR%E~Fi%grl7GWdUhL6Cz%gCT&KQILU2kdaxC@&6G9c?JeXR-hL^ zzJLNoCZHSH*f}`4xPc0`3NSD+GBY!=FoRqTR9y>{XJ8Rz6;d>GWD^cdWLGK_F>0K+ zkVDyN<3Z7&iyu^slZu)+xx~aJB&Af<)HO7bPj7;S~%q+;ls%Xe2F1b*EtW{J2irc{|5Rh;qqCu;y0#r~= z;;yIQ3fxd}JBi|Y!xhkjQBm{+?ot+GPZ9!pQeXA*Pxs&b=6kJ==s|dq-|VdeC@qBy z0N{Z`cED62ssSYc8`1#PXkk=0p!A4`p&kwB2r}(H#t~UZ03BW+k+bb|Hlm1jx)Yu? zMb$j2>QUT@N|wj1c``Ks(*XdEI*6jFN*4-i#9#=_uB(a?K;bO94iI3XSs;MtG`$1# zmtZR?C@uhm5Y0)m>oefU^VL>0)z&2Xy-oFrg~g?1`g6zw%rHiyL1<%nmL-H0IL^dd z1dBx!M9~^!w^?JP7*VvjY?4EEI-Qo-xOkTwZw!Jvo)tq5UF9B&pxY8bEqgD_?YHvXErcNrBU5^GOVb4-r% zDlWNE)>^x|`IOxLL1)GAWNK!$ugxsD;^G%1q%B^OzBI#~wQTu{m7cs+`2~eV#U-oP zRIXjOe#6GiHMMnH>b(tHo3?HDH}BZFd(YnXj(z((4<0&vECr+L^edg@B^F6(N z7cO2JxP0a6wd*%;4c)$T_ul;n4<9{#^7Prr=veU8>o?LJ%Z3brsW+o;O z0s6Oj-S z5fuR$!pIEN!@|nR%E~Fi%grl7GWdUhL6Cz%gCT&KQILU2kdaxC@&6G9c?JeXR-hL^ zzJLNoCZHSH*f}`4xPc0`3NSD+GBY!=FoRqTR9y>{XJ8Rz6;d>GWD^cdWLGK_F>0K+ zkVDyN<3Z7&iyu^slZu)+xx~aJB&Af<)HO7bPj7;S~%q+;ls%Xe2 file schema ref +} + +func (*imp) SupportsIncremental() bool { + // SupportsIncremental signals to the importer host that this + // importer has been optimized to be run regularly (e.g. every 5 + // minutes or half hour). If it returns false, the user must + // manually start imports. + return false +} + +func (*imp) NeedsAPIKey() bool { + // This tells the importer framework that we our importer will + // be calling the {RunContext,SetupContext}.Credentials method + // to get the OAuth client ID & client secret, which may be + // either configured on the importer permanode, or statically + // in the server's config file. + return true +} + +const ( + acctAttrToken = "my_token" + acctAttrUsername = "username" + acctAttrRunNumber = "run_number" // some state +) + +func (*imp) IsAccountReady(acct *importer.Object) (ready bool, err error) { + // This method tells the importer framework whether this account + // permanode (accessed via the importer.Object) is ready to start + // an import. Here you would typically check whether you have the + // right metadata/tokens on the account. + return acct.Attr(acctAttrToken) != "" && acct.Attr(acctAttrUsername) != "", nil +} + +func (*imp) SummarizeAccount(acct *importer.Object) string { + // This method is run by the importer framework if the account is + // ready (see IsAccountReady) and summarizes the account in + // the list of accounts on the importer page. + return acct.Attr(acctAttrUsername) +} + +func (*imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error { + // ServeSetup gets called at the beginning of adding a new account + // to an importer, or when an account is being re-logged into to + // refresh its access token. + // You typically start the OAuth redirect flow here. + // The importer.OAuth2.RedirectURL and importer.OAuth2.RedirectState helpers can be used for OAuth2. + http.Redirect(w, r, ctx.CallbackURL(), http.StatusFound) + return nil +} + +// Statically declare that our importer supports the optional +// importer.ImporterSetupHTMLer interface. +// +// We do this in case importer.ImporterSetupHTMLer changes, or if we +// typo the method name below. It turns this into a compile-time +// error. In general you should do this in Go whenever you implement +// optional interfaces. +var _ importer.ImporterSetupHTMLer = (*imp)(nil) + +func (im *imp) AccountSetupHTML(host *importer.Host) string { + return "

    Hello from the dummy importer!

    I am example HTML. This importer is a demo of how to write an importer.

    " +} + +func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) { + // ServeCallback is called after ServeSetup, at the end of an + // OAuth redirect flow. + + code := r.FormValue("code") // e.g. get the OAuth code out of the redirect + if code == "" { + code = "some_dummy_code" + } + name := ctx.AccountNode.Attr(acctAttrUsername) + if name == "" { + names := []string{ + "alfred", "alice", "bob", "bethany", + "cooper", "claire", "doug", "darla", + "ed", "eve", "frank", "francine", + } + name = names[rand.Intn(len(names))] + } + if err := ctx.AccountNode.SetAttrs( + "title", fmt.Sprintf("dummy account: %s", name), + acctAttrUsername, name, + acctAttrToken, code, + ); err != nil { + httputil.ServeError(w, r, fmt.Errorf("Error setting attributes: %v", err)) + return + } + http.Redirect(w, r, ctx.AccountURL(), http.StatusFound) +} + +func (im *imp) Run(ctx *importer.RunContext) (err error) { + log.Printf("Running dummy importer.") + defer func() { + log.Printf("Dummy importer returned: %v", err) + }() + root := ctx.RootNode() + fileRef, err := schema.WriteFileFromReader(ctx.Host.Target(), "foo.txt", strings.NewReader("Some file.\n")) + if err != nil { + return err + } + obj, err := root.ChildPathObject("foo.txt") + if err != nil { + return err + } + if err = obj.SetAttr("camliContent", fileRef.String()); err != nil { + return err + } + n, _ := strconv.Atoi(ctx.AccountNode().Attr(acctAttrRunNumber)) + n++ + ctx.AccountNode().SetAttr(acctAttrRunNumber, fmt.Sprint(n)) + // Update the title each time, just to show it working. You + // wouldn't actually do this: + return root.SetAttr("title", fmt.Sprintf("dummy: %s import #%d", ctx.AccountNode().Attr(acctAttrUsername), n)) +} + +func (im *imp) ServeHTTP(w http.ResponseWriter, r *http.Request) { + httputil.BadRequestError(w, "Unexpected path: %s", r.URL.Path) +} + +func (im *imp) CallbackRequestAccount(r *http.Request) (blob.Ref, error) { + // We do not actually use OAuth, but this method works for us anyway. + // Even if your importer implementation does not use OAuth, you can + // probably just embed importer.OAuth1 in your implementation type. + // If OAuth2, embedding importer.OAuth2 should work. + return importer.OAuth1{}.CallbackRequestAccount(r) +} + +func (im *imp) CallbackURLParameters(acctRef blob.Ref) url.Values { + // See comment in CallbackRequestAccount. + return importer.OAuth1{}.CallbackURLParameters(acctRef) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/feed/atom/atom.go b/vendor/github.com/camlistore/camlistore/pkg/importer/feed/atom/atom.go new file mode 100644 index 00000000..73c3ab05 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/feed/atom/atom.go @@ -0,0 +1,61 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Adapted from encoding/xml/read_test.go. + +// Package atom defines XML data structures for an Atom feed. +package atom + +import ( + "encoding/xml" + "time" +) + +type Feed struct { + XMLName xml.Name `xml:"feed"` + Title string `xml:"title"` + ID string `xml:"id"` + Link []Link `xml:"link"` + Updated TimeStr `xml:"updated"` + Author *Person `xml:"author"` + Entry []*Entry `xml:"entry"` + XMLBase string `xml:"base,attr"` +} + +type Entry struct { + Title *Text `xml:"title"` + ID string `xml:"id"` + Link []Link `xml:"link"` + Published TimeStr `xml:"published"` + Updated TimeStr `xml:"updated"` + Author *Person `xml:"author"` + Summary *Text `xml:"summary"` + Content *Text `xml:"content"` + XMLBase string `xml:"base,attr"` +} + +type Link struct { + Rel string `xml:"rel,attr"` + Href string `xml:"href,attr"` + Type string `xml:"type,attr"` +} + +type Person struct { + Name string `xml:"name"` + URI string `xml:"uri"` + Email string `xml:"email"` + InnerXML string `xml:",innerxml"` +} + +type Text struct { + Type string `xml:"type,attr"` + Body string `xml:",chardata"` + InnerXML string `xml:",innerxml"` +} + +type TimeStr string + +func Time(t time.Time) TimeStr { + return TimeStr(t.Format("2006-01-02T15:04:05-07:00")) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/feed/feed.go b/vendor/github.com/camlistore/camlistore/pkg/importer/feed/feed.go new file mode 100644 index 00000000..fc337d61 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/feed/feed.go @@ -0,0 +1,282 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package feed implements an importer for RSS, Atom, and RDF feeds. +package feed + +import ( + "bytes" + "fmt" + "html/template" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" + "sync" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + "camlistore.org/pkg/schema" + "camlistore.org/third_party/code.google.com/p/go.net/html" + "camlistore.org/third_party/code.google.com/p/go.net/html/atom" +) + +const ( + // Permanode attributes on account node: + acctAttrFeedURL = "feedURL" +) + +func init() { + importer.Register("feed", &imp{ + urlFileRef: make(map[string]blob.Ref), + }) +} + +type imp struct { + mu sync.Mutex // guards following + urlFileRef map[string]blob.Ref // url to file schema blob + + importer.OAuth1 // for CallbackRequestAccount and CallbackURLParameters +} + +func (im *imp) NeedsAPIKey() bool { return false } + +func (im *imp) SupportsIncremental() bool { return true } + +func (im *imp) IsAccountReady(acctNode *importer.Object) (ok bool, err error) { + if acctNode.Attr(acctAttrFeedURL) != "" { + return true, nil + } + return false, nil +} + +func (im *imp) SummarizeAccount(acct *importer.Object) string { + ok, err := im.IsAccountReady(acct) + if err != nil { + return "Not configured; error = " + err.Error() + } + if !ok { + return "Not configured" + } + return fmt.Sprintf("feed %s", acct.Attr(acctAttrFeedURL)) +} + +// A run is our state for a given run of the importer. +type run struct { + *importer.RunContext + im *imp +} + +func (im *imp) Run(ctx *importer.RunContext) error { + r := &run{ + RunContext: ctx, + im: im, + } + + if err := r.importFeed(); err != nil { + return err + } + return nil +} + +func (r *run) importFeed() error { + accountNode := r.RunContext.AccountNode() + feedURL, err := url.Parse(accountNode.Attr(acctAttrFeedURL)) + if err != nil { + return err + } + body, err := doGet(r.Context, feedURL.String()) + if err != nil { + return err + } + if auto, err := autoDiscover(body); err == nil { + if autoURL, err := url.Parse(auto); err == nil { + if autoURL.Scheme == "" { + autoURL.Scheme = feedURL.Scheme + } + if autoURL.Host == "" { + autoURL.Host = feedURL.Host + } + body, err = doGet(r.Context, autoURL.String()) + if err != nil { + return err + } + } + } + feed, err := parseFeed(body, feedURL.String()) + if err != nil { + return err + } + itemsNode := r.RootNode() + if accountNode.Attr("title") == "" { + accountNode.SetAttr("title", fmt.Sprintf("%s Feed", feed.Title)) + } + if itemsNode.Attr("title") == "" { + itemsNode.SetAttr("title", fmt.Sprintf("%s Items", feed.Title)) + } + for _, item := range feed.Items { + if err := r.importItem(itemsNode, item); err != nil { + log.Printf("Feed importer: error importing item %s %v", item.ID, err) + continue + } + } + return nil +} + +func (r *run) importItem(parent *importer.Object, item *item) error { + itemNode, err := parent.ChildPathObject(item.ID) + if err != nil { + return err + } + fileRef, err := schema.WriteFileFromReader(r.Host.Target(), "", bytes.NewBufferString(item.Content)) + if err != nil { + return err + } + if err := itemNode.SetAttrs( + "feedItemId", item.ID, + "camliNodeType", "feed:item", + "title", item.Title, + "link", item.Link, + "author", item.Author, + "camliContent", fileRef.String(), + "feedMediaContentURL", item.MediaContent, + ); err != nil { + return err + } + return nil +} + +// autodiscover takes an HTML document and returns the autodiscovered feed +// URL. Returns an error if there is no such URL. +func autoDiscover(body []byte) (feedURL string, err error) { + r := bytes.NewReader(body) + z := html.NewTokenizer(r) + for { + if z.Next() == html.ErrorToken { + break + } + t := z.Token() + switch t.DataAtom { + case atom.Link: + if t.Type == html.StartTagToken || t.Type == html.SelfClosingTagToken { + attrs := make(map[string]string) + for _, a := range t.Attr { + attrs[a.Key] = a.Val + } + if attrs["rel"] == "alternate" && attrs["href"] != "" && + (attrs["type"] == "application/rss+xml" || attrs["type"] == "application/atom+xml") { + return attrs["href"], nil + } + } + } + } + return "", fmt.Errorf("No feed link found") +} + +func doGet(ctx *context.Context, url string) ([]byte, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + res, err := ctx.HTTPClient().Do(req) + if err != nil { + log.Printf("Error fetching %s: %v", url, err) + return nil, err + } + defer httputil.CloseBody(res.Body) + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Get request on %s failed with: %s", url, res.Status) + } + return ioutil.ReadAll(io.LimitReader(res.Body, 8<<20)) +} + +// urlFileRef slurps urlstr from the net, writes to a file and returns its +// fileref or "" on error +func (r *run) urlFileRef(urlstr string) string { + if urlstr == "" { + return "" + } + im := r.im + im.mu.Lock() + if br, ok := im.urlFileRef[urlstr]; ok { + im.mu.Unlock() + return br.String() + } + im.mu.Unlock() + + res, err := r.HTTPClient().Get(urlstr) + if err != nil { + log.Printf("couldn't get file: %v", err) + return "" + } + defer res.Body.Close() + + filename := urlstr[strings.LastIndex(urlstr, "/")+1:] + fileRef, err := schema.WriteFileFromReader(r.Host.Target(), filename, res.Body) + if err != nil { + log.Printf("couldn't write file: %v", err) + return "" + } + + im.mu.Lock() + defer im.mu.Unlock() + im.urlFileRef[urlstr] = fileRef + return fileRef.String() +} + +func (im *imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error { + return tmpl.ExecuteTemplate(w, "serveSetup", ctx) +} + +var tmpl = template.Must(template.New("root").Parse(` +{{define "serveSetup"}} +

    Configuring Feed

    +
    + + + + +
    Feed URL
    +
    +{{end}} +`)) + +func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) { + u := r.FormValue("feedURL") + if u == "" { + http.Error(w, "Expected a feed URL", 400) + return + } + feed, err := url.Parse(u) + if err != nil { + httputil.ServeError(w, r, err) + return + } + if feed.Scheme == "" { + feed.Scheme = "http" + } + if err := ctx.AccountNode.SetAttrs( + acctAttrFeedURL, feed.String(), + ); err != nil { + httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err)) + return + } + http.Redirect(w, r, ctx.AccountURL(), http.StatusFound) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/feed/parse.go b/vendor/github.com/camlistore/camlistore/pkg/importer/feed/parse.go new file mode 100644 index 00000000..6c6a0e15 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/feed/parse.go @@ -0,0 +1,508 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package feed + +import ( + "bytes" + "encoding/xml" + "fmt" + "html" + "log" + "net/url" + "strings" + "time" + + "camlistore.org/pkg/importer/feed/atom" + "camlistore.org/pkg/importer/feed/rdf" + "camlistore.org/pkg/importer/feed/rss" + "camlistore.org/third_party/code.google.com/p/go-charset/charset" + _ "camlistore.org/third_party/code.google.com/p/go-charset/data" +) + +type feed struct { + Title string + Updated time.Time + Link string + Items []*item +} + +type item struct { + ID string + Title string + Link string + Created time.Time + Published time.Time + Updated time.Time + Author string + Content string + MediaContent string +} + +func parseFeed(body []byte, feedURL string) (*feed, error) { + var f *feed + var atomerr, rsserr, rdferr error + f, atomerr = parseAtom(body) + if f == nil { + f, rsserr = parseRSS(body) + } + if f == nil { + f, rdferr = parseRDF(body) + } + if f == nil { + log.Printf("atom parse error: %s", atomerr.Error()) + log.Printf("xml parse error: %s", rsserr.Error()) + log.Printf("rdf parse error: %s", rdferr.Error()) + return nil, fmt.Errorf("Could not parse feed data") + } + return f, nil +} + +func parseAtom(body []byte) (*feed, error) { + var f feed + var a atom.Feed + d := xml.NewDecoder(bytes.NewReader(body)) + d.CharsetReader = charset.NewReader + if err := d.Decode(&a); err != nil { + return nil, err + } + f.Title = a.Title + if t, err := parseDate(string(a.Updated)); err == nil { + f.Updated = t + } + fb, err := url.Parse(a.XMLBase) + if err != nil { + fb, _ = url.Parse("") + } + if len(a.Link) > 0 { + f.Link = findBestAtomLink(a.Link) + if l, err := fb.Parse(f.Link); err == nil { + f.Link = l.String() + } + } + + for _, i := range a.Entry { + eb, err := fb.Parse(i.XMLBase) + if err != nil { + eb = fb + } + st := item{ + ID: i.ID, + Title: atomTitle(i.Title), + } + if t, err := parseDate(string(i.Updated)); err == nil { + st.Updated = t + } + if t, err := parseDate(string(i.Published)); err == nil { + st.Published = t + } + if len(i.Link) > 0 { + st.Link = findBestAtomLink(i.Link) + if l, err := eb.Parse(st.Link); err == nil { + st.Link = l.String() + } + } + if i.Author != nil { + st.Author = i.Author.Name + } + if i.Content != nil { + if len(strings.TrimSpace(i.Content.Body)) != 0 { + st.Content = i.Content.Body + } else if len(i.Content.InnerXML) != 0 { + st.Content = i.Content.InnerXML + } + } else if i.Summary != nil { + st.Content = i.Summary.Body + } + f.Items = append(f.Items, &st) + } + return &f, nil +} + +func parseRSS(body []byte) (*feed, error) { + var f feed + var r rss.RSS + d := xml.NewDecoder(bytes.NewReader(body)) + d.CharsetReader = charset.NewReader + d.DefaultSpace = "DefaultSpace" + if err := d.Decode(&r); err != nil { + return nil, err + } + f.Title = r.Title + if t, err := parseDate(r.LastBuildDate, r.PubDate); err == nil { + f.Updated = t + } + f.Link = r.BaseLink() + + for _, i := range r.Items { + st := item{ + Link: i.Link, + Author: i.Author, + } + if i.Content != "" { + st.Content = i.Content + } else if i.Description != "" { + st.Content = i.Description + } + if i.Title != "" { + st.Title = i.Title + } else if i.Description != "" { + st.Title = i.Description + } + if st.Content == st.Title { + st.Title = "" + } + st.Title = textTitle(st.Title) + if i.Guid != nil { + st.ID = i.Guid.Guid + } + if i.Enclosure != nil && strings.HasPrefix(i.Enclosure.Type, "audio/") { + st.MediaContent = i.Enclosure.Url + } else if i.Media != nil && strings.HasPrefix(i.Media.Type, "audio/") { + st.MediaContent = i.Media.URL + } + if t, err := parseDate(i.PubDate, i.Date, i.Published); err == nil { + st.Published = t + st.Updated = t + } + f.Items = append(f.Items, &st) + } + + return &f, nil +} + +func parseRDF(body []byte) (*feed, error) { + var f feed + var rd rdf.RDF + d := xml.NewDecoder(bytes.NewReader(body)) + d.CharsetReader = charset.NewReader + if err := d.Decode(&rd); err != nil { + return nil, err + } + if rd.Channel != nil { + f.Title = rd.Channel.Title + f.Link = rd.Channel.Link + if t, err := parseDate(rd.Channel.Date); err == nil { + f.Updated = t + } + } + + for _, i := range rd.Item { + st := item{ + ID: i.About, + Title: textTitle(i.Title), + Link: i.Link, + Author: i.Creator, + } + if len(i.Description) > 0 { + st.Content = html.UnescapeString(i.Description) + } else if len(i.Content) > 0 { + st.Content = html.UnescapeString(i.Content) + } + if t, err := parseDate(i.Date); err == nil { + st.Published = t + st.Updated = t + } + f.Items = append(f.Items, &st) + } + + return &f, nil +} + +func textTitle(t string) string { + return html.UnescapeString(t) +} + +func atomTitle(t *atom.Text) string { + if t == nil { + return "" + } + if t.Type == "html" { + // see: https://github.com/mjibson/goread/blob/59aec794f3ef87b36c1bac029438c33a6aa6d8d3/utils.go#L533 + //return html.UnescapeString(sanitizer.StripTags(t.Body)) + } + return textTitle(t.Body) +} + +func findBestAtomLink(links []atom.Link) string { + getScore := func(l atom.Link) int { + switch { + case l.Rel == "hub": + return 0 + case l.Rel == "alternate" && l.Type == "text/html": + return 5 + case l.Type == "text/html": + return 4 + case l.Rel == "self": + return 2 + case l.Rel == "": + return 3 + default: + return 1 + } + } + + var bestlink string + bestscore := -1 + for _, l := range links { + score := getScore(l) + if score > bestscore { + bestlink = l.Href + bestscore = score + } + } + + return bestlink +} + +func parseFix(f *feed, feedURL string) (*feed, error) { + f.Link = strings.TrimSpace(f.Link) + f.Title = html.UnescapeString(strings.TrimSpace(f.Title)) + + if u, err := url.Parse(feedURL); err == nil { + if ul, err := u.Parse(f.Link); err == nil { + f.Link = ul.String() + } + } + base, err := url.Parse(f.Link) + if err != nil { + log.Printf("unable to parse link: %v", f.Link) + } + + var nss []*item + now := time.Now() + for _, s := range f.Items { + s.Created = now + s.Link = strings.TrimSpace(s.Link) + if s.ID == "" { + if s.Link != "" { + s.ID = s.Link + } else if s.Title != "" { + s.ID = s.Title + } else { + log.Printf("item has no id: %v", s) + continue + } + } + // if a story doesn't have a link, see if its id is a URL + if s.Link == "" { + if u, err := url.Parse(s.ID); err == nil { + s.Link = u.String() + } + } + if base != nil && s.Link != "" { + link, err := base.Parse(s.Link) + if err == nil { + s.Link = link.String() + } else { + log.Printf("unable to resolve link: %v", s.Link) + } + } + nss = append(nss, s) + } + f.Items = nss + + return f, nil +} + +var dateFormats = []string{ + "01-02-2006", + "01/02/2006", + "01/02/2006 - 15:04", + "01/02/2006 15:04:05 MST", + "01/02/2006 3:04 PM", + "02-01-2006", + "02/01/2006", + "02.01.2006 -0700", + "02/01/2006 - 15:04", + "02.01.2006 15:04", + "02/01/2006 15:04:05", + "02.01.2006 15:04:05", + "02-01-2006 15:04:05 MST", + "02/01/2006 15:04 MST", + "02 Jan 2006", + "02 Jan 2006 15:04:05", + "02 Jan 2006 15:04:05 -0700", + "02 Jan 2006 15:04:05 MST", + "02 Jan 2006 15:04:05 UT", + "02 Jan 2006 15:04 MST", + "02 Monday, Jan 2006 15:04", + "06-1-2 15:04", + "06/1/2 15:04", + "1/2/2006", + "1/2/2006 15:04:05 MST", + "1/2/2006 3:04:05 PM", + "1/2/2006 3:04:05 PM MST", + "15:04 02.01.2006 -0700", + "2006-01-02", + "2006/01/02", + "2006-01-02 00:00:00.0 15:04:05.0 -0700", + "2006-01-02 15:04", + "2006-01-02 15:04:05 -0700", + "2006-01-02 15:04:05-07:00", + "2006-01-02 15:04:05-0700", + "2006-01-02 15:04:05 MST", + "2006-01-02 15:04:05Z", + "2006-01-02 at 15:04:05", + "2006-01-02T15:04:05", + "2006-01-02T15:04:05:00", + "2006-01-02T15:04:05 -0700", + "2006-01-02T15:04:05-07:00", + "2006-01-02T15:04:05-0700", + "2006-01-02T15:04:05:-0700", + "2006-01-02T15:04:05-07:00:00", + "2006-01-02T15:04:05Z", + "2006-01-02T15:04-07:00", + "2006-01-02T15:04Z", + "2006-1-02T15:04:05Z", + "2006-1-2", + "2006-1-2 15:04:05", + "2006-1-2T15:04:05Z", + "2006 January 02", + "2-1-2006", + "2/1/2006", + "2.1.2006 15:04:05", + "2 Jan 2006", + "2 Jan 2006 15:04:05 -0700", + "2 Jan 2006 15:04:05 MST", + "2 Jan 2006 15:04:05 Z", + "2 January 2006", + "2 January 2006 15:04:05 -0700", + "2 January 2006 15:04:05 MST", + "6-1-2 15:04", + "6/1/2 15:04", + "Jan 02, 2006", + "Jan 02 2006 03:04:05PM", + "Jan 2, 2006", + "Jan 2, 2006 15:04:05 MST", + "Jan 2, 2006 3:04:05 PM", + "Jan 2, 2006 3:04:05 PM MST", + "January 02, 2006", + "January 02, 2006 03:04 PM", + "January 02, 2006 15:04", + "January 02, 2006 15:04:05 MST", + "January 2, 2006", + "January 2, 2006 03:04 PM", + "January 2, 2006 15:04:05", + "January 2, 2006 15:04:05 MST", + "January 2, 2006, 3:04 p.m.", + "January 2, 2006 3:04 PM", + "Mon, 02 Jan 06 15:04:05 MST", + "Mon, 02 Jan 2006", + "Mon, 02 Jan 2006 15:04:05", + "Mon, 02 Jan 2006 15:04:05 00", + "Mon, 02 Jan 2006 15:04:05 -07", + "Mon 02 Jan 2006 15:04:05 -0700", + "Mon, 02 Jan 2006 15:04:05 --0700", + "Mon, 02 Jan 2006 15:04:05 -07:00", + "Mon, 02 Jan 2006 15:04:05 -0700", + "Mon,02 Jan 2006 15:04:05 -0700", + "Mon, 02 Jan 2006 15:04:05 GMT-0700", + "Mon , 02 Jan 2006 15:04:05 MST", + "Mon, 02 Jan 2006 15:04:05 MST", + "Mon, 02 Jan 2006 15:04:05MST", + "Mon, 02 Jan 2006, 15:04:05 MST", + "Mon, 02 Jan 2006 15:04:05 MST -0700", + "Mon, 02 Jan 2006 15:04:05 MST-07:00", + "Mon, 02 Jan 2006 15:04:05 UT", + "Mon, 02 Jan 2006 15:04:05 Z", + "Mon, 02 Jan 2006 15:04 -0700", + "Mon, 02 Jan 2006 15:04 MST", + "Mon,02 Jan 2006 15:04 MST", + "Mon, 02 Jan 2006 15 -0700", + "Mon, 02 Jan 2006 3:04:05 PM MST", + "Mon, 02 January 2006", + "Mon,02 January 2006 14:04:05 MST", + "Mon, 2006-01-02 15:04", + "Mon, 2 Jan 06 15:04:05 -0700", + "Mon, 2 Jan 06 15:04:05 MST", + "Mon, 2 Jan 15:04:05 MST", + "Mon, 2 Jan 2006", + "Mon,2 Jan 2006", + "Mon, 2 Jan 2006 15:04", + "Mon, 2 Jan 2006 15:04:05", + "Mon, 2 Jan 2006 15:04:05 -0700", + "Mon, 2 Jan 2006 15:04:05-0700", + "Mon, 2 Jan 2006 15:04:05 -0700 MST", + "mon,2 Jan 2006 15:04:05 MST", + "Mon 2 Jan 2006 15:04:05 MST", + "Mon, 2 Jan 2006 15:04:05 MST", + "Mon, 2 Jan 2006 15:04:05MST", + "Mon, 2 Jan 2006 15:04:05 UT", + "Mon, 2 Jan 2006 15:04 -0700", + "Mon, 2 Jan 2006, 15:04 -0700", + "Mon, 2 Jan 2006 15:04 MST", + "Mon, 2, Jan 2006 15:4", + "Mon, 2 Jan 2006 15:4:5 -0700 GMT", + "Mon, 2 Jan 2006 15:4:5 MST", + "Mon, 2 Jan 2006 3:04:05 PM -0700", + "Mon, 2 January 2006", + "Mon, 2 January 2006 15:04:05 -0700", + "Mon, 2 January 2006 15:04:05 MST", + "Mon, 2 January 2006, 15:04:05 MST", + "Mon, 2 January 2006, 15:04 -0700", + "Mon, 2 January 2006 15:04 MST", + "Monday, 02 January 2006 15:04:05", + "Monday, 02 January 2006 15:04:05 -0700", + "Monday, 02 January 2006 15:04:05 MST", + "Monday, 2 Jan 2006 15:04:05 -0700", + "Monday, 2 Jan 2006 15:04:05 MST", + "Monday, 2 January 2006 15:04:05 -0700", + "Monday, 2 January 2006 15:04:05 MST", + "Monday, January 02, 2006", + "Monday, January 2, 2006", + "Monday, January 2, 2006 03:04 PM", + "Monday, January 2, 2006 15:04:05 MST", + "Mon Jan 02 2006 15:04:05 -0700", + "Mon, Jan 02,2006 15:04:05 MST", + "Mon Jan 02, 2006 3:04 pm", + "Mon Jan 2 15:04:05 2006 MST", + "Mon Jan 2 15:04 2006", + "Mon, Jan 2 2006 15:04:05 -0700", + "Mon, Jan 2 2006 15:04:05 -700", + "Mon, Jan 2, 2006 15:04:05 MST", + "Mon, Jan 2 2006 15:04 MST", + "Mon, Jan 2, 2006 15:04 MST", + "Mon, January 02, 2006 15:04:05 MST", + "Mon, January 02, 2006, 15:04:05 MST", + "Mon, January 2 2006 15:04:05 -0700", + "Updated January 2, 2006", + time.ANSIC, + time.RFC1123, + time.RFC1123Z, + time.RFC3339, + time.RFC822, + time.RFC822Z, + time.RFC850, + time.RubyDate, + time.UnixDate, +} + +func parseDate(ds ...string) (t time.Time, err error) { + for _, d := range ds { + d = strings.TrimSpace(d) + if d == "" { + continue + } + for _, f := range dateFormats { + if t, err = time.Parse(f, d); err == nil { + return + } + } + } + err = fmt.Errorf("could not parse dates: %v", strings.Join(ds, ", ")) + return +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/feed/rdf/rdf.go b/vendor/github.com/camlistore/camlistore/pkg/importer/feed/rdf/rdf.go new file mode 100644 index 00000000..f931c419 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/feed/rdf/rdf.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2013 Matt Jibson + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +// Package rdf defines XML data structures for an RDF feed. +package rdf + +import ( + "encoding/xml" +) + +type RDF struct { + XMLName xml.Name `xml:"RDF"` + Channel *Channel `xml:"channel"` + Item []*Item `xml:"item"` +} + +type Channel struct { + Title string `xml:"title"` + Description string `xml:"description"` + Link string `xml:"link"` + Date string `xml:"date"` +} + +type Item struct { + About string `xml:"about,attr"` + Format string `xml:"format"` + Date string `xml:"date"` + Source string `xml:"source"` + Creator string `xml:"creator"` + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + Content string `xml:"encoded"` +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/feed/rss/rss.go b/vendor/github.com/camlistore/camlistore/pkg/importer/feed/rss/rss.go new file mode 100644 index 00000000..2598c042 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/feed/rss/rss.go @@ -0,0 +1,69 @@ +// Copyright 2012 Evan Farrer. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package rss defines XML data structures for an RSS feed. +package rss + +type RSS struct { + XMLName string `xml:"rss"` + Title string `xml:"channel>title"` + Link []Link `xml:"channel>link"` + Description string `xml:"channel>description"` + PubDate string `xml:"channel>pubDate,omitempty"` + LastBuildDate string `xml:"channel>lastBuildDate,omitempty"` + Items []*Item `xml:"channel>item"` +} + +func (r *RSS) BaseLink() string { + for _, l := range r.Link { + if l.Rel == "" && l.Type == "" && l.Href == "" && l.Chardata != "" { + return l.Chardata + } + } + return "" +} + +type Link struct { + Rel string `xml:"rel,attr"` + Href string `xml:"href,attr"` + Type string `xml:"type,attr"` + Chardata string `xml:",chardata"` +} + +type Item struct { + Title string `xml:"title,omitempty"` + Link string `xml:"link,omitempty"` + Description string `xml:"description,omitempty"` + Author string `xml:"author,omitempty"` + Enclosure *Enclosure `xml:"enclosure"` + Guid *Guid `xml:"guid"` + PubDate string `xml:"pubDate,omitempty"` + Source *Source `xml:"source"` + Content string `xml:"encoded,omitempty"` + Date string `xml:"date,omitempty"` + Published string `xml:"published,omitempty"` + Media *MediaContent `xml:"content"` +} + +type MediaContent struct { + XMLBase string `xml:"http://search.yahoo.com/mrss/ content"` + URL string `xml:"url,attr"` + Type string `xml:"type,attr"` +} + +type Source struct { + Source string `xml:",chardata"` + Url string `xml:"url,attr"` +} + +type Guid struct { + Guid string `xml:",chardata"` + IsPermaLink bool `xml:"isPermaLink,attr,omitempty"` +} + +type Enclosure struct { + Url string `xml:"url,attr"` + Length string `xml:"length,attr,omitempty"` + Type string `xml:"type,attr"` +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/flickr/README b/vendor/github.com/camlistore/camlistore/pkg/importer/flickr/README new file mode 100644 index 00000000..395d73cd --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/flickr/README @@ -0,0 +1,20 @@ +Flickr Importer +=============== + +This is an incomplete Camlistore importer for Flickr. So far it can import the +first 100 photos from a photostream and also their set metadata. + +To use: + +1) Fill out http://www.flickr.com/services/apps/create/noncommercial/ to get a + Flickr API key and secret. +2) Start the devcam server with flickrapikey flag: + $ devcam server -flickrapikey=: +3) Navigate to http:///importer-flickr/login +4) Watch import progress on the command line + + +TODO: + +https://github.com/camlistore/camlistore/issues?q=is%3Aopen+flickr+ + diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/flickr/flickr.go b/vendor/github.com/camlistore/camlistore/pkg/importer/flickr/flickr.go new file mode 100644 index 00000000..07094b6e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/flickr/flickr.go @@ -0,0 +1,586 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package flickr implements an importer for flickr.com accounts. +package flickr + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "net/url" + "strconv" + "time" + + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/schema/nodeattr" + "camlistore.org/third_party/github.com/garyburd/go-oauth/oauth" +) + +const ( + apiURL = "https://api.flickr.com/services/rest/" + temporaryCredentialRequestURL = "https://www.flickr.com/services/oauth/request_token" + resourceOwnerAuthorizationURL = "https://www.flickr.com/services/oauth/authorize" + tokenRequestURL = "https://www.flickr.com/services/oauth/access_token" + + photosetsAPIPath = "flickr.photosets.getList" + photosetAPIPath = "flickr.photosets.getPhotos" + photosAPIPath = "flickr.people.getPhotos" + + attrFlickrId = "flickrId" +) + +var oAuthURIs = importer.OAuthURIs{ + TemporaryCredentialRequestURI: temporaryCredentialRequestURL, + ResourceOwnerAuthorizationURI: resourceOwnerAuthorizationURL, + TokenRequestURI: tokenRequestURL, +} + +func init() { + importer.Register("flickr", imp{}) +} + +var _ importer.ImporterSetupHTMLer = imp{} + +type imp struct { + importer.OAuth1 // for CallbackRequestAccount and CallbackURLParameters +} + +func (imp) NeedsAPIKey() bool { return true } + +func (imp) SupportsIncremental() bool { return false } + +func (imp) IsAccountReady(acctNode *importer.Object) (ok bool, err error) { + return acctNode.Attr(importer.AcctAttrUserName) != "" && acctNode.Attr(importer.AcctAttrAccessToken) != "", nil +} + +func (im imp) SummarizeAccount(acct *importer.Object) string { + ok, err := im.IsAccountReady(acct) + if err != nil || !ok { + return "" + } + return acct.Attr(importer.AcctAttrUserName) +} + +func (imp) AccountSetupHTML(host *importer.Host) string { + base := host.ImporterBaseURL() + "flickr" + return fmt.Sprintf(` +

    Configuring Flickr

    +

    Visit http://www.flickr.com/services/apps/create/noncommercial/, fill out whatever's needed, and click on SUBMIT.

    +

    From your newly created app's main page, go to "Edit the authentication flow", use the following settings:

    +
      +
    • App Type: Web Application
    • +
    • Callback URL: %s
    • +
    +

    and SAVE CHANGES

    +

    Then go to "View the API Key for this app", and copy the "Key" and "Secret" into the "Client ID" and "Client Secret" boxes above.

    +`, base+"/callback") +} + +// A run is our state for a given run of the importer. +type run struct { + userID string + *importer.RunContext + oauthClient *oauth.Client // No need to guard, used read-only. + accessCreds *oauth.Credentials // No need to guard, used read-only. + + // primaryPhoto maps an album id to the id of its primary photo. + // If some concurrency is added to some of the importing routines, + // it will need some guarding. + primaryPhoto map[string]string +} + +func (imp) Run(ctx *importer.RunContext) error { + clientID, secret, err := ctx.Credentials() + if err != nil { + return fmt.Errorf("no API credentials: %v", err) + } + accountNode := ctx.AccountNode() + accessToken := accountNode.Attr(importer.AcctAttrAccessToken) + accessSecret := accountNode.Attr(importer.AcctAttrAccessTokenSecret) + if accessToken == "" || accessSecret == "" { + return errors.New("access credentials not found") + } + userID := ctx.AccountNode().Attr(importer.AcctAttrUserID) + if userID == "" { + return errors.New("UserID hasn't been set by account setup.") + } + r := &run{ + userID: userID, + RunContext: ctx, + oauthClient: &oauth.Client{ + TemporaryCredentialRequestURI: temporaryCredentialRequestURL, + ResourceOwnerAuthorizationURI: resourceOwnerAuthorizationURL, + TokenRequestURI: tokenRequestURL, + Credentials: oauth.Credentials{ + Token: clientID, + Secret: secret, + }, + }, + accessCreds: &oauth.Credentials{ + Token: accessToken, + Secret: accessSecret, + }, + primaryPhoto: make(map[string]string), + } + + if err := r.importPhotosets(); err != nil { + return err + } + if err := r.importPhotos(); err != nil { + return err + } + return nil +} + +type photosetList struct { + Page jsonInt + Pages jsonInt + PerPage jsonInt + Photoset []*photosetInfo +} + +type photosetInfo struct { + Id string `json:"id"` + PrimaryPhotoId string `json:"primary"` + Title contentString + Description contentString +} + +type photosetItems struct { + Id string `json:"id"` + Page jsonInt + Pages jsonInt + Photo []struct { + Id string + OriginalFormat string + } +} + +func (r *run) importPhotosets() error { + resp := struct { + Photosets photosetList + }{} + if err := r.flickrAPIRequest(&resp, + photosetsAPIPath, "user_id", r.userID); err != nil { + return err + } + + setsNode, err := r.getTopLevelNode("sets", "Sets") + if err != nil { + return err + } + log.Printf("Importing %d sets", len(resp.Photosets.Photoset)) + + for _, item := range resp.Photosets.Photoset { + if r.Context.IsCanceled() { + log.Printf("Flickr importer: interrupted") + return context.ErrCanceled + } + for page := 1; page >= 1; { + page, err = r.importPhotoset(setsNode, item, page) + if err != nil { + log.Printf("Flickr importer: error importing photoset %s: %s", item.Id, err) + continue + } + } + } + return nil +} + +func (r *run) importPhotoset(parent *importer.Object, photoset *photosetInfo, page int) (int, error) { + photosetNode, err := parent.ChildPathObject(photoset.Id) + if err != nil { + return 0, err + } + + if err := photosetNode.SetAttrs( + attrFlickrId, photoset.Id, + nodeattr.Title, photoset.Title.Content, + nodeattr.Description, photoset.Description.Content); err != nil { + return 0, err + } + // keep track of primary photo so we can set the fileRef of the photo as CamliContentImage + // on photosetNode when we eventually know that fileRef. + r.primaryPhoto[photoset.Id] = photoset.PrimaryPhotoId + + resp := struct { + Photoset photosetItems + }{} + if err := r.flickrAPIRequest(&resp, photosetAPIPath, "user_id", r.userID, + "page", fmt.Sprintf("%d", page), "photoset_id", photoset.Id, "extras", "original_format"); err != nil { + return 0, err + } + + log.Printf("Importing page %d from photoset %s", page, photoset.Id) + + photosNode, err := r.getPhotosNode() + if err != nil { + return 0, err + } + + for _, item := range resp.Photoset.Photo { + filename := fmt.Sprintf("%s.%s", item.Id, item.OriginalFormat) + photoNode, err := photosNode.ChildPathObject(filename) + if err != nil { + log.Printf("Flickr importer: error finding photo node %s for addition to photoset %s: %s", + item.Id, photoset.Id, err) + continue + } + if err := photosetNode.SetAttr("camliPath:"+filename, photoNode.PermanodeRef().String()); err != nil { + log.Printf("Flickr importer: error adding photo %s to photoset %s: %s", + item.Id, photoset.Id, err) + } + } + + if resp.Photoset.Page < resp.Photoset.Pages { + return page + 1, nil + } else { + return 0, nil + } +} + +type photosSearch struct { + Photos struct { + Page jsonInt + Pages jsonInt + Perpage jsonInt + Total jsonInt + Photo []*photosSearchItem + } + + Stat string +} + +type photosSearchItem struct { + Id string `json:"id"` + Title string + IsPublic jsonInt + IsFriend jsonInt + IsFamily jsonInt + Description contentString + DateUpload string // Unix timestamp, in GMT. + DateTaken string // formatted as "2006-01-02 15:04:05", so no timezone info. + OriginalFormat string + LastUpdate string // Unix timestamp. + Latitude jsonFloat + Longitude jsonFloat + Tags string + MachineTags string `json:"machine_tags"` + Views string + Media string + URL string `json:"url_o"` +} + +type contentString struct { + Content string `json:"_content"` +} + +// jsonInt is for unmarshaling quoted and unquoted integers ("0" and 0), too. +type jsonInt int + +func (jf jsonInt) MarshalJSON() ([]byte, error) { + return json.Marshal(int(jf)) +} +func (jf *jsonInt) UnmarshalJSON(p []byte) error { + return json.Unmarshal(bytes.Trim(p, `"`), (*int)(jf)) +} + +// jsonFloat is for unmarshaling quoted and unquoted numbers ("0" and 0), too. +type jsonFloat float32 + +func (jf jsonFloat) MarshalJSON() ([]byte, error) { + return json.Marshal(float32(jf)) +} +func (jf *jsonFloat) UnmarshalJSON(p []byte) error { + if len(p) == 1 && p[0] == '0' { // shortcut + *jf = 0 + return nil + } + return json.Unmarshal(bytes.Trim(p, `"`), (*float32)(jf)) +} + +func (r *run) importPhotos() error { + for page := 1; page >= 1; { + var err error + page, err = r.importPhotosPage(page) + if err != nil { + return err + } + } + return nil +} + +func (r *run) importPhotosPage(page int) (int, error) { + resp := photosSearch{} + if err := r.flickrAPIRequest(&resp, photosAPIPath, "user_id", r.userID, "page", fmt.Sprintf("%d", page), + "extras", "description,date_upload,date_taken,original_format,last_update,geo,tags,machine_tags,views,media,url_o"); err != nil { + return 0, err + } + + photosNode, err := r.getPhotosNode() + if err != nil { + return 0, err + } + log.Printf("Importing %d photos on page %d of %d", len(resp.Photos.Photo), page, resp.Photos.Pages) + + for _, item := range resp.Photos.Photo { + if err := r.importPhoto(photosNode, item); err != nil { + log.Printf("Flickr importer: error importing %s: %s", item.Id, err) + continue + } + } + + if resp.Photos.Pages > resp.Photos.Page { + return page + 1, nil + } else { + return 0, nil + } +} + +// TODO(aa): +// * Parallelize: http://golang.org/doc/effective_go.html#concurrency +// * Do more than one "page" worth of results +// * Report progress and errors back through host interface +// * All the rest of the metadata (see photoMeta) +// * Conflicts: For all metadata changes, prefer any non-imported claims +// * Test! +func (r *run) importPhoto(parent *importer.Object, photo *photosSearchItem) error { + filename := fmt.Sprintf("%s.%s", photo.Id, photo.OriginalFormat) + photoNode, err := parent.ChildPathObject(filename) + if err != nil { + return err + } + + // https://www.flickr.com/services/api/misc.dates.html + dateTaken, err := time.ParseInLocation("2006-01-02 15:04:05", photo.DateTaken, schema.UnknownLocation) + if err != nil { + // default to the published date otherwise + log.Printf("Flickr importer: problem with date taken of photo %v, defaulting to published date instead.", photo.Id) + seconds, err := strconv.ParseInt(photo.DateUpload, 10, 64) + if err != nil { + return fmt.Errorf("could not parse date upload time %q for image %v: %v", photo.DateUpload, photo.Id, err) + } + dateTaken = time.Unix(seconds, 0) + } + + attrs := []string{ + attrFlickrId, photo.Id, + nodeattr.DateCreated, schema.RFC3339FromTime(dateTaken), + nodeattr.Description, photo.Description.Content, + } + if schema.IsInterestingTitle(photo.Title) { + attrs = append(attrs, nodeattr.Title, photo.Title) + } + // Import all the metadata. SetAttrs() is a no-op if the value hasn't changed, so there's no cost to doing these on every run. + // And this way if we add more things to import, they will get picked up. + if err := photoNode.SetAttrs(attrs...); err != nil { + return err + } + + // Import the photo itself. Since it is expensive to fetch the image, we store its lastupdate and only refetch if it might have changed. + // lastupdate is a Unix timestamp according to https://www.flickr.com/services/api/flickr.photos.getInfo.html + seconds, err := strconv.ParseInt(photo.LastUpdate, 10, 64) + if err != nil { + return fmt.Errorf("could not parse lastupdate time for image %v: %v", photo.Id, err) + } + lastUpdate := time.Unix(seconds, 0) + if lastUpdateString := photoNode.Attr(nodeattr.DateModified); lastUpdateString != "" { + oldLastUpdate, err := time.Parse(time.RFC3339, lastUpdateString) + if err != nil { + return fmt.Errorf("could not parse last stored update time for image %v: %v", photo.Id, err) + } + if lastUpdate.Equal(oldLastUpdate) { + if err := r.updatePrimaryPhoto(photoNode); err != nil { + return err + } + return nil + } + } + form := url.Values{} + form.Set("user_id", r.userID) + res, err := r.fetch(photo.URL, form) + if err != nil { + log.Printf("Flickr importer: Could not fetch %s: %s", photo.URL, err) + return err + } + defer res.Body.Close() + + fileRef, err := schema.WriteFileFromReader(r.Host.Target(), filename, res.Body) + if err != nil { + return err + } + if err := photoNode.SetAttr(nodeattr.CamliContent, fileRef.String()); err != nil { + return err + } + if err := r.updatePrimaryPhoto(photoNode); err != nil { + return err + } + // Write lastupdate last, so that if any of the preceding fails, we will try again next time. + if err := photoNode.SetAttr(nodeattr.DateModified, schema.RFC3339FromTime(lastUpdate)); err != nil { + return err + } + + return nil +} + +// updatePrimaryPhoto uses the camliContent of photoNode to set the +// camliContentImage of any album for which photoNode is the primary photo. +func (r *run) updatePrimaryPhoto(photoNode *importer.Object) error { + photoId := photoNode.Attr(attrFlickrId) + for album, photo := range r.primaryPhoto { + if photoId != photo { + continue + } + setsNode, err := r.getTopLevelNode("sets", "Sets") + if err != nil { + return fmt.Errorf("could not set %v as primary photo of %v, no root sets: %v", photoId, album, err) + } + setNode, err := setsNode.ChildPathObject(album) + if err != nil { + return fmt.Errorf("could not set %v as primary photo of %v, no album: %v", photoId, album, err) + } + fileRef := photoNode.Attr(nodeattr.CamliContent) + if fileRef == "" { + return fmt.Errorf("could not set %v as primary photo of %v: fileRef of photo is unknown", photoId, album) + } + if err := setNode.SetAttr(nodeattr.CamliContentImage, fileRef); err != nil { + return fmt.Errorf("could not set %v as primary photo of %v: %v", photoId, album, err) + } + delete(r.primaryPhoto, album) + } + return nil +} + +func (r *run) getPhotosNode() (*importer.Object, error) { + return r.getTopLevelNode("photos", "Photos") +} + +func (r *run) getTopLevelNode(path string, title string) (*importer.Object, error) { + photos, err := r.RootNode().ChildPathObject(path) + if err != nil { + return nil, err + } + + if err := photos.SetAttr(nodeattr.Title, title); err != nil { + return nil, err + } + return photos, nil +} + +func (r *run) flickrAPIRequest(result interface{}, method string, keyval ...string) error { + keyval = append([]string{"method", method, "format", "json", "nojsoncallback", "1"}, keyval...) + return importer.OAuthContext{ + r.Context, + r.oauthClient, + r.accessCreds}.PopulateJSONFromURL(result, apiURL, keyval...) +} + +func (r *run) fetch(url string, form url.Values) (*http.Response, error) { + return importer.OAuthContext{ + r.Context, + r.oauthClient, + r.accessCreds}.Get(url, form) +} + +// TODO(mpl): same in twitter. refactor. Except for the additional perms in AuthorizationURL call. +func (imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error { + oauthClient, err := ctx.NewOAuthClient(oAuthURIs) + if err != nil { + err = fmt.Errorf("error getting OAuth client: %v", err) + httputil.ServeError(w, r, err) + return err + } + tempCred, err := oauthClient.RequestTemporaryCredentials(ctx.HTTPClient(), ctx.CallbackURL(), nil) + if err != nil { + err = fmt.Errorf("Error getting temp cred: %v", err) + httputil.ServeError(w, r, err) + return err + } + if err := ctx.AccountNode.SetAttrs( + importer.AcctAttrTempToken, tempCred.Token, + importer.AcctAttrTempSecret, tempCred.Secret, + ); err != nil { + err = fmt.Errorf("Error saving temp creds: %v", err) + httputil.ServeError(w, r, err) + return err + } + + authURL := oauthClient.AuthorizationURL(tempCred, url.Values{"perms": {"read"}}) + http.Redirect(w, r, authURL, http.StatusFound) + return nil +} + +func (imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) { + tempToken := ctx.AccountNode.Attr(importer.AcctAttrTempToken) + tempSecret := ctx.AccountNode.Attr(importer.AcctAttrTempSecret) + if tempToken == "" || tempSecret == "" { + log.Printf("flicker: no temp creds in callback") + httputil.BadRequestError(w, "no temp creds in callback") + return + } + if tempToken != r.FormValue("oauth_token") { + log.Printf("unexpected oauth_token: got %v, want %v", r.FormValue("oauth_token"), tempToken) + httputil.BadRequestError(w, "unexpected oauth_token") + return + } + oauthClient, err := ctx.NewOAuthClient(oAuthURIs) + if err != nil { + err = fmt.Errorf("error getting OAuth client: %v", err) + httputil.ServeError(w, r, err) + return + } + tokenCred, vals, err := oauthClient.RequestToken( + ctx.Context.HTTPClient(), + &oauth.Credentials{ + Token: tempToken, + Secret: tempSecret, + }, + r.FormValue("oauth_verifier"), + ) + if err != nil { + httputil.ServeError(w, r, fmt.Errorf("Error getting request token: %v ", err)) + return + } + userID := vals.Get("user_nsid") + if userID == "" { + httputil.ServeError(w, r, fmt.Errorf("Couldn't get user id: %v", err)) + return + } + username := vals.Get("username") + if username == "" { + httputil.ServeError(w, r, fmt.Errorf("Couldn't get user name: %v", err)) + return + } + + // TODO(mpl): get a few more bits of info (first name, last name etc) like I did for twitter, if possible. + if err := ctx.AccountNode.SetAttrs( + importer.AcctAttrAccessToken, tokenCred.Token, + importer.AcctAttrAccessTokenSecret, tokenCred.Secret, + importer.AcctAttrUserID, userID, + importer.AcctAttrUserName, username, + ); err != nil { + httputil.ServeError(w, r, fmt.Errorf("Error setting basic account attributes: %v", err)) + return + } + http.Redirect(w, r, ctx.AccountURL(), http.StatusFound) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/flickr/flickr_test.go b/vendor/github.com/camlistore/camlistore/pkg/importer/flickr/flickr_test.go new file mode 100644 index 00000000..605845b9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/flickr/flickr_test.go @@ -0,0 +1,254 @@ +/* +Copyright 2015 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package flickr + +import ( + "encoding/json" + "testing" +) + +func TestParseJSONFloat(t *testing.T) { + var ps photosSearch + if err := json.Unmarshal([]byte(photosSearchData1), &ps); err != nil { + t.Errorf("unmarshal error: %v", err) + } + for i, want := range []jsonFloat{-37.899166, -37.899166, 0} { + if ps.Photos.Photo[i].Latitude != want { + t.Errorf("%d. want latitude=%f got %f", i+1, want, ps.Photos.Photo[i].Latitude) + } + } +} +func TestParseJSONInt(t *testing.T) { + var ps photosSearch + if err := json.Unmarshal([]byte(photosSearchData2), &ps); err != nil { + t.Errorf("unmarshal error: %v", err) + } + for i, want := range []jsonInt{1, 1} { + if ps.Photos.Photo[i].IsPublic != want { + t.Errorf("%d. want ispublic=%d, got %d", i+1, want, ps.Photos.Photo[i].IsPublic) + } + } +} + +const ( + photosSearchData2 = `{ + "photos": { + "page": 63, + "pages": 63, + "perpage": 100, + "total": 6236, + "photo": [ + { + "id": "0000084", + "owner": "00002737@N00", + "secret": "0000f03945", + "server": "3", + "farm": 1, + "title": "Machinery in Brickmakers' park", + "ispublic": 1, + "isfriend": 0, + "isfamily": 0, + "description": { + "_content": "" + }, + "dateupload": "1106791534", + "lastupdate": "1251702196", + "datetaken": "2005-01-26 07:47:33", + "datetakengranularity": "0", + "datetakenunknown": 0, + "views": "145", + "tags": "australia melbourne victoria machinery oakleigh pc3166 auspctagged 3166 geo:country=australia geo:zip=3166 brickmakerspark", + "machine_tags": "geo:zip=3166 geo:country=australia", + "originalsecret": "0000f03945", + "originalformat": "jpg", + "latitude": "-37.895717", + "longitude": "145.099933", + "accuracy": "15", + "context": 0, + "place_id": "0000fRpQU7qUoVfD", + "woeid": "0000751", + "geo_is_family": 0, + "geo_is_friend": 0, + "geo_is_contact": 0, + "geo_is_public": 1, + "media": "photo", + "media_status": "ready", + "url_o": "https://farm1.staticflickr.com/3/3850184_87b1f03945_o.jpg", + "height_o": "640", + "width_o": "480" + }, + { + "id": "3850183", + "owner": "47322737@N00", + "secret": "492b7f19de", + "server": "3", + "farm": 1, + "title": "Machinery in Brickmakers' park", + "ispublic": "1", + "isfriend": 0, + "isfamily": 0, + "description": { + "_content": "" + }, + "dateupload": "1106791534", + "lastupdate": "1251702196", + "datetaken": "2005-01-26 07:50:58", + "datetakengranularity": "0", + "datetakenunknown": 0, + "views": "204", + "tags": "australia melbourne victoria machinery oakleigh pc3166 auspctagged 3166 geo:country=australia geo:zip=3166 brickmakerspark", + "machine_tags": "geo:zip=3166 geo:country=australia", + "originalsecret": "492b7f19de", + "originalformat": "jpg", + "latitude": "-37.895717", + "longitude": "145.099933", + "accuracy": "15", + "context": 0, + "place_id": "SrpyfRpQU7qUoVfD", + "woeid": "1104751", + "geo_is_family": 0, + "geo_is_friend": 0, + "geo_is_contact": 0, + "geo_is_public": 1, + "media": "photo", + "media_status": "ready", + "url_o": "https://farm1.staticflickr.com/3/3850183_492b7f19de_o.jpg", + "height_o": "480", + "width_o": "640" + } + ] + }, + "stat": "ok" +}` + + photosSearchData1 = `{ + "photos": { + "page": 1, + "pages": 63, + "perpage": 100, + "total": "6226", + "photo": [ + { + "id": "00007283018", + "owner": "00002737@N00", + "secret": "00000fa7ec", + "server": "331", + "farm": 1, + "title": "The mysterious masked man waits for his #milkshake", + "ispublic": 1, + "isfriend": 0, + "isfamily": 0, + "description": { + "_content": "" + }, + "dateupload": "1435974606", + "lastupdate": "1435974611", + "datetaken": "2015-07-04 11:50:06", + "datetakengranularity": 0, + "datetakenunknown": "1", + "views": "0", + "tags": "square squareformat juno iphoneography instagramapp uploaded:by=instagram", + "machine_tags": "uploaded:by=instagram", + "originalsecret": "0000958ab8", + "originalformat": "jpg", + "latitude": "-37.899166", + "longitude": "145.090277", + "accuracy": "16", + "context": 0, + "place_id": "0000fRpQU7qUoVfD", + "woeid": "0000751", + "geo_is_family": 0, + "geo_is_friend": 0, + "geo_is_contact": 0, + "geo_is_public": 1, + "media": "photo", + "media_status": "ready", + "url_o": "https://farm1.staticflickr.com/331/00007283018_0000958ab8_o.jpg", + "height_o": "1080", + "width_o": "1080" + }, + { + "id": "00001743956", + "owner": "00002737@N00", + "secret": "aa00088ef7", + "server": "380", + "farm": 1, + "title": "A #LEGO #maze", + "ispublic": 1, + "isfriend": 0, + "isfamily": 0, + "description": { + "_content": "" + }, + "dateupload": "1435481921", + "lastupdate": "1435481924", + "datetaken": "2015-06-28 18:58:41", + "datetakengranularity": 0, + "datetakenunknown": "1", + "views": "33", + "tags": "square squareformat lark iphoneography instagramapp uploaded:by=instagram", + "machine_tags": "uploaded:by=instagram", + "originalsecret": "000df6239a", + "originalformat": "jpg", + "latitude": -37.899166, + "longitude": "0", + "accuracy": 0, + "context": 0, + "media": "photo", + "media_status": "ready", + "url_o": "https://farm1.staticflickr.com/380/00001743956_0000f6239a_o.jpg", + "height_o": "640", + "width_o": "640" + }, + { + "id": "00001743956", + "owner": "00002737@N00", + "secret": "aa00088ef7", + "server": "380", + "farm": 1, + "title": "A #LEGO #maze", + "ispublic": 1, + "isfriend": 0, + "isfamily": 0, + "description": { + "_content": "" + }, + "dateupload": "1435481921", + "lastupdate": "1435481924", + "datetaken": "2015-06-28 18:58:41", + "datetakengranularity": 0, + "datetakenunknown": "1", + "views": "33", + "tags": "square squareformat lark iphoneography instagramapp uploaded:by=instagram", + "machine_tags": "uploaded:by=instagram", + "originalsecret": "000df6239a", + "originalformat": "jpg", + "latitude": 0, + "longitude": 0, + "accuracy": 0, + "context": 0, + "media": "photo", + "media_status": "ready", + "url_o": "https://farm1.staticflickr.com/380/00001743956_0000f6239a_o.jpg", + "height_o": "640", + "width_o": "640" + } + ] + }, + "stat": "ok" +}` +) diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/flickr/testdata.go b/vendor/github.com/camlistore/camlistore/pkg/importer/flickr/testdata.go new file mode 100644 index 00000000..715e7dac --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/flickr/testdata.go @@ -0,0 +1,288 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package flickr + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "os" + "path/filepath" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + "camlistore.org/pkg/osutil" +) + +var _ importer.TestDataMaker = imp{} + +func (im imp) SetTestAccount(acctNode *importer.Object) error { + return acctNode.SetAttrs( + importer.AcctAttrAccessToken, "fakeAccessToken", + importer.AcctAttrAccessTokenSecret, "fakeAccessSecret", + importer.AcctAttrUserID, "fakeUserId", + importer.AcctAttrName, "fakeName", + importer.AcctAttrUserName, "fakeScreenName", + ) +} + +func (im imp) MakeTestData() http.RoundTripper { + const ( + nPhotosets = 5 // Arbitrary number of sets. + perPage = 3 // number of photos per page (both when getting sets and when getting photos). + fakeUserId = "fakeUserId" + ) + // Photoset N has N photos, so we've got 15 ( = 5 + 4 + 3 + 2 + 1) photos in total. + var nPhotos int + for i := 1; i <= nPhotosets; i++ { + nPhotos += i + } + nPhotosPages := nPhotos / perPage + if nPhotos%perPage != 0 { + nPhotosPages++ + } + + okHeader := `HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 + +` + + // TODO(mpl): this scheme does not take into account that we could have the same photo + // in different albums. These two photos will end up with a different photoId. + buildPhotoIds := func(nsets, perPage int) []string { + var ids []string + for i := 1; i <= nsets; i++ { + photosetId := blob.RefFromString(fmt.Sprintf("Photoset %d", i)).DigestPrefix(10) + page := 1 + // Photoset N has N photos. + indexOnPage := 1 + for j := 1; j <= i; j++ { + photoId := blob.RefFromString(fmt.Sprintf("Photo %d on page %d of photoset %s", indexOnPage, page, photosetId)).DigestPrefix(10) + ids = append(ids, photoId) + indexOnPage++ + if indexOnPage > perPage { + page++ + indexOnPage = 1 + } + } + } + return ids + } + photoIds := buildPhotoIds(nPhotosets, perPage) + + responses := make(map[string]func() *http.Response) + // Initial photo sets list + photosetsURL := fmt.Sprintf("%s?format=json&method=%s&nojsoncallback=1&user_id=%s", apiURL, photosetsAPIPath, fakeUserId) + response := fmt.Sprintf("%s%s", okHeader, fakePhotosetsList(nPhotosets)) + responses[photosetsURL] = httputil.StaticResponder(response) + + // All the photoset calls. One call for each page of each photoset. + // Each page as perPage photos, or maybe less if end of the photoset. + { + pageStart := 0 + albumEnd, pageEnd, albumNum, pages, page := 1, 1, 1, 1, 1 + photosetId := blob.RefFromString(fmt.Sprintf("Photoset %d", albumNum)).DigestPrefix(10) + photosURL := fmt.Sprintf("%s?extras=original_format&format=json&method=%s&nojsoncallback=1&page=%d&photoset_id=%s&user_id=%s", + apiURL, photosetAPIPath, page, photosetId, fakeUserId) + response := fmt.Sprintf("%s%s", okHeader, fakePhotoset(photosetId, page, pages, photoIds[pageStart:pageEnd])) + responses[photosURL] = httputil.StaticResponder(response) + for k, _ := range photoIds { + if k < pageEnd { + continue + } + page++ + pageStart = k + pageEnd = k + perPage + if page > pages { + albumNum++ + page = 1 + pages = albumNum / perPage + if albumNum%perPage != 0 { + pages++ + } + albumEnd = pageStart + albumNum + photosetId = blob.RefFromString(fmt.Sprintf("Photoset %d", albumNum)).DigestPrefix(10) + } + if pageEnd > albumEnd { + pageEnd = albumEnd + } + photosURL := fmt.Sprintf("%s?extras=original_format&format=json&method=%s&nojsoncallback=1&page=%d&photoset_id=%s&user_id=%s", + apiURL, photosetAPIPath, page, photosetId, fakeUserId) + response := fmt.Sprintf("%s%s", okHeader, fakePhotoset(photosetId, page, pages, photoIds[pageStart:pageEnd])) + responses[photosURL] = httputil.StaticResponder(response) + } + } + + // All the photo page calls (to get the photos info). + // Each page has perPage photos, until end of photos. + for i := 1; i <= nPhotosPages; i++ { + photosURL := fmt.Sprintf("%s?extras=", apiURL) + + url.QueryEscape("description,date_upload,date_taken,original_format,last_update,geo,tags,machine_tags,views,media,url_o") + + fmt.Sprintf("&format=json&method=%s&nojsoncallback=1&page=%d&user_id=%s", photosAPIPath, i, fakeUserId) + response := fmt.Sprintf("%s%s", okHeader, fakePhotosPage(i, nPhotosPages, perPage, photoIds)) + responses[photosURL] = httputil.StaticResponder(response) + } + + // Actual photo(s) URL. + pudgyPic := fakePicture() + for _, v := range photoIds { + photoURL := fmt.Sprintf("https://farm3.staticflickr.com/2897/14198397111_%s_o.jpg?user_id=%s", v, fakeUserId) + responses[photoURL] = httputil.FileResponder(pudgyPic) + } + + return httputil.NewFakeTransport(responses) +} + +func fakePhotosetsList(sets int) string { + var photosets []*photosetInfo + for i := 1; i <= sets; i++ { + title := fmt.Sprintf("Photoset %d", i) + photosetId := blob.RefFromString(title).DigestPrefix(10) + primaryPhotoId := blob.RefFromString(fmt.Sprintf("Photo 1 on page 1 of photoset %s", photosetId)).DigestPrefix(10) + item := &photosetInfo{ + Id: photosetId, + PrimaryPhotoId: primaryPhotoId, + Title: contentString{Content: title}, + Description: contentString{Content: "fakePhotosetDescription"}, + } + photosets = append(photosets, item) + } + + setslist := struct { + Photosets photosetList + }{ + Photosets: photosetList{ + Photoset: photosets, + }, + } + + list, err := json.MarshalIndent(&setslist, "", " ") + if err != nil { + log.Fatalf("%v", err) + } + return string(list) +} + +func fakePhotoset(photosetId string, page, pages int, photoIds []string) string { + var photos []struct { + Id string + OriginalFormat string + } + for _, v := range photoIds { + item := struct { + Id string + OriginalFormat string + }{ + Id: v, + OriginalFormat: "jpg", + } + photos = append(photos, item) + } + + photoslist := struct { + Photoset photosetItems + }{ + Photoset: photosetItems{ + Id: photosetId, + Page: jsonInt(page), + Pages: jsonInt(pages), + Photo: photos, + }, + } + + list, err := json.MarshalIndent(&photoslist, "", " ") + if err != nil { + log.Fatalf("%v", err) + } + return string(list) + +} + +func fakePhotosPage(page, pages, perPage int, photoIds []string) string { + var photos []*photosSearchItem + currentPage := 1 + indexOnPage := 1 + day := time.Hour * 24 + year := day * 365 + const dateCreatedFormat = "2006-01-02 15:04:05" + + for k, v := range photoIds { + if indexOnPage > perPage { + currentPage++ + indexOnPage = 1 + } + if currentPage < page { + indexOnPage++ + continue + } + created := time.Now().Add(-time.Duration(k) * year) + published := created.Add(day) + updated := published.Add(day) + item := &photosSearchItem{ + Id: v, + Title: fmt.Sprintf("Photo %d", k+1), + Description: contentString{Content: "fakePhotoDescription"}, + DateUpload: fmt.Sprintf("%d", published.Unix()), + DateTaken: created.Format(dateCreatedFormat), + LastUpdate: fmt.Sprintf("%d", updated.Unix()), + URL: fmt.Sprintf("https://farm3.staticflickr.com/2897/14198397111_%s_o.jpg", v), + OriginalFormat: "jpg", + } + photos = append(photos, item) + if len(photos) >= perPage { + break + } + indexOnPage++ + } + + photosPage := &photosSearch{ + Photos: struct { + Page jsonInt + Pages jsonInt + Perpage jsonInt + Total jsonInt + Photo []*photosSearchItem + }{ + Page: jsonInt(page), + Pages: jsonInt(pages), + Perpage: jsonInt(perPage), + Photo: photos, + }, + } + + list, err := json.MarshalIndent(photosPage, "", " ") + if err != nil { + log.Fatalf("%v", err) + } + return string(list) + +} + +func fakePicture() string { + camliDir, err := osutil.GoPackagePath("camlistore.org") + if err == os.ErrNotExist { + log.Fatal("Directory \"camlistore.org\" not found under GOPATH/src; are you not running with devcam?") + } + if err != nil { + log.Fatalf("Error searching for \"camlistore.org\" under GOPATH: %v", err) + } + return filepath.Join(camliDir, filepath.FromSlash("third_party/glitch/npc_piggy__x1_walk_png_1354829432.png")) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/README b/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/README new file mode 100644 index 00000000..73ff3092 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/README @@ -0,0 +1,19 @@ +Foursquare Importer +=================== + +This is an incomplete Camlistore importer for Foursquare. + +To use: + +1) Visit https://foursquare.com/developers/apps and "Create a new app" + to get a Foursquare Client ID and secret. +2) Start the devcam server with foursquareapikey flag: + $ devcam server -foursquareapikey=: +3) Navigate to http:///importer-foursquare/login +4) Watch import progress on the command line + + +TODO: + +https://github.com/camlistore/camlistore/issues?q=is%3Aopen+is%3Aissue+foursquare + diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/api.go b/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/api.go new file mode 100644 index 00000000..e3048174 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/api.go @@ -0,0 +1,108 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Types for Foursquare's JSON API. + +package foursquare + +type user struct { + Id string + FirstName string + LastName string +} + +type userInfo struct { + Response struct { + User user + } +} + +type checkinsList struct { + Response struct { + Checkins struct { + Items []*checkinItem + } + } +} + +type checkinItem struct { + Id string + CreatedAt int64 // unix time in seconds from 4sq + TimeZoneOffset int // offset in minutes. positive is east. + Shout string // "Message from check-in, if present and visible to the acting user." + Venue venueItem +} + +type venueItem struct { + Id string // eg 42474900f964a52087201fe3 from 4sq + Name string + Location *venueLocationItem + Categories []*venueCategory +} + +type photosList struct { + Response struct { + Photos struct { + Items []*photoItem + } + } +} + +type photoItem struct { + Id string + Prefix string + Suffix string + Width int + Height int +} + +func (vi *venueItem) primaryCategory() *venueCategory { + for _, c := range vi.Categories { + if c.Primary { + return c + } + } + return nil +} + +func (vi *venueItem) icon() string { + c := vi.primaryCategory() + if c == nil || c.Icon == nil || c.Icon.Prefix == "" { + return "" + } + return c.Icon.Prefix + "bg_88" + c.Icon.Suffix +} + +type venueLocationItem struct { + Address string + City string + PostalCode string + State string + Country string // 4sq provides "US" + Lat float64 + Lng float64 +} + +type venueCategory struct { + Primary bool + Name string + Icon *categoryIcon +} + +type categoryIcon struct { + Prefix string + Suffix string +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/foursquare.go b/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/foursquare.go new file mode 100644 index 00000000..e8f2ed75 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/foursquare.go @@ -0,0 +1,520 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package foursquare implements an importer for foursquare.com accounts. +package foursquare + +import ( + "fmt" + "log" + "net/http" + "net/url" + "path" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/schema/nodeattr" + "camlistore.org/third_party/code.google.com/p/goauth2/oauth" +) + +const ( + apiURL = "https://api.foursquare.com/v2/" + authURL = "https://foursquare.com/oauth2/authenticate" + tokenURL = "https://foursquare.com/oauth2/access_token" + + apiVersion = "20140225" + checkinsAPIPath = "users/self/checkins" + + // runCompleteVersion is a cache-busting version number of the + // importer code. It should be incremented whenever the + // behavior of this importer is updated enough to warrant a + // complete run. Otherwise, if the importer runs to + // completion, this version number is recorded on the account + // permanode and subsequent importers can stop early. + runCompleteVersion = "1" + + // Permanode attributes on account node: + acctAttrUserId = "foursquareUserId" + acctAttrUserFirst = "foursquareFirstName" + acctAttrUserLast = "foursquareLastName" + acctAttrAccessToken = "oauthAccessToken" + + checkinsRequestLimit = 100 // max number of checkins we will ask for in a checkins list request + photosRequestLimit = 5 + + attrFoursquareId = "foursquareId" + attrFoursquareVenuePermanode = "foursquareVenuePermanode" + attrFoursquareCategoryName = "foursquareCategoryName" +) + +func init() { + importer.Register("foursquare", &imp{ + imageFileRef: make(map[string]blob.Ref), + }) +} + +var _ importer.ImporterSetupHTMLer = (*imp)(nil) + +type imp struct { + mu sync.Mutex // guards following + imageFileRef map[string]blob.Ref // url to file schema blob + + importer.OAuth2 // for CallbackRequestAccount and CallbackURLParameters +} + +func (im *imp) NeedsAPIKey() bool { return true } +func (im *imp) SupportsIncremental() bool { return true } + +func (im *imp) IsAccountReady(acctNode *importer.Object) (ok bool, err error) { + if acctNode.Attr(acctAttrUserId) != "" && acctNode.Attr(acctAttrAccessToken) != "" { + return true, nil + } + return false, nil +} + +func (im *imp) SummarizeAccount(acct *importer.Object) string { + ok, err := im.IsAccountReady(acct) + if err != nil { + return "Not configured; error = " + err.Error() + } + if !ok { + return "Not configured" + } + if acct.Attr(acctAttrUserFirst) == "" && acct.Attr(acctAttrUserLast) == "" { + return fmt.Sprintf("userid %s", acct.Attr(acctAttrUserId)) + } + return fmt.Sprintf("userid %s (%s %s)", acct.Attr(acctAttrUserId), + acct.Attr(acctAttrUserFirst), acct.Attr(acctAttrUserLast)) +} + +func (im *imp) AccountSetupHTML(host *importer.Host) string { + base := host.ImporterBaseURL() + "foursquare" + return fmt.Sprintf(` +

    Configuring Foursquare

    +

    Visit https://foursquare.com/developers/apps and click "Create a new app".

    +

    Use the following settings:

    +
      +
    • Download / welcome page url: %s
    • +
    • Your privacy policy url: %s
    • +
    • Redirect URI(s): %s
    • +
    +

    Click "SAVE CHANGES". Copy the "Client ID" and "Client Secret" into the boxes above.

    +`, base, base+"/privacy", base+"/callback") +} + +// A run is our state for a given run of the importer. +type run struct { + *importer.RunContext + im *imp + incremental bool // whether we've completed a run in the past + + mu sync.Mutex // guards anyErr + anyErr bool +} + +func (r *run) token() string { + return r.RunContext.AccountNode().Attr(acctAttrAccessToken) +} + +func (im *imp) Run(ctx *importer.RunContext) error { + r := &run{ + RunContext: ctx, + im: im, + incremental: ctx.AccountNode().Attr(importer.AcctAttrCompletedVersion) == runCompleteVersion, + } + + if err := r.importCheckins(); err != nil { + return err + } + + r.mu.Lock() + anyErr := r.anyErr + r.mu.Unlock() + + if !anyErr { + if err := r.AccountNode().SetAttrs(importer.AcctAttrCompletedVersion, runCompleteVersion); err != nil { + return err + } + } + + return nil +} + +func (r *run) errorf(format string, args ...interface{}) { + log.Printf(format, args...) + r.mu.Lock() + defer r.mu.Unlock() + r.anyErr = true +} + +// urlFileRef slurps urlstr from the net, writes to a file and returns its +// fileref or "" on error +func (r *run) urlFileRef(urlstr, filename string) string { + im := r.im + im.mu.Lock() + if br, ok := im.imageFileRef[urlstr]; ok { + im.mu.Unlock() + return br.String() + } + im.mu.Unlock() + + res, err := r.HTTPClient().Get(urlstr) + if err != nil { + log.Printf("couldn't get image: %v", err) + return "" + } + defer res.Body.Close() + + fileRef, err := schema.WriteFileFromReader(r.Host.Target(), filename, res.Body) + if err != nil { + r.errorf("couldn't write file: %v", err) + return "" + } + + im.mu.Lock() + defer im.mu.Unlock() + im.imageFileRef[urlstr] = fileRef + return fileRef.String() +} + +type byCreatedAt []*checkinItem + +func (s byCreatedAt) Less(i, j int) bool { return s[i].CreatedAt < s[j].CreatedAt } +func (s byCreatedAt) Len() int { return len(s) } +func (s byCreatedAt) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func (r *run) importCheckins() error { + limit := checkinsRequestLimit + offset := 0 + continueRequests := true + + for continueRequests { + resp := checkinsList{} + if err := r.im.doAPI(r.Context, r.token(), &resp, checkinsAPIPath, "limit", strconv.Itoa(limit), "offset", strconv.Itoa(offset)); err != nil { + return err + } + + itemcount := len(resp.Response.Checkins.Items) + log.Printf("foursquare: importing %d checkins (offset %d)", itemcount, offset) + if itemcount < limit { + continueRequests = false + } else { + offset += itemcount + } + + checkinsNode, err := r.getTopLevelNode("checkins", "Checkins") + if err != nil { + return err + } + + placesNode, err := r.getTopLevelNode("places", "Places") + if err != nil { + return err + } + + sort.Sort(byCreatedAt(resp.Response.Checkins.Items)) + sawOldItem := false + for _, checkin := range resp.Response.Checkins.Items { + placeNode, err := r.importPlace(placesNode, &checkin.Venue) + if err != nil { + r.errorf("Foursquare importer: error importing place %s %v", checkin.Venue.Id, err) + continue + } + + _, dup, err := r.importCheckin(checkinsNode, checkin, placeNode.PermanodeRef()) + if err != nil { + r.errorf("Foursquare importer: error importing checkin %s %v", checkin.Id, err) + continue + } + + if dup { + sawOldItem = true + } + + err = r.importPhotos(placeNode, dup) + if err != nil { + r.errorf("Foursquare importer: error importing photos for checkin %s %v", checkin.Id, err) + continue + } + } + if sawOldItem && r.incremental { + break + } + } + + return nil +} + +func (r *run) importPhotos(placeNode *importer.Object, checkinWasDup bool) error { + photosNode, err := placeNode.ChildPathObject("photos") + if err != nil { + return err + } + + if err := photosNode.SetAttrs( + nodeattr.Title, "Photos of "+placeNode.Attr("title"), + nodeattr.DefaultVisibility, "hide"); err != nil { + return err + } + + nHave := 0 + photosNode.ForeachAttr(func(key, value string) { + if strings.HasPrefix(key, "camliPath:") { + nHave++ + } + }) + nWant := photosRequestLimit + if checkinWasDup { + nWant = 1 + } + if nHave >= nWant { + return nil + } + + resp := photosList{} + if err := r.im.doAPI(r.Context, r.token(), &resp, + "venues/"+placeNode.Attr(attrFoursquareId)+"/photos", + "limit", strconv.Itoa(nWant)); err != nil { + return err + } + + var need []*photoItem + for _, photo := range resp.Response.Photos.Items { + attr := "camliPath:" + photo.Id + filepath.Ext(photo.Suffix) + if photosNode.Attr(attr) == "" { + need = append(need, photo) + } + } + + if len(need) > 0 { + venueTitle := placeNode.Attr(nodeattr.Title) + log.Printf("foursquare: importing %d photos for venue %s", len(need), venueTitle) + for _, photo := range need { + attr := "camliPath:" + photo.Id + filepath.Ext(photo.Suffix) + if photosNode.Attr(attr) != "" { + continue + } + url := photo.Prefix + "original" + photo.Suffix + log.Printf("foursquare: importing photo for venue %s: %s", venueTitle, url) + ref := r.urlFileRef(url, "") + if ref == "" { + r.errorf("Error slurping photo: %s", url) + continue + } + if err := photosNode.SetAttr(attr, ref); err != nil { + r.errorf("Error adding venue photo: %#v", err) + } + } + } + + return nil +} + +func (r *run) importCheckin(parent *importer.Object, checkin *checkinItem, placeRef blob.Ref) (checkinNode *importer.Object, dup bool, err error) { + checkinNode, err = parent.ChildPathObject(checkin.Id) + if err != nil { + return + } + + title := fmt.Sprintf("Checkin at %s", checkin.Venue.Name) + dup = checkinNode.Attr(nodeattr.StartDate) != "" + if err := checkinNode.SetAttrs( + attrFoursquareId, checkin.Id, + attrFoursquareVenuePermanode, placeRef.String(), + nodeattr.Type, "foursquare.com:checkin", + nodeattr.StartDate, schema.RFC3339FromTime(time.Unix(checkin.CreatedAt, 0)), + nodeattr.Title, title); err != nil { + return nil, false, err + } + return checkinNode, dup, nil +} + +func (r *run) importPlace(parent *importer.Object, place *venueItem) (*importer.Object, error) { + placeNode, err := parent.ChildPathObject(place.Id) + if err != nil { + return nil, err + } + + catName := "" + if cat := place.primaryCategory(); cat != nil { + catName = cat.Name + } + + icon := place.icon() + if err := placeNode.SetAttrs( + attrFoursquareId, place.Id, + nodeattr.Type, "foursquare.com:venue", + nodeattr.CamliContentImage, r.urlFileRef(icon, path.Base(icon)), + attrFoursquareCategoryName, catName, + nodeattr.Title, place.Name, + nodeattr.StreetAddress, place.Location.Address, + nodeattr.AddressLocality, place.Location.City, + nodeattr.PostalCode, place.Location.PostalCode, + nodeattr.AddressRegion, place.Location.State, + nodeattr.AddressCountry, place.Location.Country, + nodeattr.Latitude, fmt.Sprint(place.Location.Lat), + nodeattr.Longitude, fmt.Sprint(place.Location.Lng)); err != nil { + return nil, err + } + + return placeNode, nil +} + +func (r *run) getTopLevelNode(path string, title string) (*importer.Object, error) { + childObject, err := r.RootNode().ChildPathObject(path) + if err != nil { + return nil, err + } + + if err := childObject.SetAttr(nodeattr.Title, title); err != nil { + return nil, err + } + return childObject, nil +} + +func (im *imp) getUserInfo(ctx *context.Context, accessToken string) (user, error) { + var ui userInfo + if err := im.doAPI(ctx, accessToken, &ui, "users/self"); err != nil { + return user{}, err + } + if ui.Response.User.Id == "" { + return user{}, fmt.Errorf("No userid returned") + } + return ui.Response.User, nil +} + +func (im *imp) doAPI(ctx *context.Context, accessToken string, result interface{}, apiPath string, keyval ...string) error { + if len(keyval)%2 == 1 { + panic("Incorrect number of keyval arguments") + } + + form := url.Values{} + form.Set("v", apiVersion) // 4sq requires this to version their API + form.Set("oauth_token", accessToken) + for i := 0; i < len(keyval); i += 2 { + form.Set(keyval[i], keyval[i+1]) + } + + fullURL := apiURL + apiPath + res, err := doGet(ctx, fullURL, form) + if err != nil { + return err + } + err = httputil.DecodeJSON(res, result) + if err != nil { + log.Printf("Error parsing response for %s: %v", fullURL, err) + } + return err +} + +func doGet(ctx *context.Context, url string, form url.Values) (*http.Response, error) { + requestURL := url + "?" + form.Encode() + req, err := http.NewRequest("GET", requestURL, nil) + if err != nil { + return nil, err + } + res, err := ctx.HTTPClient().Do(req) + if err != nil { + log.Printf("Error fetching %s: %v", url, err) + return nil, err + } + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Get request on %s failed with: %s", requestURL, res.Status) + } + return res, nil +} + +// auth returns a new oauth.Config +func auth(ctx *importer.SetupContext) (*oauth.Config, error) { + clientId, secret, err := ctx.Credentials() + if err != nil { + return nil, err + } + return &oauth.Config{ + ClientId: clientId, + ClientSecret: secret, + AuthURL: authURL, + TokenURL: tokenURL, + RedirectURL: ctx.CallbackURL(), + }, nil +} + +func (im *imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error { + oauthConfig, err := auth(ctx) + if err != nil { + return err + } + oauthConfig.RedirectURL = im.RedirectURL(im, ctx) + state, err := im.RedirectState(im, ctx) + if err != nil { + return err + } + http.Redirect(w, r, oauthConfig.AuthCodeURL(state), http.StatusFound) + return nil +} + +func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) { + oauthConfig, err := auth(ctx) + if err != nil { + httputil.ServeError(w, r, fmt.Errorf("Error getting oauth config: %v", err)) + return + } + + if r.Method != "GET" { + http.Error(w, "Expected a GET", 400) + return + } + code := r.FormValue("code") + if code == "" { + http.Error(w, "Expected a code", 400) + return + } + transport := &oauth.Transport{Config: oauthConfig} + token, err := transport.Exchange(code) + log.Printf("Token = %#v, error %v", token, err) + if err != nil { + log.Printf("Token Exchange error: %v", err) + http.Error(w, "token exchange error", 500) + return + } + + u, err := im.getUserInfo(ctx.Context, token.AccessToken) + if err != nil { + log.Printf("Couldn't get username: %v", err) + http.Error(w, "can't get username", 500) + return + } + if err := ctx.AccountNode.SetAttrs( + acctAttrUserId, u.Id, + acctAttrUserFirst, u.FirstName, + acctAttrUserLast, u.LastName, + acctAttrAccessToken, token.AccessToken, + ); err != nil { + httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err)) + return + } + http.Redirect(w, r, ctx.AccountURL(), http.StatusFound) + +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/foursquare_test.go b/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/foursquare_test.go new file mode 100644 index 00000000..9a55ced8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/foursquare_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package foursquare + +import ( + "net/http" + "testing" + + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" +) + +func TestGetUserId(t *testing.T) { + im := &imp{} + ctx := context.New(context.WithHTTPClient(&http.Client{ + Transport: httputil.NewFakeTransport(map[string]func() *http.Response{ + "https://api.foursquare.com/v2/users/self?oauth_token=footoken&v=20140225": httputil.FileResponder("testdata/users-me-res.json"), + }), + })) + defer ctx.Cancel() + inf, err := im.getUserInfo(ctx, "footoken") + if err != nil { + t.Fatal(err) + } + want := user{ + Id: "13674", + FirstName: "Brad", + LastName: "Fitzpatrick", + } + if inf != want { + t.Errorf("user info = %+v; want %+v", inf, want) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/testdata.go b/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/testdata.go new file mode 100644 index 00000000..5477ddbc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/testdata.go @@ -0,0 +1,269 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package foursquare + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + "camlistore.org/pkg/osutil" +) + +var _ importer.TestDataMaker = (*imp)(nil) + +func (im *imp) SetTestAccount(acctNode *importer.Object) error { + // TODO(mpl): refactor with twitter + return acctNode.SetAttrs( + importer.AcctAttrAccessToken, "fakeAccessToken", + importer.AcctAttrAccessTokenSecret, "fakeAccessSecret", + importer.AcctAttrUserID, "fakeUserID", + importer.AcctAttrName, "fakeName", + importer.AcctAttrUserName, "fakeScreenName", + ) +} + +func (im *imp) MakeTestData() http.RoundTripper { + + const nCheckins = 150 // Arbitrary number of checkins generated. + + // if you add another venue, make sure the venueCounter reset + // in fakeCheckinsList allows for that case to happen. + // We could use global vars instead, but don't want to pollute the + // fousquare pkg namespace. + towns := map[int]*venueLocationItem{ + 0: { + Address: "Baker street", + City: "Dublin", + PostalCode: "0", + State: "none", + Country: "Ireland", + Lat: 53.4053427, + Lng: -8.3320801, + }, + 1: { + Address: "Fish&Ships street", + City: "London", + PostalCode: "1", + State: "none", + Country: "England", + Lat: 55.3617609, + Lng: -3.4433238, + }, + 2: { + Address: "Haggis street", + City: "Glasgow", + PostalCode: "2", + State: "none", + Country: "Scotland", + Lat: 57.7394571, + Lng: -4.686997, + }, + 3: { + Address: "rue du croissant", + City: "Grenoble", + PostalCode: "38000", + State: "none", + Country: "France", + Lat: 45.1841655, + Lng: 5.7155424, + }, + 4: { + Address: "burrito street", + City: "San Francisco", + PostalCode: "94114", + State: "CA", + Country: "US", + Lat: 37.7593625, + Lng: -122.4266995, + }, + } + + // We need to compute the venueIds in advance, because the venue id is used as a parameter + // in some of the requests we need to register. + var venueIds []string + for _, v := range towns { + venueIds = append(venueIds, blob.RefFromString(v.City).DigestPrefix(10)) + } + + checkinsURL := apiURL + checkinsAPIPath + checkinsListCached := make(map[int]string) + okHeader := `HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 + +` + + responses := make(map[string]func() *http.Response) + + // register all the checkins calls; offset varies. + for i := 0; i < nCheckins; i += checkinsRequestLimit { + url := fmt.Sprintf("%s?limit=%d&oauth_token=fakeAccessToken&offset=%d&v=%s", + checkinsURL, checkinsRequestLimit, i, apiVersion) + response := okHeader + fakeCheckinsList(i, nCheckins, towns, checkinsListCached) + responses[url] = httputil.StaticResponder(response) + } + + // register all the venue photos calls (venueId varies) + photosURL := apiURL + "venues" + photosResponse := okHeader + fakePhotosList() + for _, id := range venueIds { + url := fmt.Sprintf("%s/%s/photos?limit=%d&oauth_token=fakeAccessToken&v=%s", + photosURL, id, photosRequestLimit, apiVersion) + responses[url] = httputil.StaticResponder(photosResponse) + } + + // register the photoitem calls + pudgyPic := fakePhoto() + photoURL := "https://camlistore.org/pic/pudgy.png" + originalPhotoURL := "https://camlistore.org/original/pic/pudgy.png" + iconURL := "https://camlistore.org/bg_88/pic/pudgy.png" + responses[photoURL] = httputil.FileResponder(pudgyPic) + responses[originalPhotoURL] = httputil.FileResponder(pudgyPic) + responses[iconURL] = httputil.FileResponder(pudgyPic) + + return httputil.NewFakeTransport(responses) +} + +// fakeCheckinsList returns a JSON checkins list of checkinsRequestLimit checkin +// items, starting at offset. It stops before checkinsRequestLimit if maxCheckin is +// reached. It uses towns to populate the venues. The returned list is saved in +// cached. +func fakeCheckinsList(offset, maxCheckin int, towns map[int]*venueLocationItem, cached map[int]string) string { + if cl, ok := cached[offset]; ok { + return cl + } + max := offset + checkinsRequestLimit + if max > maxCheckin { + max = maxCheckin + } + var items []*checkinItem + tzCounter := 0 + venueCounter := 0 + for i := offset; i < max; i++ { + shout := fmt.Sprintf("fakeShout %d", i) + item := &checkinItem{ + Id: blob.RefFromString(shout).DigestPrefix(10), + Shout: shout, + CreatedAt: time.Now().Unix(), + TimeZoneOffset: tzCounter * 60, + Venue: fakeVenue(venueCounter, towns), + } + items = append(items, item) + tzCounter++ + venueCounter++ + if tzCounter == 24 { + tzCounter = 0 + } + if venueCounter == 5 { + venueCounter = 0 + } + } + + response := struct { + Checkins struct { + Items []*checkinItem + } + }{ + Checkins: struct { + Items []*checkinItem + }{ + Items: items, + }, + } + list, err := json.MarshalIndent(checkinsList{Response: response}, "", " ") + if err != nil { + log.Fatalf("%v", err) + } + cached[offset] = string(list) + return cached[offset] +} + +func fakeVenue(counter int, towns map[int]*venueLocationItem) venueItem { + prefix := "https://camlistore.org/" + suffix := "/pic/pudgy.png" + // TODO: add more. + categories := []*venueCategory{ + { + Primary: true, + Name: "town", + Icon: &categoryIcon{ + Prefix: prefix, + Suffix: suffix, + }, + }, + } + + return venueItem{ + Id: blob.RefFromString(towns[counter].City).DigestPrefix(10), + Name: towns[counter].City, + Location: towns[counter], + Categories: categories, + } +} + +func fakePhotosList() string { + items := []*photoItem{ + fakePhotoItem(), + } + response := struct { + Photos struct { + Items []*photoItem + } + }{ + Photos: struct { + Items []*photoItem + }{ + Items: items, + }, + } + list, err := json.MarshalIndent(photosList{Response: response}, "", " ") + if err != nil { + log.Fatalf("%v", err) + } + return string(list) +} + +func fakePhotoItem() *photoItem { + prefix := "https://camlistore.org/" + suffix := "/pic/pudgy.png" + return &photoItem{ + Id: blob.RefFromString(prefix + suffix).DigestPrefix(10), + Prefix: prefix, + Suffix: suffix, + Width: 704, + Height: 186, + } +} + +// TODO(mpl): refactor with twitter +func fakePhoto() string { + camliDir, err := osutil.GoPackagePath("camlistore.org") + if err == os.ErrNotExist { + log.Fatal("Directory \"camlistore.org\" not found under GOPATH/src; are you not running with devcam?") + } + if err != nil { + log.Fatalf("Error searching for \"camlistore.org\" under GOPATH: %v", err) + } + return filepath.Join(camliDir, filepath.FromSlash("third_party/glitch/npc_piggy__x1_walk_png_1354829432.png")) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/testdata/users-me-res.json b/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/testdata/users-me-res.json new file mode 100644 index 00000000..44a51312 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/foursquare/testdata/users-me-res.json @@ -0,0 +1,791 @@ +{ + "meta": { + "code": 200 + }, + "notifications": [ + { + "type": "notificationTray", + "item": { + "unreadCount": 4 + } + } + ], + "response": { + "user": { + "id": "13674", + "firstName": "Brad", + "lastName": "Fitzpatrick", + "gender": "male", + "relationship": "self", + "photo": { + "prefix": "https:\/\/irs0.4sqi.net\/img\/user\/", + "suffix": "\/CKG5FOF2WMCMPD3E.jpg" + }, + "friends": { + "count": 174, + "groups": [ + { + "type": "friends", + "name": "Mutual friends", + "count": 0, + "items": [] + }, + { + "type": "others", + "name": "Other friends", + "count": 174, + "items": [ + { + "id": "83878", + "firstName": "Randal", + "lastName": "Schwartz", + "gender": "male", + "relationship": "friend", + "photo": { + "prefix": "https:\/\/irs2.4sqi.net\/img\/user\/", + "suffix": "\/41NCM3VIMA30PNZ3.jpg" + }, + "tips": { + "count": 28 + }, + "lists": { + "groups": [ + { + "type": "created", + "count": 4, + "items": [] + } + ] + }, + "homeCity": "Beaverton, OR", + "bio": "Yeah, \u2022that\u2022 Randal Schwartz. I'm also a low-carb high-fat (ketogenic) consumer, so if you have location advice around that, let me know!", + "contact": { + "phone": "", + "email": "merlyn.foursquare@stonehenge.com", + "twitter": "merlyn", + "facebook": "504874371" + } + }, + { + "id": "38677952", + "firstName": "Nori", + "lastName": "Heikkinen", + "gender": "female", + "relationship": "friend", + "photo": { + "prefix": "https:\/\/irs0.4sqi.net\/img\/user\/", + "suffix": "\/VDOVTY0YUYUJ10QK.jpg" + }, + "tips": { + "count": 4 + }, + "lists": { + "groups": [ + { + "type": "created", + "count": 5, + "items": [] + } + ] + }, + "homeCity": "Baltimore, MD", + "bio": "", + "contact": { + "email": "nori.heikkinen@gmail.com", + "twitter": "n0r1", + "facebook": "677985398" + } + }, + { + "id": "64376555", + "firstName": "Miguel", + "lastName": "de Icaza", + "gender": "male", + "relationship": "friend", + "photo": { + "prefix": "https:\/\/irs0.4sqi.net\/img\/user\/", + "suffix": "\/LP510B3INEKRTNI2.jpg" + }, + "tips": { + "count": 2 + }, + "lists": { + "groups": [ + { + "type": "created", + "count": 1, + "items": [] + } + ] + }, + "homeCity": "Boston, MA", + "bio": "", + "contact": { + "email": "miguel.de.icaza@gmail.com", + "facebook": "532065026" + } + }, + { + "id": "50291", + "firstName": "Nat", + "lastName": "Friedman", + "gender": "male", + "relationship": "friend", + "photo": { + "prefix": "https:\/\/irs3.4sqi.net\/img\/user\/", + "suffix": "\/50291_1259165803611.jpg" + }, + "tips": { + "count": 12 + }, + "lists": { + "groups": [ + { + "type": "created", + "count": 2, + "items": [] + } + ] + }, + "homeCity": "San Francisco", + "bio": "", + "contact": { + "phone": "", + "email": "nat@nat.org", + "twitter": "natfriedman", + "facebook": "547946582" + } + }, + { + "id": "618", + "firstName": "Chris", + "lastName": "Messina", + "gender": "male", + "relationship": "friend", + "photo": { + "prefix": "https:\/\/irs0.4sqi.net\/img\/user\/", + "suffix": "\/-31MXPPWS4MBOET0B.jpg" + }, + "tips": { + "count": 389 + }, + "lists": { + "groups": [ + { + "type": "created", + "count": 17, + "items": [] + } + ] + }, + "homeCity": "San Francisco, CA", + "bio": "Bachelor of Arts.\r\n#godfather (http:\/\/nyti.ms\/lJ6Kdj)\r\nI am not the actor.", + "contact": { + "phone": "", + "email": "chris.messina@gmail.com", + "twitter": "chrismessina", + "facebook": "502411873" + }, + "superuser": 2 + }, + { + "id": "21279572", + "firstName": "Barry", + "lastName": "Abrahamson", + "gender": "male", + "relationship": "friend", + "photo": { + "prefix": "https:\/\/irs2.4sqi.net\/img\/user\/", + "suffix": "\/MAV25J2AQ3IORLLM.jpg" + }, + "tips": { + "count": 2 + }, + "lists": { + "groups": [ + { + "type": "created", + "count": 2, + "items": [] + } + ] + }, + "homeCity": "Houston, Texas", + "bio": "", + "contact": { + "email": "barry@yourang.org", + "twitter": "bazza", + "facebook": "732341470" + } + }, + { + "id": "76974963", + "firstName": "Tiffany", + "lastName": "Precissi", + "gender": "female", + "relationship": "friend", + "photo": { + "prefix": "https:\/\/irs2.4sqi.net\/img\/user\/", + "suffix": "\/blank_girl.png" + }, + "tips": { + "count": 0 + }, + "lists": { + "groups": [ + { + "type": "created", + "count": 1, + "items": [] + } + ] + }, + "homeCity": "Stockton, CA", + "bio": "", + "contact": { + "email": "tifflynn@gmail.com", + "twitter": "xo_tiff4ny", + "facebook": "665546095" + } + }, + { + "id": "4478390", + "firstName": "Julie", + "lastName": "Parent", + "gender": "female", + "relationship": "friend", + "photo": { + "prefix": "https:\/\/irs2.4sqi.net\/img\/user\/", + "suffix": "\/blank_girl.png" + }, + "tips": { + "count": 0 + }, + "lists": { + "groups": [ + { + "type": "created", + "count": 1, + "items": [] + } + ] + }, + "homeCity": "San Francisco, CA", + "bio": "", + "contact": { + "email": "jparent@gmail.com", + "twitter": "jewree", + "facebook": "1513396" + } + }, + { + "id": "68959715", + "firstName": "Ekaterina", + "lastName": "Ustinova", + "gender": "female", + "relationship": "friend", + "photo": { + "prefix": "https:\/\/irs2.4sqi.net\/img\/user\/", + "suffix": "\/blank_girl.png" + }, + "tips": { + "count": 0 + }, + "lists": { + "groups": [ + { + "type": "created", + "count": 1, + "items": [] + } + ] + }, + "homeCity": "New York, NY", + "bio": "", + "contact": { + "email": "ustinoid@gmail.com", + "facebook": "100000011060984" + } + }, + { + "id": "67841916", + "firstName": "Katherine", + "lastName": "Deyo", + "gender": "female", + "relationship": "friend", + "photo": { + "prefix": "https:\/\/irs2.4sqi.net\/img\/user\/", + "suffix": "\/blank_girl.png" + }, + "tips": { + "count": 0 + }, + "lists": { + "groups": [ + { + "type": "created", + "count": 1, + "items": [] + } + ] + }, + "homeCity": "San Francisco, CA", + "bio": "I wanna be myself", + "contact": { + "email": "katherine_w_deyo@gmail.com" + } + } + ] + } + ] + }, + "tips": { + "count": 18 + }, + "homeCity": "San Francisco, CA", + "bio": "", + "contact": { + "phone": "5035551212", + "email": "brad@danga.com", + "twitter": "bradfitz", + "facebook": "500033387" + }, + "superuser": 1, + "checkinPings": "off", + "pings": false, + "type": "user", + "badges": { + "count": 65, + "items": [ + { + "id": "518577cd498ebaa83dc8f7e0", + "badgeId": "4ebb078f7bebd6a83f1176bd", + "name": "Hot Tamale", + "unlockMessage": "You unlocked the Hot Tamale badge!", + "description": "Rice, beans, cheese, cilantro \u2013 why eat anything else when you can get all the important food groups wrapped into one delicious pound of foil? Now pass those nachos, will ya? It\u2019s time to guac and roll.\n\nThat's 45 different Mexican restaurants! Your taste buds must be scorched. Don't worry, some tequila shots should probably fix that. Congrats on Level 10 Hot Tamale status!", + "level": 10, + "badgeText": "Rice, beans, cheese, cilantro \u2013 why eat anything else when you can get all the important food groups wrapped into one delicious pound of foil? Now pass those nachos, will ya? It\u2019s time to guac and roll.", + "levelText": "That's 45 different Mexican restaurants! Your taste buds must be scorched. Don't worry, some tequila shots should probably fix that. Congrats on Level 10 Hot Tamale status!", + "categorySummary": "Mexican Restaurants", + "image": { + "prefix": "https:\/\/playfoursquare.s3.amazonaws.com\/badge\/", + "sizes": [ + 57, + 114, + 200, + 300, + 400 + ], + "name": "\/L2RRMCA2PGOBSFRN_10.png" + }, + "unlocks": [ + { + "checkins": [ + { + "id": "518577cc498ebaa83dc8f148", + "createdAt": 1367701452, + "type": "checkin", + "shout": "Van exchange point for The Relay. Not actually going to church.", + "timeZoneOffset": -420, + "venue": { + "id": "4bdc6861c79cc9285e6586e9", + "name": "Crosswalk Community Church", + "contact": {}, + "location": { + "lat": 38.30073598016023, + "lng": -122.30450377008302, + "postalCode": "94558", + "cc": "US", + "country": "United States" + }, + "categories": [ + { + "id": "4bf58dd8d48988d1c1941735", + "name": "Mexican Restaurant", + "pluralName": "Mexican Restaurants", + "shortName": "Mexican", + "icon": { + "prefix": "https:\/\/ss1.4sqi.net\/img\/categories_v2\/food\/mexican_", + "suffix": ".png" + }, + "primary": true + } + ], + "verified": false, + "stats": { + "checkinsCount": 248, + "usersCount": 90, + "tipCount": 4 + } + }, + "photos": { + "count": 0, + "items": [] + }, + "posts": { + "count": 0, + "textCount": 0 + }, + "comments": { + "count": 1 + }, + "source": { + "name": "foursquare for Android", + "url": "https:\/\/foursquare.com\/download\/#\/android" + } + } + ] + } + ] + } + ] + }, + "mayorships": { + "count": 4, + "items": [] + }, + "checkins": { + "count": 3272, + "items": [ + { + "id": "53396b10498e2c3aed309903", + "createdAt": 1396271888, + "type": "checkin", + "shout": "SFO-PDX", + "timeZoneOffset": -420, + "venue": { + "id": "4a7601b6f964a520efe11fe3", + "name": "Alaska Airlines Board Room", + "contact": { + "twitter": "alaskaair" + }, + "location": { + "address": "Terminal 1", + "crossStreet": "at SFO Airport", + "lat": 37.61343253150299, + "lng": -122.3850667476654, + "postalCode": "94128", + "cc": "US", + "city": "San Francisco", + "state": "CA", + "country": "United States" + }, + "categories": [ + { + "id": "4eb1bc533b7b2c5b1d4306cb", + "name": "Airport Lounge", + "pluralName": "Airport Lounges", + "shortName": "Lounge", + "icon": { + "prefix": "https:\/\/ss1.4sqi.net\/img\/categories_v2\/travel\/airport_lounge_", + "suffix": ".png" + }, + "primary": true + } + ], + "verified": false, + "stats": { + "checkinsCount": 1318, + "usersCount": 822, + "tipCount": 22 + }, + "url": "http:\/\/alaskaair.com", + "likes": { + "count": 6, + "groups": [ + { + "type": "others", + "count": 6, + "items": [ + { + "id": "6446336", + "firstName": "Aaron", + "lastName": "C.", + "gender": "male", + "photo": { + "prefix": "https:\/\/irs3.4sqi.net\/img\/user\/", + "suffix": "\/BVDOLAQG4BYFXHV3.jpg" + } + }, + { + "id": "15728179", + "firstName": "Christopher", + "lastName": "P.", + "gender": "male", + "photo": { + "prefix": "https:\/\/irs3.4sqi.net\/img\/user\/", + "suffix": "\/Q5AUNW2UZDUDG10E.jpg" + } + }, + { + "id": "7709153", + "firstName": "Farhad", + "lastName": "M.", + "gender": "male", + "photo": { + "prefix": "https:\/\/irs1.4sqi.net\/img\/user\/", + "suffix": "\/VZBNN1XYBEVWEAIO.jpg" + } + }, + { + "id": "181603", + "firstName": "Jeffrey-Ryan", + "lastName": "B.", + "gender": "male", + "photo": { + "prefix": "https:\/\/irs2.4sqi.net\/img\/user\/", + "suffix": "\/FP5Q5W1FHULIZOCV.jpg" + } + } + ] + } + ], + "summary": "Aaron Chaffee, Christopher Potter, Farhad M & 3 others" + }, + "like": false, + "beenHere": { + "count": 1, + "marked": true + } + }, + "likes": { + "count": 1, + "groups": [ + { + "type": "friends", + "count": 1, + "items": [ + { + "id": "431392", + "firstName": "Owen", + "lastName": "Thomas", + "gender": "male", + "relationship": "friend", + "photo": { + "prefix": "https:\/\/irs0.4sqi.net\/img\/user\/", + "suffix": "\/GEDBNXFSYUXRUFLD.gif" + } + } + ] + } + ], + "summary": "Owen Thomas" + }, + "like": false, + "photos": { + "count": 0, + "items": [] + }, + "posts": { + "count": 0, + "textCount": 0 + }, + "comments": { + "count": 0 + }, + "source": { + "name": "foursquare for Android", + "url": "https:\/\/foursquare.com\/download\/#\/android" + } + } + ] + }, + "following": { + "count": 1, + "groups": [ + { + "type": "following", + "name": "Mutual following", + "count": 0, + "items": [] + }, + { + "type": "others", + "name": "Other following", + "count": 1, + "items": [ + { + "id": "13276", + "firstName": "Loic", + "lastName": "L.", + "gender": "male", + "relationship": "followingThem", + "photo": { + "prefix": "https:\/\/irs0.4sqi.net\/img\/user\/", + "suffix": "\/MHWAEBRGHQOMJE22.jpg" + }, + "type": "celebrity", + "followers": { + "count": 8132, + "groups": [] + }, + "tips": { + "count": 16 + }, + "lists": { + "groups": [ + { + "type": "created", + "count": 1, + "items": [] + } + ] + }, + "homeCity": "San Francisco", + "bio": "LeWeb and Seesmic founder, love creating things", + "contact": { + "twitter": "loic", + "facebook": "1417669498" + } + } + ] + } + ] + }, + "requests": { + "count": 280 + }, + "lists": { + "count": 1, + "groups": [ + { + "type": "created", + "count": 1, + "items": [ + { + "id": "13674\/todos", + "name": "My to-do list", + "description": "", + "user": { + "id": "13674", + "firstName": "Brad", + "lastName": "Fitzpatrick", + "gender": "male", + "relationship": "self", + "photo": { + "prefix": "https:\/\/irs0.4sqi.net\/img\/user\/", + "suffix": "\/CKG5FOF2WMCMPD3E.jpg" + } + }, + "editable": false, + "public": false, + "collaborative": false, + "url": "\/bradfitz\/list\/todos", + "canonicalUrl": "https:\/\/foursquare.com\/bradfitz\/list\/todos", + "followers": { + "count": 0 + }, + "listItems": { + "count": 3 + } + } + ] + }, + { + "type": "followed", + "count": 0, + "items": [] + } + ] + }, + "photos": { + "count": 11, + "items": [ + { + "id": "530278c311d26be8ab5da961", + "createdAt": 1392670915, + "source": { + "name": "foursquare for Android", + "url": "https:\/\/foursquare.com\/download\/#\/android" + }, + "prefix": "https:\/\/irs1.4sqi.net\/img\/general\/", + "suffix": "\/13674_4TxZ1OeQuFwOlprqcI1lWGZN4Or2f4Oal1rGup6ZPS4.jpg", + "width": 960, + "height": 720, + "visibility": "public", + "venue": { + "id": "40870b00f964a520aaf21ee3", + "name": "The Liberties", + "contact": { + "phone": "4152826789", + "formattedPhone": "(415) 282-6789", + "twitter": "thelibertiesbar" + }, + "location": { + "address": "998 Guerrero St", + "crossStreet": "at 22nd St", + "lat": 37.75523648445705, + "lng": -122.4232582553582, + "postalCode": "94110", + "cc": "US", + "city": "San Francisco", + "state": "CA", + "country": "United States" + }, + "categories": [ + { + "id": "4bf58dd8d48988d116941735", + "name": "Bar", + "pluralName": "Bars", + "shortName": "Bar", + "icon": { + "prefix": "https:\/\/ss1.4sqi.net\/img\/categories_v2\/nightlife\/bar_", + "suffix": ".png" + }, + "primary": true + } + ], + "verified": true, + "stats": { + "checkinsCount": 4526, + "usersCount": 2147, + "tipCount": 35 + }, + "url": "http:\/\/www.theliberties.com", + "likes": { + "count": 21, + "groups": [ + { + "type": "others", + "count": 20, + "items": [] + } + ], + "summary": "You and 20 others" + }, + "like": true, + "menu": { + "type": "Menu", + "label": "Menu", + "anchor": "View Menu", + "url": "https:\/\/foursquare.com\/v\/the-liberties\/40870b00f964a520aaf21ee3\/menu", + "mobileUrl": "https:\/\/foursquare.com\/v\/40870b00f964a520aaf21ee3\/device_menu" + }, + "beenHere": { + "count": 24, + "marked": true + }, + "venuePage": { + "id": "46953806" + }, + "storeId": "" + }, + "checkin": { + "id": "5302789511d2c9d07c49130e", + "createdAt": 1392670869, + "type": "checkin", + "timeZoneOffset": -480 + } + } + ] + }, + "scores": { + "recent": 116, + "max": 272, + "checkinsCount": 27 + }, + "createdAt": 1242876758, + "referralId": "u-13674" + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/html.go b/vendor/github.com/camlistore/camlistore/pkg/importer/html.go new file mode 100644 index 00000000..0802238e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/html.go @@ -0,0 +1,211 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package importer + +import ( + "bytes" + "fmt" + "html/template" + "net/http" + "strings" + "time" + + "camlistore.org/pkg/blob" +) + +func (h *Host) execTemplate(w http.ResponseWriter, r *http.Request, data interface{}) { + tmplName := strings.TrimPrefix(fmt.Sprintf("%T", data), "importer.") + var buf bytes.Buffer + err := h.tmpl.ExecuteTemplate(&buf, tmplName, data) + if err != nil { + http.Error(w, fmt.Sprintf("Error executing template %q: %v", tmplName, err), 500) + return + } + w.Write(buf.Bytes()) +} + +type importersRootPage struct { + Title string + Body importersRootBody +} + +type importersRootBody struct { + Host *Host + Importers []*importer +} + +type importerPage struct { + Title string + Body importerBody +} + +type importerBody struct { + Host *Host + Importer *importer + SetupHelp template.HTML +} + +type acctPage struct { + Title string + Body acctBody +} + +type acctBody struct { + Acct *importerAcct + AcctType string + Running bool + LastStatus string + StartedAgo time.Duration // or zero if !Running + LastAgo time.Duration // non-zero if previous run && !Running + LastError string +} + +var tmpl = template.Must(template.New("root").Funcs(map[string]interface{}{ + "bloblink": func(br blob.Ref) string { + panic("should be overridden; this one won't be called") + }, +}).Parse(` +{{define "pageTop"}} + + + {{.Title}} + + +

    {{.Title}}

    +{{end}} + +{{define "pageBottom"}} + + +{{end}} + + +{{define "importersRootPage"}} + {{template "pageTop" .}} + {{template "importersRootBody" .Body}} + {{template "pageBottom"}} +{{end}} + +{{define "importersRootBody"}} +
      + {{$base := .Host.ImporterBaseURL}} + {{range .Importers}} +
    • {{.Name}}
    • + {{end}} +
    +{{end}} + + +{{define "importerPage"}} + {{template "pageTop" .}} + {{template "importerBody" .Body}} + {{template "pageBottom"}} +{{end}} + +{{define "importerBody"}} +

    [<< Back]

    +
      +
    • Importer configuration permanode: {{.Importer.Node.PermanodeRef | bloblink}}
    • +
    • Status: {{.Importer.Status}}
    • +
    + +{{if .Importer.ShowClientAuthEditForm}} +

    Client ID & Client Secret

    +
    + + + + + +
    Client ID
    Client Secret
    +
    +{{end}} + +{{.SetupHelp}} + + +

    Accounts

    + +{{if .Importer.CanAddNewAccount}} +
    + + +
    +{{end}} + +{{end}} + +{{define "acctPage"}} + {{template "pageTop" .}} + {{template "acctBody" .Body}} + {{template "pageBottom"}} +{{end}} + +{{define "acctBody"}} +

    [<< Back]

    +
      +
    • Account type: {{.AcctType}}
    • +
    • Account metadata permanode: {{.Acct.AccountObject.PermanodeRef | bloblink}}
    • +
    • Import root permanode: {{if .Acct.RootObject}}{{.Acct.RootObject.PermanodeRef | bloblink}}{{else}}(none){{end}}
    • +
    • Configured: {{.Acct.IsAccountReady}}
    • +
    • Summary: {{.Acct.AccountLinkSummary}}
    • +
    • Import interval: {{if .Acct.RefreshInterval}}{{.Acct.RefreshInterval}}{{else}}(manual){{end}}
    • +
    • Running: {{.Running}}
    • + {{if .Running}} +
    • Started: {{.StartedAgo}} ago
    • +
    • Last status: {{.LastStatus}}
    • + {{else}} + {{if .LastAgo}} +
    • Previous run: {{.LastAgo}} ago{{if .LastError}}: {{.LastError}}{{else}} (success){{end}}
    • + {{end}} + {{end}} +
    + +{{if .Acct.IsAccountReady}} +
    + {{if .Running}} + + + {{else}} + + + {{end}} +
    +{{end}} + +
    + + +
    + +
    + + +
    + +
    + + +
    + +{{end}} + +`)) diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/importer.go b/vendor/github.com/camlistore/camlistore/pkg/importer/importer.go new file mode 100644 index 00000000..ad9875e5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/importer.go @@ -0,0 +1,1335 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package importer imports content from third-party websites. +package importer + +import ( + "errors" + "fmt" + "html/template" + "log" + "net/http" + "net/url" + "os" + "sort" + "strconv" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/jsonsign/signhandler" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/search" + "camlistore.org/pkg/server" + "camlistore.org/pkg/syncutil" + "camlistore.org/pkg/types/camtypes" +) + +const ( + attrNodeType = "camliNodeType" + nodeTypeImporter = "importer" + nodeTypeImporterAccount = "importerAccount" + + attrImporterType = "importerType" // => "twitter", "foursquare", etc + attrClientID = "authClientID" + attrClientSecret = "authClientSecret" + attrImportRoot = "importRoot" + attrImportAuto = "importAuto" // => time.Duration value ("30m") or "" for off +) + +// An Importer imports from a third-party site. +type Importer interface { + // Run runs a full or incremental import. + // + // The importer should continually or periodically monitor the + // context's Done channel to exit early if requested. The + // return value should be context.ErrCanceled if the importer + // exits for that reason. + Run(*RunContext) error + + // NeedsAPIKey reports whether this importer requires an API key + // (OAuth2 client_id & client_secret, or equivalent). + // If the API only requires a username & password, or a flow to get + // an auth token per-account without an overall API key, importers + // can return false here. + NeedsAPIKey() bool + + // SupportsIncremental reports whether this importer has been optimized + // to run efficiently in regular incremental runs. (e.g. every 5 minutes + // or half hour). Eventually all importers might support this and we'll + // make it required, in which case we might delete this option. + // For now, some importers (e.g. Flickr) don't yet support this. + SupportsIncremental() bool + + // IsAccountReady reports whether the provided account node + // is configured. + IsAccountReady(acctNode *Object) (ok bool, err error) + SummarizeAccount(acctNode *Object) string + + ServeSetup(w http.ResponseWriter, r *http.Request, ctx *SetupContext) error + ServeCallback(w http.ResponseWriter, r *http.Request, ctx *SetupContext) + + // CallbackRequestAccount extracts the blobref of the importer account from + // the callback URL parameters of r. For example, it will be encoded as: + // For Twitter (OAuth1), in its own URL parameter: "acct=sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4" + // For Picasa: (OAuth2), in the OAuth2 "state" parameter: "state=acct:sha1-97911b1a5887eb5862d1c81666ba839fc1363ea1" + CallbackRequestAccount(r *http.Request) (acctRef blob.Ref, err error) + + // CallbackURLParameters uses the input importer account blobRef to build + // and return the URL parameters, that will be appended to the callback URL. + CallbackURLParameters(acctRef blob.Ref) url.Values +} + +// TestDataMaker is an optional interface that may be implemented by Importers to +// generate test data locally. The returned Roundtripper will be used as the +// transport of the HTTPClient, in the RunContext that will be passed to Run +// during tests and devcam server --makethings. +// (See http://camlistore.org/issue/417). +type TestDataMaker interface { + MakeTestData() http.RoundTripper + // SetTestAccount allows an importer to set some needed attributes on the importer + // account node before a run is started. + SetTestAccount(acctNode *Object) error +} + +// ImporterSetupHTMLer is an optional interface that may be implemented by +// Importers to return some HTML to be included on the importer setup page. +type ImporterSetupHTMLer interface { + AccountSetupHTML(*Host) string +} + +var importers = make(map[string]Importer) + +// All returns the map of importer implementation name to implementation. This +// map should not be mutated. +func All() map[string]Importer { + return importers +} + +// Register registers a site-specific importer. It should only be called from init, +// and not from concurrent goroutines. +func Register(name string, im Importer) { + if _, dup := importers[name]; dup { + panic("Dup registration of importer " + name) + } + importers[name] = im +} + +func init() { + // Register the meta "importer" handler, which handles all other handlers. + blobserver.RegisterHandlerConstructor("importer", newFromConfig) +} + +// HostConfig holds the parameters to set up a Host. +type HostConfig struct { + BaseURL string + Prefix string // URL prefix for the importer handler + Target blobserver.StatReceiver // storage for the imported object blobs + BlobSource blob.Fetcher // for additional resources, such as twitter zip file + Signer *schema.Signer + Search search.QueryDescriber + ClientId map[string]string // optionally maps importer impl name to a clientId credential + ClientSecret map[string]string // optionally maps importer impl name to a clientSecret credential + + // HTTPClient optionally specifies how to fetch external network + // resources. The Host will use http.DefaultClient otherwise. + HTTPClient *http.Client + // TODO: add more if/when needed +} + +func NewHost(hc HostConfig) (*Host, error) { + h := &Host{ + baseURL: hc.BaseURL, + importerBase: hc.BaseURL + hc.Prefix, + imp: make(map[string]*importer), + } + var err error + h.tmpl, err = tmpl.Clone() + if err != nil { + return nil, err + } + h.tmpl = h.tmpl.Funcs(map[string]interface{}{ + "bloblink": func(br blob.Ref) template.HTML { + if h.uiPrefix == "" { + return template.HTML(br.String()) + } + return template.HTML(fmt.Sprintf("%s", h.uiPrefix, br, br)) + }, + }) + for k, impl := range importers { + h.importers = append(h.importers, k) + clientId, clientSecret := hc.ClientId[k], hc.ClientSecret[k] + if clientSecret != "" && clientId == "" { + return nil, fmt.Errorf("Invalid static configuration for importer %q: clientSecret specified without clientId", k) + } + imp := &importer{ + host: h, + name: k, + impl: impl, + clientID: clientId, + clientSecret: clientSecret, + } + h.imp[k] = imp + } + + sort.Strings(h.importers) + + h.target = hc.Target + h.blobSource = hc.BlobSource + h.signer = hc.Signer + h.search = hc.Search + h.client = hc.HTTPClient + + return h, nil +} + +func newFromConfig(ld blobserver.Loader, cfg jsonconfig.Obj) (http.Handler, error) { + hc := HostConfig{ + BaseURL: ld.BaseURL(), + Prefix: ld.MyPrefix(), + } + ClientId := make(map[string]string) + ClientSecret := make(map[string]string) + for k, _ := range importers { + var clientId, clientSecret string + if impConf := cfg.OptionalObject(k); impConf != nil { + clientId = impConf.OptionalString("clientID", "") + clientSecret = impConf.OptionalString("clientSecret", "") + // Special case: allow clientSecret to be of form "clientId:clientSecret" + // if the clientId is empty. + if clientId == "" && strings.Contains(clientSecret, ":") { + if f := strings.SplitN(clientSecret, ":", 2); len(f) == 2 { + clientId, clientSecret = f[0], f[1] + } + } + if err := impConf.Validate(); err != nil { + return nil, fmt.Errorf("Invalid static configuration for importer %q: %v", k, err) + } + ClientId[k] = clientId + ClientSecret[k] = clientSecret + } + } + if err := cfg.Validate(); err != nil { + return nil, err + } + hc.ClientId = ClientId + hc.ClientSecret = ClientSecret + host, err := NewHost(hc) + if err != nil { + return nil, err + } + host.didInit.Add(1) + return host, nil +} + +var _ blobserver.HandlerIniter = (*Host)(nil) + +type SetupContext struct { + *context.Context + Host *Host + AccountNode *Object + + ia *importerAcct +} + +func (sc *SetupContext) Credentials() (clientID, clientSecret string, err error) { + return sc.ia.im.credentials() +} + +func (sc *SetupContext) CallbackURL() string { + params := sc.ia.im.impl.CallbackURLParameters(sc.AccountNode.PermanodeRef()).Encode() + if params != "" { + params = "?" + params + } + return sc.Host.ImporterBaseURL() + sc.ia.im.name + "/callback" + params +} + +// AccountURL returns the URL to an account of an importer +// (http://host/importer/TYPE/sha1-sd8fsd7f8sdf7). +func (sc *SetupContext) AccountURL() string { + return sc.Host.ImporterBaseURL() + sc.ia.im.name + "/" + sc.AccountNode.PermanodeRef().String() +} + +// RunContext is the context provided for a given Run of an importer, importing +// a certain account on a certain importer. +type RunContext struct { + *context.Context + Host *Host + + ia *importerAcct + + mu sync.Mutex // guards following + lastProgress *ProgressMessage +} + +// CreateAccount creates a new importer account for the Host h, and the importer +// implementation named impl. It returns a RunContext setup with that account. +func CreateAccount(h *Host, impl string) (*RunContext, error) { + imp, ok := h.imp[impl] + if !ok { + return nil, fmt.Errorf("host does not have a %v importer", impl) + } + ia, err := imp.newAccount() + if err != nil { + return nil, fmt.Errorf("could not create new account for importer %v: %v", impl, err) + } + return &RunContext{ + // TODO: context plumbing + Context: context.New(context.WithHTTPClient(ia.im.host.HTTPClient())), + Host: ia.im.host, + ia: ia, + }, nil +} + +// Credentials returns the credentials for the importer. This is +// typically the OAuth1, OAuth2, or equivalent client ID (api token) +// and client secret (api secret). +func (rc *RunContext) Credentials() (clientID, clientSecret string, err error) { + return rc.ia.im.credentials() +} + +// AccountNode returns the permanode storing account information for this permanode. +// It will contain the attributes: +// * camliNodeType = "importerAccount" +// * importerType = "registered-type" +// +// You must not change the camliNodeType or importerType. +// +// You should use this permanode to store state about where your +// importer left off, if it can efficiently resume later (without +// missing anything). +func (rc *RunContext) AccountNode() *Object { return rc.ia.acct } + +// RootNode returns the initially-empty permanode storing the root +// of this account's data. You can change anything at will. This will +// typically be modeled as a dynamic directory (with camliPath:xxxx +// attributes), where each path element is either a file, object, or +// another dynamic directory. +func (rc *RunContext) RootNode() *Object { return rc.ia.root } + +// Host is the HTTP handler and state for managing all the importers +// linked into the binary, even if they're not configured. +type Host struct { + tmpl *template.Template + importers []string // sorted; e.g. dummy flickr foursquare picasa twitter + imp map[string]*importer + baseURL string + importerBase string + target blobserver.StatReceiver + blobSource blob.Fetcher // e.g. twitter reading zip file + search search.QueryDescriber + signer *schema.Signer + uiPrefix string // or empty if no UI handler + + // didInit is incremented by newFromConfig and marked done + // after InitHandler. Any method on Host that requires Init + // then calls didInit.Wait to guard against initialization + // races where serverinit calls InitHandler in a random + // order on start-up and different handlers access the + // not-yet-initialized Host (notably from a goroutine) + didInit sync.WaitGroup + + // HTTPClient optionally specifies how to fetch external network + // resources. Defaults to http.DefaultClient. + client *http.Client + transport http.RoundTripper +} + +// accountStatus is the JSON representation of the status of a configured importer account. +type accountStatus struct { + Name string `json:"name"` // display name + Type string `json:"type"` + Href string `json:"href"` + + StartedUnixSec int64 `json:"startedUnixSec"` // zero if not running + LastFinishedUnixSec int64 `json:"finishedUnixSec"` // zero if no previous run + LastError string `json:"lastRunError"` // empty if last run was success +} + +// AccountsStatus returns the currently configured accounts and their status for +// inclusion in the status.json document, as rendered by the web UI. +func (h *Host) AccountsStatus() (interface{}, []camtypes.StatusError) { + h.didInit.Wait() + var s []accountStatus + var errs []camtypes.StatusError + for _, impName := range h.importers { + imp := h.imp[impName] + accts, _ := imp.Accounts() + for _, ia := range accts { + as := accountStatus{ + Type: impName, + Href: ia.AccountURL(), + Name: ia.AccountLinkSummary(), + } + ia.mu.Lock() + if ia.current != nil { + as.StartedUnixSec = ia.lastRunStart.Unix() + } + if !ia.lastRunDone.IsZero() { + as.LastFinishedUnixSec = ia.lastRunDone.Unix() + } + if ia.lastRunErr != nil { + as.LastError = ia.lastRunErr.Error() + errs = append(errs, camtypes.StatusError{ + Error: ia.lastRunErr.Error(), + URL: ia.AccountURL(), + }) + } + ia.mu.Unlock() + s = append(s, as) + } + } + return s, errs +} + +func (h *Host) InitHandler(hl blobserver.FindHandlerByTyper) error { + if prefix, _, err := hl.FindHandlerByType("ui"); err == nil { + h.uiPrefix = prefix + } + + _, handler, err := hl.FindHandlerByType("root") + if err != nil || handler == nil { + return errors.New("importer requires a 'root' handler") + } + rh := handler.(*server.RootHandler) + searchHandler, ok := rh.SearchHandler() + if !ok { + return errors.New("importer requires a 'root' handler with 'searchRoot' defined.") + } + h.search = searchHandler + if rh.Storage == nil { + return errors.New("importer requires a 'root' handler with 'blobRoot' defined.") + } + h.target = rh.Storage + h.blobSource = rh.Storage + + _, handler, _ = hl.FindHandlerByType("jsonsign") + if sigh, ok := handler.(*signhandler.Handler); ok { + h.signer = sigh.Signer() + } + if h.signer == nil { + return errors.New("importer requires a 'jsonsign' handler") + } + h.didInit.Done() + go h.startPeriodicImporters() + return nil +} + +// ServeHTTP serves: +// http://host/importer/ +// http://host/importer/twitter/ +// http://host/importer/twitter/callback +// http://host/importer/twitter/sha1-abcabcabcabcabc (single account) +func (h *Host) ServeHTTP(w http.ResponseWriter, r *http.Request) { + suffix := httputil.PathSuffix(r) + seg := strings.Split(suffix, "/") + if suffix == "" || len(seg) == 0 { + h.serveImportersRoot(w, r) + return + } + impName := seg[0] + + imp, ok := h.imp[impName] + if !ok { + http.NotFound(w, r) + return + } + + if len(seg) == 1 || seg[1] == "" { + h.serveImporter(w, r, imp) + return + } + if seg[1] == "callback" { + h.serveImporterAcctCallback(w, r, imp) + return + } + acctRef, ok := blob.Parse(seg[1]) + if !ok { + http.NotFound(w, r) + return + } + h.serveImporterAccount(w, r, imp, acctRef) +} + +// Serves list of importers at http://host/importer/ +func (h *Host) serveImportersRoot(w http.ResponseWriter, r *http.Request) { + body := importersRootBody{ + Host: h, + Importers: make([]*importer, 0, len(h.imp)), + } + for _, v := range h.importers { + body.Importers = append(body.Importers, h.imp[v]) + } + h.execTemplate(w, r, importersRootPage{ + Title: "Importers", + Body: body, + }) +} + +// Serves list of accounts at http://host/importer/twitter +func (h *Host) serveImporter(w http.ResponseWriter, r *http.Request, imp *importer) { + if r.Method == "POST" { + h.serveImporterPost(w, r, imp) + return + } + + var setup string + node, _ := imp.Node() + if setuper, ok := imp.impl.(ImporterSetupHTMLer); ok && node != nil { + setup = setuper.AccountSetupHTML(h) + } + + h.execTemplate(w, r, importerPage{ + Title: "Importer - " + imp.Name(), + Body: importerBody{ + Host: h, + Importer: imp, + SetupHelp: template.HTML(setup), + }, + }) +} + +// Serves oauth callback at http://host/importer/TYPE/callback +func (h *Host) serveImporterAcctCallback(w http.ResponseWriter, r *http.Request, imp *importer) { + if r.Method != "GET" { + http.Error(w, "invalid method", 400) + return + } + acctRef, err := imp.impl.CallbackRequestAccount(r) + if err != nil { + httputil.ServeError(w, r, err) + return + } + if !acctRef.Valid() { + httputil.ServeError(w, r, errors.New("No valid blobref returned from CallbackRequestAccount(r)")) + return + } + ia, err := imp.account(acctRef) + if err != nil { + http.Error(w, "invalid 'acct' param: "+err.Error(), 400) + return + } + imp.impl.ServeCallback(w, r, &SetupContext{ + Context: context.TODO(), + Host: h, + AccountNode: ia.acct, + ia: ia, + }) +} + +func (h *Host) serveImporterPost(w http.ResponseWriter, r *http.Request, imp *importer) { + switch r.FormValue("mode") { + default: + http.Error(w, "Unknown mode.", 400) + case "newacct": + ia, err := imp.newAccount() + if err != nil { + http.Error(w, err.Error(), 500) + return + } + ia.setup(w, r) + return + case "saveclientidsecret": + n, err := imp.Node() + if err != nil { + http.Error(w, "Error getting node: "+err.Error(), 500) + return + } + if err := n.SetAttrs( + attrClientID, r.FormValue("clientID"), + attrClientSecret, r.FormValue("clientSecret"), + ); err != nil { + http.Error(w, "Error saving node: "+err.Error(), 500) + return + } + http.Redirect(w, r, h.ImporterBaseURL()+imp.name, http.StatusFound) + } +} + +// Serves details of accounts at http://host/importer/twitter/sha1-23098429382934 +func (h *Host) serveImporterAccount(w http.ResponseWriter, r *http.Request, imp *importer, acctRef blob.Ref) { + ia, err := imp.account(acctRef) + if err != nil { + http.Error(w, "Unknown or invalid importer account "+acctRef.String()+": "+err.Error(), 400) + return + } + ia.ServeHTTP(w, r) +} + +func (h *Host) startPeriodicImporters() { + res, err := h.search.Query(&search.SearchQuery{ + Expression: "attr:camliNodeType:importerAccount", + Describe: &search.DescribeRequest{ + Depth: 1, + }, + }) + if err != nil { + log.Printf("periodic importer search fail: %v", err) + return + } + if res.Describe == nil { + log.Printf("No describe response in search result") + return + } + for _, resBlob := range res.Blobs { + blob := resBlob.Blob + desBlob, ok := res.Describe.Meta[blob.String()] + if !ok || desBlob.Permanode == nil { + continue + } + attrs := desBlob.Permanode.Attr + if attrs.Get(attrNodeType) != nodeTypeImporterAccount { + panic("Search result returned non-importerAccount") + } + impType := attrs.Get("importerType") + imp, ok := h.imp[impType] + if !ok { + continue + } + ia, err := imp.account(blob) + if err != nil { + log.Printf("Can't load importer account %v for regular importing: %v", blob, err) + continue + } + go ia.maybeStart() + } +} + +var disableImporters, _ = strconv.ParseBool(os.Getenv("CAMLI_DISABLE_IMPORTERS")) + +func (ia *importerAcct) maybeStart() { + if disableImporters { + log.Printf("Importers disabled, per environment.") + return + } + acctObj, err := ia.im.host.ObjectFromRef(ia.acct.PermanodeRef()) + if err != nil { + log.Printf("Error maybe starting %v: %v", ia.acct.PermanodeRef(), err) + return + } + duration, err := time.ParseDuration(acctObj.Attr(attrImportAuto)) + if duration == 0 || err != nil { + return + } + ia.mu.Lock() + defer ia.mu.Unlock() + if ia.current != nil { + return + } + if ia.lastRunDone.After(time.Now().Add(-duration)) { + sleepFor := ia.lastRunDone.Add(duration).Sub(time.Now()) + log.Printf("%v ran recently enough. Sleeping for %v.", ia, sleepFor) + time.AfterFunc(sleepFor, ia.maybeStart) + return + } + + log.Printf("Starting regular periodic import for %v", ia) + go ia.start() +} + +// BaseURL returns the root of the whole server, without trailing +// slash. +func (h *Host) BaseURL() string { + return h.baseURL +} + +// ImporterBaseURL returns the URL base of the importer handler, +// including trailing slash. +func (h *Host) ImporterBaseURL() string { + return h.importerBase +} + +func (h *Host) Target() blobserver.StatReceiver { + return h.target +} + +func (h *Host) BlobSource() blob.Fetcher { + return h.blobSource +} + +func (h *Host) Searcher() search.QueryDescriber { return h.search } + +// importer is an importer for a certain site, but not a specific account on that site. +type importer struct { + host *Host + name string // importer name e.g. "twitter" + impl Importer + + // If statically configured in config file, else + // they come from the importer node's attributes. + clientID string + clientSecret string + + nodemu sync.Mutex // guards nodeCache + nodeCache *Object // or nil if unset + + acctmu sync.Mutex + acct map[blob.Ref]*importerAcct // key: account permanode +} + +func (im *importer) Name() string { return im.name } + +func (im *importer) StaticConfig() bool { return im.clientSecret != "" } + +// URL returns the importer's URL without trailing slash. +func (im *importer) URL() string { return im.host.ImporterBaseURL() + im.name } + +func (im *importer) ShowClientAuthEditForm() bool { + if im.StaticConfig() { + // Don't expose the server's statically-configured client secret + // to the user. (e.g. a hosted multi-user configuation) + return false + } + return im.impl.NeedsAPIKey() +} + +func (im *importer) CanAddNewAccount() bool { + if !im.impl.NeedsAPIKey() { + return true + } + id, sec, err := im.credentials() + return id != "" && sec != "" && err == nil +} + +func (im *importer) ClientID() (v string, err error) { + v, _, err = im.credentials() + return +} + +func (im *importer) ClientSecret() (v string, err error) { + _, v, err = im.credentials() + return +} + +func (im *importer) Status() (status string, err error) { + if !im.impl.NeedsAPIKey() { + return "no configuration required", nil + } + if im.StaticConfig() { + return "API key configured on server", nil + } + n, err := im.Node() + if err != nil { + return + } + if n.Attr(attrClientID) != "" && n.Attr(attrClientSecret) != "" { + return "API key configured on node", nil + } + return "API key (client ID & Secret) not configured", nil +} + +func (im *importer) credentials() (clientID, clientSecret string, err error) { + if im.StaticConfig() { + return im.clientID, im.clientSecret, nil + } + n, err := im.Node() + if err != nil { + return + } + return n.Attr(attrClientID), n.Attr(attrClientSecret), nil +} + +func (im *importer) deleteAccount(acctRef blob.Ref) { + im.acctmu.Lock() + delete(im.acct, acctRef) + im.acctmu.Unlock() +} + +func (im *importer) account(nodeRef blob.Ref) (*importerAcct, error) { + im.acctmu.Lock() + ia, ok := im.acct[nodeRef] + im.acctmu.Unlock() + if ok { + return ia, nil + } + + acct, err := im.host.ObjectFromRef(nodeRef) + if err != nil { + return nil, err + } + if acct.Attr(attrNodeType) != nodeTypeImporterAccount { + return nil, errors.New("account has wrong node type") + } + if acct.Attr(attrImporterType) != im.name { + return nil, errors.New("account has wrong importer type") + } + var root *Object + if v := acct.Attr(attrImportRoot); v != "" { + rootRef, ok := blob.Parse(v) + if !ok { + return nil, errors.New("invalid import root attribute") + } + root, err = im.host.ObjectFromRef(rootRef) + if err != nil { + return nil, err + } + } else { + root, err = im.host.NewObject() + if err != nil { + return nil, err + } + if err := acct.SetAttr(attrImportRoot, root.PermanodeRef().String()); err != nil { + return nil, err + } + } + ia = &importerAcct{ + im: im, + acct: acct, + root: root, + } + im.acctmu.Lock() + defer im.acctmu.Unlock() + im.addAccountLocked(ia) + return ia, nil +} + +func (im *importer) newAccount() (*importerAcct, error) { + acct, err := im.host.NewObject() + if err != nil { + return nil, err + } + root, err := im.host.NewObject() + if err != nil { + return nil, err + } + if err := acct.SetAttrs( + "title", fmt.Sprintf("%s account", im.name), + attrNodeType, nodeTypeImporterAccount, + attrImporterType, im.name, + attrImportRoot, root.PermanodeRef().String(), + ); err != nil { + return nil, err + } + + ia := &importerAcct{ + im: im, + acct: acct, + root: root, + } + im.acctmu.Lock() + defer im.acctmu.Unlock() + im.addAccountLocked(ia) + return ia, nil +} + +func (im *importer) addAccountLocked(ia *importerAcct) { + if im.acct == nil { + im.acct = make(map[blob.Ref]*importerAcct) + } + im.acct[ia.acct.PermanodeRef()] = ia +} + +func (im *importer) Accounts() ([]*importerAcct, error) { + var accts []*importerAcct + + // TODO: cache this search. invalidate when new accounts are made. + res, err := im.host.search.Query(&search.SearchQuery{ + Expression: fmt.Sprintf("attr:%s:%s attr:%s:%s", + attrNodeType, nodeTypeImporterAccount, + attrImporterType, im.name, + ), + }) + if err != nil { + return nil, err + } + for _, res := range res.Blobs { + ia, err := im.account(res.Blob) + if err != nil { + return nil, err + } + accts = append(accts, ia) + } + return accts, nil +} + +// node returns the importer node. (not specific to a certain account +// on that importer site) +// +// It is a permanode with: +// camliNodeType: "importer" +// importerType: "twitter" +// And optionally: +// authClientID: "xxx" // e.g. api token +// authClientSecret: "sdkojfsldfjlsdkf" +func (im *importer) Node() (*Object, error) { + im.nodemu.Lock() + defer im.nodemu.Unlock() + if im.nodeCache != nil { + return im.nodeCache, nil + } + + expr := fmt.Sprintf("attr:%s:%s attr:%s:%s", + attrNodeType, nodeTypeImporter, + attrImporterType, im.name, + ) + res, err := im.host.search.Query(&search.SearchQuery{ + Limit: 10, // only expect 1 + Expression: expr, + }) + if err != nil { + return nil, err + } + if len(res.Blobs) > 1 { + return nil, fmt.Errorf("Ambiguous; too many permanodes matched query %q: %v", expr, res.Blobs) + } + if len(res.Blobs) == 1 { + return im.host.ObjectFromRef(res.Blobs[0].Blob) + } + o, err := im.host.NewObject() + if err != nil { + return nil, err + } + if err := o.SetAttrs( + "title", fmt.Sprintf("%s importer", im.name), + attrNodeType, nodeTypeImporter, + attrImporterType, im.name, + ); err != nil { + return nil, err + } + + im.nodeCache = o + return o, nil +} + +// importerAcct is a long-lived type representing account +type importerAcct struct { + im *importer + acct *Object + root *Object + + mu sync.Mutex + current *RunContext // or nil if not running + stopped bool // stop requested (context canceled) + lastRunErr error + lastRunStart time.Time + lastRunDone time.Time +} + +func (ia *importerAcct) String() string { + return fmt.Sprintf("%v importer account, %v", ia.im.name, ia.acct.PermanodeRef()) +} + +func (ia *importerAcct) delete() error { + if err := ia.acct.SetAttrs( + attrNodeType, nodeTypeImporterAccount+"-deleted", + ); err != nil { + return err + } + ia.im.deleteAccount(ia.acct.PermanodeRef()) + return nil +} + +func (ia *importerAcct) toggleAuto() error { + old := ia.acct.Attr(attrImportAuto) + if old == "" && !ia.im.impl.SupportsIncremental() { + return fmt.Errorf("Importer %q doesn't support automatic mode.", ia.im.name) + } + var new string + if old == "" { + new = "30m" // TODO: configurable? + } + return ia.acct.SetAttrs(attrImportAuto, new) +} + +func (ia *importerAcct) IsAccountReady() (bool, error) { + return ia.im.impl.IsAccountReady(ia.acct) +} + +func (ia *importerAcct) AccountObject() *Object { return ia.acct } +func (ia *importerAcct) RootObject() *Object { return ia.root } + +func (ia *importerAcct) AccountURL() string { + return ia.im.URL() + "/" + ia.acct.PermanodeRef().String() +} + +func (ia *importerAcct) AccountLinkText() string { + return ia.acct.PermanodeRef().String() +} + +func (ia *importerAcct) AccountLinkSummary() string { + return ia.im.impl.SummarizeAccount(ia.acct) +} + +func (ia *importerAcct) RefreshInterval() time.Duration { + ds := ia.acct.Attr(attrImportAuto) + if ds == "" { + return 0 + } + d, _ := time.ParseDuration(ds) + return d +} + +func (ia *importerAcct) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + ia.serveHTTPPost(w, r) + return + } + ia.mu.Lock() + defer ia.mu.Unlock() + body := acctBody{ + Acct: ia, + AcctType: fmt.Sprintf("%T", ia.im.impl), + } + if run := ia.current; run != nil { + body.Running = true + body.StartedAgo = time.Since(ia.lastRunStart) + run.mu.Lock() + body.LastStatus = fmt.Sprintf("%+v", run.lastProgress) + run.mu.Unlock() + } else if !ia.lastRunDone.IsZero() { + body.LastAgo = time.Since(ia.lastRunDone) + if ia.lastRunErr != nil { + body.LastError = ia.lastRunErr.Error() + } + } + title := fmt.Sprintf("%s account: ", ia.im.name) + if summary := ia.im.impl.SummarizeAccount(ia.acct); summary != "" { + title += summary + } else { + title += ia.acct.PermanodeRef().String() + } + ia.im.host.execTemplate(w, r, acctPage{ + Title: title, + Body: body, + }) +} + +func (ia *importerAcct) serveHTTPPost(w http.ResponseWriter, r *http.Request) { + // TODO: XSRF token + + switch r.FormValue("mode") { + case "": + // Nothing. + case "start": + ia.start() + case "stop": + ia.stop() + case "login": + ia.setup(w, r) + return + case "toggleauto": + if err := ia.toggleAuto(); err != nil { + http.Error(w, err.Error(), 500) + return + } + case "delete": + ia.stop() // can't hurt + if err := ia.delete(); err != nil { + http.Error(w, err.Error(), 500) + return + } + http.Redirect(w, r, ia.im.URL(), http.StatusFound) + return + default: + http.Error(w, "Unknown mode", 400) + return + } + http.Redirect(w, r, ia.AccountURL(), http.StatusFound) +} + +func (ia *importerAcct) setup(w http.ResponseWriter, r *http.Request) { + if err := ia.im.impl.ServeSetup(w, r, &SetupContext{ + Context: context.TODO(), + Host: ia.im.host, + AccountNode: ia.acct, + ia: ia, + }); err != nil { + log.Printf("%v", err) + } +} + +func (ia *importerAcct) start() { + ia.mu.Lock() + defer ia.mu.Unlock() + if ia.current != nil { + return + } + rc := &RunContext{ + // TODO: context plumbing + Context: context.New(context.WithHTTPClient(ia.im.host.HTTPClient())), + Host: ia.im.host, + ia: ia, + } + ia.current = rc + ia.stopped = false + ia.lastRunStart = time.Now() + go func() { + log.Printf("Starting %v: %s", ia, ia.AccountLinkSummary()) + err := ia.im.impl.Run(rc) + if err != nil { + log.Printf("%v error: %v", ia, err) + } else { + log.Printf("%v finished.", ia) + } + ia.mu.Lock() + defer ia.mu.Unlock() + ia.current = nil + ia.stopped = false + ia.lastRunDone = time.Now() + ia.lastRunErr = err + go ia.maybeStart() + }() +} + +func (ia *importerAcct) stop() { + ia.mu.Lock() + defer ia.mu.Unlock() + if ia.current == nil || ia.stopped { + return + } + ia.current.Context.Cancel() + ia.stopped = true +} + +// HTTPClient returns the HTTP client to use. +func (h *Host) HTTPClient() *http.Client { + if h.client == nil { + return http.DefaultClient + } + return h.client +} + +// HTTPTransport returns the HTTP transport to use. +func (h *Host) HTTPTransport() http.RoundTripper { + if h.transport == nil { + return http.DefaultTransport + } + return h.transport +} + +type ProgressMessage struct { + ItemsDone, ItemsTotal int + BytesDone, BytesTotal int64 +} + +func (h *Host) upload(bb *schema.Builder) (br blob.Ref, err error) { + signed, err := bb.Sign(h.signer) + if err != nil { + return + } + sb, err := blobserver.ReceiveString(h.target, signed) + if err != nil { + return + } + return sb.Ref, nil +} + +// NewObject creates a new permanode and returns its Object wrapper. +func (h *Host) NewObject() (*Object, error) { + pn, err := h.upload(schema.NewUnsignedPermanode()) + if err != nil { + return nil, err + } + // No need to do a describe query against it: we know it's + // empty (has no claims against it yet). + return &Object{h: h, pn: pn}, nil +} + +// An Object is wrapper around a permanode that the importer uses +// to synchronize. +type Object struct { + h *Host + pn blob.Ref // permanode ref + + mu sync.RWMutex + attr map[string][]string +} + +// PermanodeRef returns the permanode that this object wraps. +func (o *Object) PermanodeRef() blob.Ref { + return o.pn +} + +// Attr returns the object's attribute value for the provided attr, +// or the empty string if unset. To distinguish between unset, +// an empty string, or multiple attribute values, use Attrs. +func (o *Object) Attr(attr string) string { + o.mu.RLock() + defer o.mu.RUnlock() + if v := o.attr[attr]; len(v) > 0 { + return v[0] + } + return "" +} + +// Attrs returns the attribute values for the provided attr. +func (o *Object) Attrs(attr string) []string { + o.mu.RLock() + defer o.mu.RUnlock() + return o.attr[attr] +} + +// ForeachAttr runs fn for each of the object's attributes & values. +// There might be multiple values for the same attribute. +// The internal lock is held while running, so no mutations should be +// made or it will deadlock. +func (o *Object) ForeachAttr(fn func(key, value string)) { + o.mu.RLock() + defer o.mu.RUnlock() + for k, vv := range o.attr { + for _, v := range vv { + fn(k, v) + } + } +} + +// SetAttr sets the attribute key to value. +func (o *Object) SetAttr(key, value string) error { + if o.Attr(key) == value { + return nil + } + _, err := o.h.upload(schema.NewSetAttributeClaim(o.pn, key, value)) + if err != nil { + return err + } + o.mu.Lock() + defer o.mu.Unlock() + if o.attr == nil { + o.attr = make(map[string][]string) + } + o.attr[key] = []string{value} + return nil +} + +// SetAttrs sets multiple attributes. The provided keyval should be an +// even number of alternating key/value pairs to set. +func (o *Object) SetAttrs(keyval ...string) error { + _, err := o.SetAttrs2(keyval...) + return err +} + +// SetAttrs2 sets multiple attributes and returns whether there were +// any changes. The provided keyval should be an even number of +// alternating key/value pairs to set. +func (o *Object) SetAttrs2(keyval ...string) (changes bool, err error) { + if len(keyval)%2 == 1 { + panic("importer.SetAttrs: odd argument count") + } + + g := syncutil.Group{} + for i := 0; i < len(keyval); i += 2 { + key, val := keyval[i], keyval[i+1] + if val != o.Attr(key) { + changes = true + g.Go(func() error { + return o.SetAttr(key, val) + }) + } + } + return changes, g.Err() +} + +// SetAttrValues sets multi-valued attribute. +func (o *Object) SetAttrValues(key string, attrs []string) error { + exists := asSet(o.Attrs(key)) + actual := asSet(attrs) + o.mu.Lock() + defer o.mu.Unlock() + // add new values + for v := range actual { + if exists[v] { + delete(exists, v) + continue + } + _, err := o.h.upload(schema.NewAddAttributeClaim(o.pn, key, v)) + if err != nil { + return err + } + } + // delete unneeded values + for v := range exists { + _, err := o.h.upload(schema.NewDelAttributeClaim(o.pn, key, v)) + if err != nil { + return err + } + } + if o.attr == nil { + o.attr = make(map[string][]string) + } + o.attr[key] = attrs + return nil +} + +func asSet(elts []string) map[string]bool { + if len(elts) == 0 { + return nil + } + set := make(map[string]bool, len(elts)) + for _, elt := range elts { + set[elt] = true + } + return set +} + +// ChildPathObject returns (creating if necessary) the child object +// from the permanode o, given by the "camliPath:xxxx" attribute, +// where xxx is the provided path. +func (o *Object) ChildPathObject(path string) (*Object, error) { + return o.ChildPathObjectOrFunc(path, o.h.NewObject) +} + +// ChildPathObject returns the child object from the permanode o, +// given by the "camliPath:xxxx" attribute, where xxx is the provided +// path. If the path doesn't exist, the provided func should return an +// appropriate object. If the func fails, the return error is +// returned directly without any attempt to make a permanode. +func (o *Object) ChildPathObjectOrFunc(path string, fn func() (*Object, error)) (*Object, error) { + attrName := "camliPath:" + path + if v := o.Attr(attrName); v != "" { + br, ok := blob.Parse(v) + if !ok { + return nil, fmt.Errorf("invalid blobref %q already stored at camliPath %q", br, path) + } + return o.h.ObjectFromRef(br) + } + newObj, err := fn() + if err != nil { + return nil, err + } + if err := o.SetAttr(attrName, newObj.PermanodeRef().String()); err != nil { + return nil, err + } + return newObj, nil +} + +// ObjectFromRef returns the object given by the named permanode +func (h *Host) ObjectFromRef(permanodeRef blob.Ref) (*Object, error) { + res, err := h.search.Describe(&search.DescribeRequest{ + BlobRef: permanodeRef, + Depth: 1, + }) + if err != nil { + return nil, err + } + db, ok := res.Meta[permanodeRef.String()] + if !ok { + return nil, fmt.Errorf("permanode %v wasn't in Describe response", permanodeRef) + } + if db.Permanode == nil { + return nil, fmt.Errorf("permanode %v had no DescribedPermanode in Describe response", permanodeRef) + } + return &Object{ + h: h, + pn: permanodeRef, + attr: map[string][]string(db.Permanode.Attr), + }, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/importer_test.go b/vendor/github.com/camlistore/camlistore/pkg/importer/importer_test.go new file mode 100644 index 00000000..b80ce8f4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/importer_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package importer + +import ( + "testing" + + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/test" +) + +func init() { + Register("dummy1", TODOImporter) + Register("dummy2", TODOImporter) +} + +func TestStaticConfig(t *testing.T) { + ld := test.NewLoader() + h, err := newFromConfig(ld, jsonconfig.Obj{ + "dummy1": map[string]interface{}{ + "clientID": "id1", + "clientSecret": "secret1", + }, + "dummy2": map[string]interface{}{ + "clientSecret": "id2:secret2", + }, + }) + if err != nil { + t.Fatal(err) + } + host := h.(*Host) + if g, w := host.imp["dummy1"].clientID, "id1"; g != w { + t.Errorf("dummy1 id = %q; want %q", g, w) + } + if g, w := host.imp["dummy1"].clientSecret, "secret1"; g != w { + t.Errorf("dummy1 secret = %q; want %q", g, w) + } + if g, w := host.imp["dummy2"].clientID, "id2"; g != w { + t.Errorf("dummy2 id = %q; want %q", g, w) + } + if g, w := host.imp["dummy2"].clientSecret, "secret2"; g != w { + t.Errorf("dummy2 secret = %q; want %q", g, w) + } + + if _, err := newFromConfig(ld, jsonconfig.Obj{"dummy1": map[string]interface{}{"bogus": ""}}); err == nil { + t.Errorf("expected error from unknown key") + } + + if _, err := newFromConfig(ld, jsonconfig.Obj{"dummy1": map[string]interface{}{"clientSecret": "x"}}); err == nil { + t.Errorf("expected error from secret without id") + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/noop.go b/vendor/github.com/camlistore/camlistore/pkg/importer/noop.go new file mode 100644 index 00000000..bffc04cc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/noop.go @@ -0,0 +1,57 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package importer + +import ( + "errors" + "fmt" + "net/http" +) + +var TODOImporter Importer = todoImp{} + +type todoImp struct { + OAuth1 // for CallbackRequestAccount and CallbackURLParameters +} + +func (todoImp) NeedsAPIKey() bool { return false } + +func (todoImp) SupportsIncremental() bool { return false } + +func (todoImp) Run(*RunContext) error { + return errors.New("fake error from todo importer") +} + +func (todoImp) IsAccountReady(acctNode *Object) (ok bool, err error) { + return +} + +func (todoImp) SummarizeAccount(acctNode *Object) string { return "" } + +func (todoImp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *SetupContext) error { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprintf(w, "The Setup page for the TODO importer.\nnode = %v\ncallback = %s\naccount URL = %s\n", + ctx.AccountNode, + ctx.CallbackURL(), + "ctx.AccountURL()") + return nil +} + +func (todoImp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *SetupContext) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprintf(w, "The callback page for the TODO importer.\n") +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/oauth.go b/vendor/github.com/camlistore/camlistore/pkg/importer/oauth.go new file mode 100644 index 00000000..1353cd62 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/oauth.go @@ -0,0 +1,223 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package importer + +import ( + "errors" + "fmt" + "log" + "net/http" + "net/url" + "strings" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" + "camlistore.org/third_party/github.com/garyburd/go-oauth/oauth" +) + +const ( + AcctAttrTempToken = "oauthTempToken" + AcctAttrTempSecret = "oauthTempSecret" + AcctAttrAccessToken = "oauthAccessToken" + AcctAttrAccessTokenSecret = "oauthAccessTokenSecret" +) + +// OAuth1 provides methods that the importer implementations can use to +// help with OAuth authentication. +type OAuth1 struct{} + +func (OAuth1) CallbackRequestAccount(r *http.Request) (blob.Ref, error) { + acctRef, ok := blob.Parse(r.FormValue("acct")) + if !ok { + return blob.Ref{}, errors.New("missing 'acct=' blobref param") + } + return acctRef, nil +} + +func (OAuth1) CallbackURLParameters(acctRef blob.Ref) url.Values { + v := url.Values{} + v.Add("acct", acctRef.String()) + return v +} + +// OAuth2 provides methods that the importer implementations can use to +// help with OAuth2 authentication. +type OAuth2 struct{} + +func (OAuth2) CallbackRequestAccount(r *http.Request) (blob.Ref, error) { + state := r.FormValue("state") + if state == "" { + return blob.Ref{}, errors.New("missing 'state' parameter") + } + if !strings.HasPrefix(state, "acct:") { + return blob.Ref{}, errors.New("wrong 'state' parameter value, missing 'acct:' prefix.") + } + acctRef, ok := blob.Parse(strings.TrimPrefix(state, "acct:")) + if !ok { + return blob.Ref{}, errors.New("invalid account blobref in 'state' parameter") + } + return acctRef, nil +} + +func (OAuth2) CallbackURLParameters(acctRef blob.Ref) url.Values { + v := url.Values{} + v.Set("state", "acct:"+acctRef.String()) + return v +} + +// RedirectURL returns the redirect URI that imp should set in an oauth.Config +// for the authorization phase of OAuth2 authentication. +func (OAuth2) RedirectURL(imp Importer, ctx *SetupContext) string { + // We strip our callback URL of its query component, because the Redirect URI + // we send during authorization has to match exactly the registered redirect + // URI(s). This query component should be stored in the "state" paremeter instead. + // See http://tools.ietf.org/html/rfc6749#section-3.1.2.2 + fullCallback := ctx.CallbackURL() + queryPart := imp.CallbackURLParameters(ctx.AccountNode.PermanodeRef()) + if len(queryPart) == 0 { + log.Printf("WARNING: callback URL %q has no query component", fullCallback) + } + u, _ := url.Parse(fullCallback) + v := u.Query() + // remove query params in CallbackURLParameters + for k := range queryPart { + v.Del(k) + } + u.RawQuery = v.Encode() + return u.String() +} + +// RedirectState returns the "state" query parameter that should be used for the authorization +// phase of OAuth2 authentication. This parameter contains the query component of the redirection +// URI. See http://tools.ietf.org/html/rfc6749#section-3.1.2.2 +func (OAuth2) RedirectState(imp Importer, ctx *SetupContext) (state string, err error) { + m := imp.CallbackURLParameters(ctx.AccountNode.PermanodeRef()) + state = m.Get("state") + if state == "" { + return "", errors.New("\"state\" not found in callback parameters") + } + return state, nil +} + +// IsAccountReady returns whether the account has been properly configured +// - whether the user ID and access token has been stored in the given account node. +func (OAuth2) IsAccountReady(acctNode *Object) (ok bool, err error) { + if acctNode.Attr(AcctAttrUserID) != "" && + acctNode.Attr(AcctAttrAccessToken) != "" { + return true, nil + } + return false, nil +} + +// NeedsAPIKey returns whether the importer needs an API key - returns constant true. +func (OAuth2) NeedsAPIKey() bool { return true } + +// SummarizeAccount returns a summary for the account if it is configured, +// or an error string otherwise. +func (im OAuth2) SummarizeAccount(acct *Object) string { + ok, err := im.IsAccountReady(acct) + if err != nil { + return "" + } + if !ok { + return "" + } + if acct.Attr(AcctAttrGivenName) == "" && + acct.Attr(AcctAttrFamilyName) == "" { + return fmt.Sprintf("userid %s", acct.Attr(AcctAttrUserID)) + } + return fmt.Sprintf("userid %s (%s %s)", + acct.Attr(AcctAttrUserID), + acct.Attr(AcctAttrGivenName), + acct.Attr(AcctAttrFamilyName)) +} + +// OAuthContext wraps the OAuth1 state needed to perform API calls. +// +// It is used as a value type. +type OAuthContext struct { + Ctx *context.Context + Client *oauth.Client + Creds *oauth.Credentials +} + +// Get fetches through octx the resource defined by url and the values in form. +func (octx OAuthContext) Get(url string, form url.Values) (*http.Response, error) { + if octx.Creds == nil { + return nil, errors.New("No OAuth credentials. Not logged in?") + } + if octx.Client == nil { + return nil, errors.New("No OAuth client.") + } + res, err := octx.Client.Get(octx.Ctx.HTTPClient(), octx.Creds, url, form) + if err != nil { + return nil, fmt.Errorf("Error fetching %s: %v", url, err) + } + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Get request on %s failed with: %s", url, res.Status) + } + return res, nil +} + +// PopulateJSONFromURL makes a GET call at apiURL, using keyval as parameters of +// the associated form. The JSON response is decoded into result. +func (ctx OAuthContext) PopulateJSONFromURL(result interface{}, apiURL string, keyval ...string) error { + if len(keyval)%2 == 1 { + return errors.New("Incorrect number of keyval arguments. must be even.") + } + + form := url.Values{} + for i := 0; i < len(keyval); i += 2 { + form.Set(keyval[i], keyval[i+1]) + } + + hres, err := ctx.Get(apiURL, form) + if err != nil { + return err + } + err = httputil.DecodeJSON(hres, result) + if err != nil { + return fmt.Errorf("could not parse response for %s: %v", apiURL, err) + } + return err +} + +// OAuthURIs holds the URIs needed to initialize an OAuth 1 client. +type OAuthURIs struct { + TemporaryCredentialRequestURI string + ResourceOwnerAuthorizationURI string + TokenRequestURI string +} + +// NewOAuthClient returns an oauth Client configured with uris and the +// credentials obtained from ctx. +func (ctx *SetupContext) NewOAuthClient(uris OAuthURIs) (*oauth.Client, error) { + clientId, secret, err := ctx.Credentials() + if err != nil { + return nil, err + } + return &oauth.Client{ + TemporaryCredentialRequestURI: uris.TemporaryCredentialRequestURI, + ResourceOwnerAuthorizationURI: uris.ResourceOwnerAuthorizationURI, + TokenRequestURI: uris.TokenRequestURI, + Credentials: oauth.Credentials{ + Token: clientId, + Secret: secret, + }, + }, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/README b/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/README new file mode 100644 index 00000000..7c6a67d8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/README @@ -0,0 +1,43 @@ + +Picasa Importer +=============== + +This is a working Camlistore importer for Picasa. So far it can import +all photos but not their metadata. + +To use: + +1) Retrieve an api credential from a project of you from + https://console.developers.google.com/ + Select/create a project, then under APIs & auth / Credentials, create a new + web application client id. + +2a) Start the devcam server with picasakey flag: + $ devcam server -verbose -picasakey='Client ID:Client secret' + +2b) Place the Client ID and the Client secret in your (low-level) server-config.json: + + "/importer-picasa/": { + "handler": "importer-picasa", + "handlerArgs": { + "apiKey": "Client ID:Client secret" + } + }, + + and start your camlistore server. + +3) Navigate to http:///importer-picasa/start and authorize the app + to manage your Photos. + +4) Watch import progress on the command line (start devcam with -verbose flag). + + +TODO +---- + + * The used OAuth2 scope is for managing (read & modify) photos, but this + needs only read rights. Is a stricter scope available? + * The album's author name is not used yet, and the album's short name is needed. + * Picasa Web dumps a lot of metadata on us. Which would be usable? + +See https://camlistore.org/issue/391 diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/oa2_importers.go b/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/oa2_importers.go new file mode 100644 index 00000000..169db93c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/oa2_importers.go @@ -0,0 +1,218 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package picasa + +import ( + "fmt" + "log" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + + "camlistore.org/third_party/code.google.com/p/goauth2/oauth" +) + +const ( + // acctAttrOAuthToken stores `access + " " + refresh + " " + expiry` + // See encodeToken and decodeToken. + acctAttrOAuthToken = "oauthToken" +) + +// extendedOAuth2 provides implementation for some common importer methods regarding authentication. +// +// The oauthConfig is used in the authentications - think Scope and AuthURL. +// +// The getUserInfo function (if provided) should return the +// user ID, first name and last name of the user. +type extendedOAuth2 struct { + importer.OAuth2 + oauthConfig oauth.Config + getUserInfo func(ctx *context.Context) (*userInfo, error) +} + +// newExtendedOAuth2 returns a default implementation of +// some common methods for OAuth2-based importers. +func newExtendedOAuth2(oauthConfig oauth.Config, + getUserInfo func(ctx *context.Context) (*userInfo, error), +) extendedOAuth2 { + return extendedOAuth2{oauthConfig: oauthConfig, getUserInfo: getUserInfo} +} + +func (extendedOAuth2) IsAccountReady(acctNode *importer.Object) (ok bool, err error) { + if acctNode.Attr(importer.AcctAttrUserID) != "" && acctNode.Attr(acctAttrOAuthToken) != "" { + return true, nil + } + return false, nil +} + +func (im extendedOAuth2) SummarizeAccount(acct *importer.Object) string { + ok, err := im.IsAccountReady(acct) + if err != nil || !ok { + return "" + } + if acct.Attr(importer.AcctAttrGivenName) == "" && acct.Attr(importer.AcctAttrFamilyName) == "" { + return fmt.Sprintf("userid %s", acct.Attr(importer.AcctAttrUserID)) + } + return fmt.Sprintf("userid %s (%s %s)", + acct.Attr(importer.AcctAttrUserID), + acct.Attr(importer.AcctAttrGivenName), + acct.Attr(importer.AcctAttrFamilyName)) +} + +func (im extendedOAuth2) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error { + oauthConfig, err := im.auth(ctx) + if err == nil { + // we will get back this with the token, so use it for preserving account info + state := "acct:" + ctx.AccountNode.PermanodeRef().String() + http.Redirect(w, r, oauthConfig.AuthCodeURL(state), 302) + } + return err +} + +// CallbackURLParameters returns the needed callback parameters - empty for Google Picasa. +func (im extendedOAuth2) CallbackURLParameters(acctRef blob.Ref) url.Values { + return url.Values{} +} + +// notOAuthTransport returns c's Transport, or its underlying transport if c.Transport +// is an OAuth Transport. +func notOAuthTransport(c *http.Client) (tr http.RoundTripper) { + tr = c.Transport + if otr, ok := tr.(*oauth.Transport); ok { + tr = otr.Transport + } + return +} + +func (im extendedOAuth2) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) { + if im.getUserInfo == nil { + panic("No getUserInfo is provided, don't use the default ServeCallback!") + } + + oauthConfig, err := im.auth(ctx) + if err != nil { + httputil.ServeError(w, r, fmt.Errorf("Error getting oauth config: %v", err)) + return + } + + if r.Method != "GET" { + http.Error(w, "Expected a GET", 400) + return + } + code := r.FormValue("code") + if code == "" { + http.Error(w, "Expected a code", 400) + return + } + + // picago calls take an *http.Client, so we need to provide one which already + // has a transport set up correctly wrt to authentication. In particular, it + // needs to have the access token that is obtained during Exchange. + transport := &oauth.Transport{ + Config: oauthConfig, + Transport: notOAuthTransport(ctx.HTTPClient()), + } + token, err := transport.Exchange(code) + log.Printf("Token = %#v, error %v", token, err) + if err != nil { + log.Printf("Token Exchange error: %v", err) + httputil.ServeError(w, r, fmt.Errorf("token exchange error: %v", err)) + return + } + + picagoCtx := ctx.Context.New(context.WithHTTPClient(transport.Client())) + defer picagoCtx.Cancel() + + userInfo, err := im.getUserInfo(picagoCtx) + if err != nil { + log.Printf("Couldn't get username: %v", err) + httputil.ServeError(w, r, fmt.Errorf("can't get username: %v", err)) + return + } + + if err := ctx.AccountNode.SetAttrs( + importer.AcctAttrUserID, userInfo.ID, + importer.AcctAttrGivenName, userInfo.FirstName, + importer.AcctAttrFamilyName, userInfo.LastName, + acctAttrOAuthToken, encodeToken(token), + ); err != nil { + httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err)) + return + } + http.Redirect(w, r, ctx.AccountURL(), http.StatusFound) +} + +// encodeToken encodes the oauth.Token as +// AccessToken + " " + RefreshToken + " " + Expiry.Unix() +func encodeToken(token *oauth.Token) string { + if token == nil { + return "" + } + var seconds int64 + if !token.Expiry.IsZero() { + seconds = token.Expiry.Unix() + } + return token.AccessToken + " " + token.RefreshToken + " " + strconv.FormatInt(seconds, 10) +} + +// decodeToken parses an access token, refresh token, and optional +// expiry unix timestamp separated by spaces into an oauth.Token. +// It returns as much as it can. +func decodeToken(encoded string) oauth.Token { + var t oauth.Token + f := strings.Fields(encoded) + if len(f) > 0 { + t.AccessToken = f[0] + } + if len(f) > 1 { + t.RefreshToken = f[1] + } + if len(f) > 2 && f[2] != "0" { + sec, err := strconv.ParseInt(f[2], 10, 64) + if err == nil { + t.Expiry = time.Unix(sec, 0) + } + } + return t +} + +func (im extendedOAuth2) auth(ctx *importer.SetupContext) (*oauth.Config, error) { + clientId, secret, err := ctx.Credentials() + if err != nil { + return nil, err + } + conf := im.oauthConfig + conf.ClientId, conf.ClientSecret, conf.RedirectURL = clientId, secret, ctx.CallbackURL() + return &conf, nil +} + +// userInfo contains basic information about the identity of the imported +// account owner. Its use is discouraged as it might be refactored soon. +// Importer implementations should rather make their own dedicated type for +// now. +type userInfo struct { + ID string + FirstName string + LastName string +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/picasa.go b/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/picasa.go new file mode 100644 index 00000000..dbe2369e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/picasa.go @@ -0,0 +1,468 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package picasa implements an importer for picasa.com accounts. +package picasa + +// TODO: removing camliPath from gallery permanode when pic deleted from gallery + +import ( + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "sync" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/context" + "camlistore.org/pkg/importer" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/schema/nodeattr" + "camlistore.org/pkg/search" + "camlistore.org/pkg/syncutil" + + "camlistore.org/third_party/code.google.com/p/goauth2/oauth" + "camlistore.org/third_party/github.com/tgulacsi/picago" +) + +const ( + apiURL = "https://api.picasa.com/v2/" + authURL = "https://accounts.google.com/o/oauth2/auth" + tokenURL = "https://accounts.google.com/o/oauth2/token" + scopeURL = "https://picasaweb.google.com/data/" + + // runCompleteVersion is a cache-busting version number of the + // importer code. It should be incremented whenever the + // behavior of this importer is updated enough to warrant a + // complete run. Otherwise, if the importer runs to + // completion, this version number is recorded on the account + // permanode and subsequent importers can stop early. + runCompleteVersion = "4" + + // attrPicasaId is used for both picasa photo IDs and gallery IDs. + attrPicasaId = "picasaId" +) + +var _ importer.ImporterSetupHTMLer = imp{} + +type imp struct { + extendedOAuth2 +} + +func (imp) SupportsIncremental() bool { return true } + +var baseOAuthConfig = oauth.Config{ + AuthURL: authURL, + TokenURL: tokenURL, + Scope: scopeURL, + + // AccessType needs to be "offline", as the user is not here all the time; + // ApprovalPrompt needs to be "force" to be able to get a RefreshToken + // everytime, even for Re-logins, too. + // + // Source: https://developers.google.com/youtube/v3/guides/authentication#server-side-apps + AccessType: "offline", + ApprovalPrompt: "force", +} + +func init() { + importer.Register("picasa", imp{ + newExtendedOAuth2( + baseOAuthConfig, + func(ctx *context.Context) (*userInfo, error) { + u, err := picago.GetUser(ctx.HTTPClient(), "default") + if err != nil { + return nil, err + } + firstName, lastName := u.Name, "" + i := strings.LastIndex(u.Name, " ") + if i >= 0 { + firstName, lastName = u.Name[:i], u.Name[i+1:] + } + return &userInfo{ + ID: u.ID, + FirstName: firstName, + LastName: lastName, + }, nil + }), + }) +} + +func (imp) AccountSetupHTML(host *importer.Host) string { + // Picasa doesn't allow a path in the origin. Remove it. + origin := host.ImporterBaseURL() + if u, err := url.Parse(origin); err == nil { + u.Path = "" + origin = u.String() + } + + callback := host.ImporterBaseURL() + "picasa/callback" + return fmt.Sprintf(` +

    Configuring Picasa

    +

    Visit https://console.developers.google.com/ +and click "Create Project".

    +

    Then under "APIs & Auth" in the left sidebar, click on "Credentials", then click the button "Create new Client ID".

    +

    Use the following settings:

    +
      +
    • Web application
    • +
    • Authorized JavaScript origins: %s
    • +
    • Authorized Redirect URI: %s
    • +
    +

    Click "Create Client ID". Copy the "Client ID" and "Client Secret" into the boxes above.

    +`, origin, callback) +} + +// A run is our state for a given run of the importer. +type run struct { + *importer.RunContext + incremental bool // whether we've completed a run in the past + photoGate *syncutil.Gate + + mu sync.Mutex // guards anyErr + anyErr bool +} + +func (r *run) errorf(format string, args ...interface{}) { + log.Printf(format, args...) + r.mu.Lock() + defer r.mu.Unlock() + r.anyErr = true +} + +var forceFullImport, _ = strconv.ParseBool(os.Getenv("CAMLI_PICASA_FULL_IMPORT")) + +func (imp) Run(ctx *importer.RunContext) error { + clientId, secret, err := ctx.Credentials() + if err != nil { + return err + } + acctNode := ctx.AccountNode() + ocfg := baseOAuthConfig + ocfg.ClientId, ocfg.ClientSecret = clientId, secret + token := decodeToken(acctNode.Attr(acctAttrOAuthToken)) + transport := &oauth.Transport{ + Config: &ocfg, + Token: &token, + Transport: notOAuthTransport(ctx.HTTPClient()), + } + ctx.Context = ctx.Context.New(context.WithHTTPClient(transport.Client())) + + root := ctx.RootNode() + if root.Attr(nodeattr.Title) == "" { + if err := root.SetAttr(nodeattr.Title, + fmt.Sprintf("%s %s - Google/Picasa Photos", + acctNode.Attr(importer.AcctAttrGivenName), + acctNode.Attr(importer.AcctAttrFamilyName))); err != nil { + return err + } + } + + r := &run{ + RunContext: ctx, + incremental: !forceFullImport && acctNode.Attr(importer.AcctAttrCompletedVersion) == runCompleteVersion, + photoGate: syncutil.NewGate(3), + } + if err := r.importAlbums(); err != nil { + return err + } + + r.mu.Lock() + anyErr := r.anyErr + r.mu.Unlock() + if !anyErr { + if err := acctNode.SetAttrs(importer.AcctAttrCompletedVersion, runCompleteVersion); err != nil { + return err + } + } + + return nil +} + +func (r *run) importAlbums() error { + albums, err := picago.GetAlbums(r.HTTPClient(), "default") + if err != nil { + return fmt.Errorf("importAlbums: error listing albums: %v", err) + } + albumsNode, err := r.getTopLevelNode("albums", "Albums") + for _, album := range albums { + if r.Context.IsCanceled() { + return context.ErrCanceled + } + if err := r.importAlbum(albumsNode, album); err != nil { + return fmt.Errorf("picasa importer: error importing album %s: %v", album, err) + } + } + return nil +} + +func (r *run) importAlbum(albumsNode *importer.Object, album picago.Album) (ret error) { + if album.ID == "" { + return errors.New("album has no ID") + } + albumNode, err := albumsNode.ChildPathObject(album.ID) + if err != nil { + return fmt.Errorf("importAlbum: error listing album: %v", err) + } + + dateMod := schema.RFC3339FromTime(album.Updated) + + // Data reference: https://developers.google.com/picasa-web/docs/2.0/reference + // TODO(tgulacsi): add more album info + changes, err := albumNode.SetAttrs2( + attrPicasaId, album.ID, + nodeattr.Type, "picasaweb.google.com:album", + nodeattr.Title, album.Title, + nodeattr.DatePublished, schema.RFC3339FromTime(album.Published), + nodeattr.LocationText, album.Location, + nodeattr.Description, album.Description, + nodeattr.URL, album.URL, + ) + if err != nil { + return fmt.Errorf("error setting album attributes: %v", err) + } + if !changes && r.incremental && albumNode.Attr(nodeattr.DateModified) == dateMod { + return nil + } + defer func() { + // Don't update DateModified on the album node until + // we've successfully imported all the photos. + if ret == nil { + ret = albumNode.SetAttr(nodeattr.DateModified, dateMod) + } + }() + + log.Printf("Importing album %v: %v/%v (published %v, updated %v)", album.ID, album.Name, album.Title, album.Published, album.Updated) + + // TODO(bradfitz): GetPhotos does multiple HTTP requests to + // return a slice of all photos. My "InstantUpload/Auto + // Backup" album has 6678 photos (and growing) and this + // currently takes like 40 seconds. Fix. + photos, err := picago.GetPhotos(r.HTTPClient(), "default", album.ID) + if err != nil { + return err + } + + log.Printf("Importing %d photos from album %q (%s)", len(photos), albumNode.Attr(nodeattr.Title), + albumNode.PermanodeRef()) + + var grp syncutil.Group + for i := range photos { + if r.Context.IsCanceled() { + return context.ErrCanceled + } + photo := photos[i] + r.photoGate.Start() + grp.Go(func() error { + defer r.photoGate.Done() + return r.updatePhotoInAlbum(albumNode, photo) + }) + } + return grp.Err() +} + +func (r *run) updatePhotoInAlbum(albumNode *importer.Object, photo picago.Photo) (ret error) { + if photo.ID == "" { + return errors.New("photo has no ID") + } + + getMediaBytes := func() (io.ReadCloser, error) { + log.Printf("Importing media from %v", photo.URL) + resp, err := r.HTTPClient().Get(photo.URL) + if err != nil { + return nil, fmt.Errorf("importing photo %s: %v", photo.ID, err) + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("importing photo %s: status code = %d", photo.ID, resp.StatusCode) + } + return resp.Body, nil + } + + var fileRefStr string + idFilename := photo.ID + "-" + photo.Filename + photoNode, err := albumNode.ChildPathObjectOrFunc(idFilename, func() (*importer.Object, error) { + h := blob.NewHash() + rc, err := getMediaBytes() + if err != nil { + return nil, err + } + fileRef, err := schema.WriteFileFromReader(r.Host.Target(), photo.Filename, io.TeeReader(rc, h)) + if err != nil { + return nil, err + } + fileRefStr = fileRef.String() + wholeRef := blob.RefFromHash(h) + if pn, err := findExistingPermanode(r.Host.Searcher(), wholeRef); err == nil { + return r.Host.ObjectFromRef(pn) + } + return r.Host.NewObject() + }) + if err != nil { + return err + } + + const attrMediaURL = "picasaMediaURL" + if fileRefStr == "" { + fileRefStr = photoNode.Attr(nodeattr.CamliContent) + // Only re-download the source photo if its URL has changed. + // Empirically this seems to work: cropping a photo in the + // photos.google.com UI causes its URL to change. And it makes + // sense, looking at the ugliness of the URLs with all their + // encoded/signed state. + if !mediaURLsEqual(photoNode.Attr(attrMediaURL), photo.URL) { + rc, err := getMediaBytes() + if err != nil { + return err + } + fileRef, err := schema.WriteFileFromReader(r.Host.Target(), photo.Filename, rc) + rc.Close() + if err != nil { + return err + } + fileRefStr = fileRef.String() + } + } + + title := strings.TrimSpace(photo.Description) + if strings.Contains(title, "\n") { + title = title[:strings.Index(title, "\n")] + } + if title == "" && schema.IsInterestingTitle(photo.Filename) { + title = photo.Filename + } + + // TODO(tgulacsi): add more attrs (comments ?) + // for names, see http://schema.org/ImageObject and http://schema.org/CreativeWork + attrs := []string{ + nodeattr.CamliContent, fileRefStr, + attrPicasaId, photo.ID, + nodeattr.Title, title, + nodeattr.Description, photo.Description, + nodeattr.LocationText, photo.Location, + nodeattr.DateModified, schema.RFC3339FromTime(photo.Updated), + nodeattr.DatePublished, schema.RFC3339FromTime(photo.Published), + nodeattr.URL, photo.PageURL, + } + if photo.Latitude != 0 || photo.Longitude != 0 { + attrs = append(attrs, + nodeattr.Latitude, fmt.Sprintf("%f", photo.Latitude), + nodeattr.Longitude, fmt.Sprintf("%f", photo.Longitude), + ) + } + if err := photoNode.SetAttrs(attrs...); err != nil { + return err + } + if err := photoNode.SetAttrValues("tag", photo.Keywords); err != nil { + return err + } + if photo.Position > 0 { + if err := albumNode.SetAttr( + nodeattr.CamliPathOrderColon+strconv.Itoa(photo.Position-1), + photoNode.PermanodeRef().String()); err != nil { + return err + } + } + + // Do this last, after we're sure the "camliContent" attribute + // has been saved successfully, because this is the one that + // causes us to do it again in the future or not. + if err := photoNode.SetAttrs(attrMediaURL, photo.URL); err != nil { + return err + } + return nil +} + +func (r *run) getTopLevelNode(path string, title string) (*importer.Object, error) { + childObject, err := r.RootNode().ChildPathObject(path) + if err != nil { + return nil, err + } + + if err := childObject.SetAttr(nodeattr.Title, title); err != nil { + return nil, err + } + return childObject, nil +} + +var sensitiveAttrs = []string{ + nodeattr.Type, + attrPicasaId, + nodeattr.Title, + nodeattr.DateModified, + nodeattr.DatePublished, + nodeattr.Latitude, + nodeattr.Longitude, + nodeattr.Description, +} + +// findExistingPermanode finds an existing permanode that has a +// camliContent pointing to a file with the provided wholeRef and +// doesn't have any conflicting attributes that would prevent the +// picasa importer from re-using that permanode for its own use. +func findExistingPermanode(qs search.QueryDescriber, wholeRef blob.Ref) (pn blob.Ref, err error) { + res, err := qs.Query(&search.SearchQuery{ + Constraint: &search.Constraint{ + Permanode: &search.PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &search.Constraint{ + File: &search.FileConstraint{ + WholeRef: wholeRef, + }, + }, + }, + }, + Describe: &search.DescribeRequest{ + Depth: 1, + }, + }) + if err != nil { + return + } + if res.Describe == nil { + return pn, os.ErrNotExist + } +Res: + for _, resBlob := range res.Blobs { + br := resBlob.Blob + desBlob, ok := res.Describe.Meta[br.String()] + if !ok || desBlob.Permanode == nil { + continue + } + attrs := desBlob.Permanode.Attr + for _, attr := range sensitiveAttrs { + if attrs.Get(attr) != "" { + continue Res + } + } + return br, nil + } + return pn, os.ErrNotExist +} + +func mediaURLsEqual(a, b string) bool { + const sub = ".googleusercontent.com/" + ai := strings.Index(a, sub) + bi := strings.Index(b, sub) + if ai >= 0 && bi >= 0 { + return a[ai:] == b[bi:] + } + return a == b +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/picasa_test.go b/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/picasa_test.go new file mode 100644 index 00000000..50cc6f00 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/picasa_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package picasa + +import ( + "net/http" + "testing" + + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" + + "camlistore.org/third_party/github.com/tgulacsi/picago" +) + +func TestGetUserId(t *testing.T) { + userID := "11047045264" + responder := httputil.FileResponder("testdata/users-me-res.xml") + ctx := context.New(context.WithHTTPClient(&http.Client{ + Transport: httputil.NewFakeTransport(map[string]func() *http.Response{ + "https://picasaweb.google.com/data/feed/api/user/default/contacts?kind=user": responder, + "https://picasaweb.google.com/data/feed/api/user/" + userID + "/contacts?kind=user": responder, + }), + })) + defer ctx.Cancel() + inf, err := picago.GetUser(ctx.HTTPClient(), "default") + if err != nil { + t.Fatal(err) + } + want := picago.User{ + ID: userID, + URI: "https://picasaweb.google.com/" + userID, + Name: "Tamás Gulácsi", + Thumbnail: "https://lh4.googleusercontent.com/-qqove344/AAAAAAAAAAI/AAAAAAABcbg/TXl3f2K9dzI/s64-c/11047045264.jpg", + } + if inf != want { + t.Errorf("user info = %+v; want %+v", inf, want) + } +} + +func TestMediaURLsEqual(t *testing.T) { + if !mediaURLsEqual("https://lh1.googleusercontent.com/foo.jpg", "https://lh100.googleusercontent.com/foo.jpg") { + t.Fatal("want equal") + } + if mediaURLsEqual("https://foo.com/foo.jpg", "https://bar.com/foo.jpg") { + t.Fatal("want not equal") + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/testdata.go b/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/testdata.go new file mode 100644 index 00000000..7839ce5d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/testdata.go @@ -0,0 +1,239 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package picasa + +import ( + "encoding/xml" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + "camlistore.org/pkg/osutil" + + "camlistore.org/third_party/github.com/tgulacsi/picago" +) + +var _ importer.TestDataMaker = (*imp)(nil) + +func (im *imp) SetTestAccount(acctNode *importer.Object) error { + // TODO(mpl): refactor with twitter + return acctNode.SetAttrs( + importer.AcctAttrAccessToken, "fakeAccessToken", + importer.AcctAttrAccessTokenSecret, "fakeAccessSecret", + importer.AcctAttrUserID, "fakeUserId", + importer.AcctAttrName, "fakeName", + importer.AcctAttrUserName, "fakeScreenName", + ) +} + +func (im *imp) MakeTestData() http.RoundTripper { + + const ( + apiURL = "https://picasaweb.google.com/data/feed/api" + nAlbums = 10 // Arbitrary number of albums generated. + nEntries = 3 // number of albums or photos returned in the feed at each call. + defaultUserId = "default" + ) + + albumsListCached := make(map[int]string) + okHeader := `HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 + +` + + responses := make(map[string]func() *http.Response) + + // register the get albums list calls + for i := 1; i < nAlbums+1; i += nEntries { + url := fmt.Sprintf("%s/user/%s?start-index=%d", apiURL, defaultUserId, i) + response := okHeader + fakeAlbumsList(i, nAlbums, nEntries, albumsListCached) + responses[url] = httputil.StaticResponder(response) + } + + // register the get album calls + for i := 1; i < nAlbums+1; i++ { + albumId := blob.RefFromString(fmt.Sprintf("Album %d", i)).DigestPrefix(10) + for j := 1; j < i+1; j += nEntries { + url := fmt.Sprintf("%s/user/%s/albumid/%s?imgmax=d&start-index=%d", apiURL, defaultUserId, albumId, j) + // Using i as nTotal argument means album N will have N photos in it. + response := okHeader + fakePhotosList(j, i, nEntries) + responses[url] = httputil.StaticResponder(response) + } + } + + // register the photo download calls + pudgyPic := fakePhoto() + photoURL1 := "https://camlistore.org/pic/pudgy1.png" + photoURL2 := "https://camlistore.org/pic/pudgy2.png" + responses[photoURL1] = httputil.FileResponder(pudgyPic) + responses[photoURL2] = httputil.FileResponder(pudgyPic) + + return httputil.NewFakeTransport(responses) +} + +// fakeAlbumsList returns an xml feed of albums. The feed starts at index, and +// ends at index + nEntries (exclusive), or at nTotal (inclusive), whichever is the +// lowest. +func fakeAlbumsList(index, nTotal, nEntries int, cached map[int]string) string { + if cl, ok := cached[index]; ok { + return cl + } + + max := index + nEntries + if max > nTotal+1 { + max = nTotal + 1 + } + var entries []picago.Entry + for i := index; i < max; i++ { + entries = append(entries, fakeAlbum(i)) + } + atom := &picago.Atom{ + TotalResults: nTotal, + Entries: entries, + } + + feed, err := xml.MarshalIndent(atom, "", " ") + if err != nil { + log.Fatalf("%v", err) + } + cached[index] = string(feed) + return cached[index] +} + +func fakeAlbum(counter int) picago.Entry { + author := picago.Author{ + Name: "fakeAuthorName", + } + media := &picago.Media{ + Description: "fakeAlbumDescription", + Keywords: "fakeKeyword1,fakeKeyword2", + } + title := fmt.Sprintf("Album %d", counter) + year := time.Hour * 24 * 365 + month := year / 12 + return picago.Entry{ + ID: blob.RefFromString(title).DigestPrefix(10), + Published: time.Now().Add(-time.Duration(counter) * year), + Updated: time.Now().Add(-time.Duration(counter) * month), + Name: "fakeAlbumName", + Title: title, + Summary: "fakeAlbumSummary", + Location: "fakeAlbumLocation", + Author: author, + Media: media, + } +} + +// fakePhotosList returns an xml feed of an album's photos. The feed starts at +// index, and ends at index + nEntries (exclusive), or at nTotal (inclusive), +// whichever is the lowest. +func fakePhotosList(index, nTotal, nEntries int) string { + max := index + nEntries + if max > nTotal+1 { + max = nTotal + 1 + } + var entries []picago.Entry + for i := index; i < max; i++ { + entries = append(entries, fakePhotoEntry(i, nTotal)) + } + atom := &picago.Atom{ + NumPhotos: nTotal, + Entries: entries, + } + + feed, err := xml.MarshalIndent(atom, "", " ") + if err != nil { + log.Fatalf("%v", err) + } + return string(feed) +} + +func fakePhotoEntry(photoNbr int, albumNbr int) picago.Entry { + var content picago.EntryContent + if photoNbr%2 == 0 { + content = picago.EntryContent{ + URL: "https://camlistore.org/pic/pudgy1.png", + Type: "image/png", + } + } + var point string + if photoNbr%3 == 0 { + point = "37.7447124 -122.4341914" + } else { + point = "45.1822842 5.7141854" + } + mediaContent := picago.MediaContent{ + URL: "https://camlistore.org/pic/pudgy2.png", + Type: "image/png", + } + media := &picago.Media{ + Title: "fakePhotoTitle", + Description: "fakePhotoDescription", + Keywords: "fakeKeyword1,fakeKeyword2", + Content: []picago.MediaContent{mediaContent}, + } + // to be consistent, all the pics times should be anterior to their respective albums times. whatever. + day := time.Hour * 24 + year := day * 365 + created := time.Now().Add(-time.Duration(photoNbr) * year) + published := created.Add(day) + updated := published.Add(day) + + exif := &picago.Exif{ + FStop: 7.7, + Make: "whatisthis?", // not obvious to me, needs doc in picago + Model: "potato", + Exposure: 7.7, + Flash: false, + FocalLength: 7.7, + ISO: 100, + Timestamp: created.Unix(), + UID: "whatisthis?", // not obvious to me, needs doc in picago + } + + title := fmt.Sprintf("Photo %d of album %d", photoNbr, albumNbr) + return picago.Entry{ + ID: blob.RefFromString(title).DigestPrefix(10), + Exif: exif, + Summary: "fakePhotoSummary", + Title: title, + Location: "fakePhotoLocation", + Published: published, + Updated: updated, + Media: media, + Point: point, + Content: content, + } +} + +// TODO(mpl): refactor with twitter +func fakePhoto() string { + camliDir, err := osutil.GoPackagePath("camlistore.org") + if err == os.ErrNotExist { + log.Fatal("Directory \"camlistore.org\" not found under GOPATH/src; are you not running with devcam?") + } + if err != nil { + log.Fatalf("Error searching for \"camlistore.org\" under GOPATH: %v", err) + } + return filepath.Join(camliDir, filepath.FromSlash("third_party/glitch/npc_piggy__x1_walk_png_1354829432.png")) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/testdata/users-me-res.xml b/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/testdata/users-me-res.xml new file mode 100644 index 00000000..c313dc69 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/picasa/testdata/users-me-res.xml @@ -0,0 +1,69 @@ + + + https://picasaweb.google.com/data/feed/api/user/default/contacts + 2014-04-14T20:44:46.102Z + + 11047045264 + + https://lh4.googleusercontent.com/-qqMg344/AAAAAAAAAAI/AAAAAAABcbg/TXl3f2K9dzI/s64-c/11047045264.jpg + + + + + + Tamás Gulácsi + https://picasaweb.google.com/11047045264 + + Picasaweb + 2 + 1 + 500 + 110415264 + Tamás + https://lh4.googleusercontent.com/-qqove344/AAAAAAAAAAI/AAAAAAABcbg/TXl3f2K9dzI/s64-c/11047045264.jpg + 38654705664 + 17032238989 + 2000 + + https://picasaweb.google.com/data/entry/api/user/110415264/contacts/106948621299403 + 2013-12-17T18:40:10.000Z + 2014-04-14T20:44:46.102Z + + 106948621299403 + + + + + + + Petra + https://picasaweb.google.com/106948621299403 + + 106948621299403 + Petra + https://lh5.googleusercontent.com/-CiCHgcc/AAAAAAAAAAI/AAAAAAAAAAA/moXXlYbkPsk/s64-c/106948621299403.jpg + true + true + + + https://picasaweb.google.com/data/entry/api/user/11047045264/contacts/1163008697 + 1970-01-01T00:00:00.000Z + 2014-04-14T20:44:46.102Z + + 1163008697 + + + + + + + Viktória + https://picasaweb.google.com/1163008697 + + 1163008697 + Viktória + https://lh3.googleusercontent.com/-HmzwFI/AAAAAAAAAAI/AAAAAAAAAAA/g7DJ3IovKMY/s64-c/1163008697.jpg + true + true + + diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/pinboard/pinboard.go b/vendor/github.com/camlistore/camlistore/pkg/importer/pinboard/pinboard.go new file mode 100644 index 00000000..a88d9516 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/pinboard/pinboard.go @@ -0,0 +1,346 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package pinboard imports pinboard.in posts. + +This package uses the v1 api documented here: https://pinboard.in/api. + +Note that the api document seems to use 'post' and 'bookmark' +interchangeably. We use 'post' everywhere in this code. + +Posts in pinboard are mutable; they can be edited or deleted. + +We handle edited posts by always reimporting everything and rewriting +any nodes. Perhaps this would become more efficient if we would first +compare the meta tag from pinboard to the meta tag we have stored to +only write the node if there are changes. + +We don't handle deleted posts. One possible approach for this would +be to import everything under a new permanode, then once it is +successful, swap the new permanode and the posts node (note: I don't +think I really understand the data model here, so this is sort of +gibberish). + +I have exchanged email with Maciej Ceglowski of pinboard, who may in +the future provide an api that lets us query what has changed. We +might want to switch to that when available to make the import process +more light-weight. +*/ +package pinboard + +import ( + "encoding/json" + "fmt" + "html/template" + "io/ioutil" + "log" + "net/http" + "strings" + "time" + + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/schema/nodeattr" + "camlistore.org/pkg/syncutil" +) + +func init() { + importer.Register("pinboard", imp{}) +} + +const ( + fetchUrl = "https://api.pinboard.in/v1/posts/all?auth_token=%s&format=json&results=%d&todt=%s" + + timeFormat = "2006-01-02T15:04:05Z" + + // pauseInterval is the time we wait between fetching batches (for + // a particualar user). This time is pretty long, but is what the + // api documentation suggests. + pauseInterval = 5 * time.Minute + + // batchLimit is the maximum number of posts we will fetch in one batch. + batchLimit = 10000 + + attrAuthToken = "authToken" + + // StatusTooManyRequests is the http status code returned by + // pinboard servers if we have made too many requests for a + // particular user. If we receive this status code, we should + // double the amount of time we wait before trying agian. + StatusTooManyRequests = 429 +) + +// We expect :. Sometimes pinboard calls this an +// auth token and sometimes they call it an api token. +func extractUsername(authToken string) string { + split := strings.SplitN(authToken, ":", 2) + if len(split) == 2 { + return split[0] + } else { + return "" + } +} + +type imp struct { + importer.OAuth1 // for CallbackRequestAccount and CallbackURLParameters +} + +func (imp) SupportsIncremental() bool { return false } + +func (imp) NeedsAPIKey() bool { return false } + +func (imp) IsAccountReady(acct *importer.Object) (ready bool, err error) { + ready = acct.Attr(attrAuthToken) != "" + return ready, nil +} + +func (im imp) SummarizeAccount(acct *importer.Object) string { + ok, err := im.IsAccountReady(acct) + if err != nil { + return "Not configured; error = " + err.Error() + } + if !ok { + return "Not configured" + } + return fmt.Sprintf("Pinboard account for %s", extractUsername(acct.Attr(attrAuthToken))) +} + +func (imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error { + return tmpl.ExecuteTemplate(w, "serveSetup", ctx) +} + +var tmpl = template.Must(template.New("root").Parse(` +{{define "serveSetup"}} +

    Configuring Pinboad Account

    +
    + + + + +
    API token (You can find it here)
    +
    +{{end}} +`)) + +func (im imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) { + t := r.FormValue("apiToken") + if t == "" { + http.Error(w, "Expected an API Token", 400) + return + } + if extractUsername(t) == "" { + errText := fmt.Sprintf("Unable to parse %q as an api token. We expect :", t) + http.Error(w, errText, 400) + } + if err := ctx.AccountNode.SetAttrs( + attrAuthToken, t, + ); err != nil { + httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err)) + return + } + http.Redirect(w, r, ctx.AccountURL(), http.StatusFound) +} + +func (im imp) Run(ctx *importer.RunContext) (err error) { + log.Printf("pinboard: Running importer.") + r := &run{ + RunContext: ctx, + im: im, + postGate: syncutil.NewGate(3), + nextCursor: time.Now().Format(timeFormat), + nextAfter: time.Now(), + lastPause: pauseInterval, + } + _, err = r.importPosts() + log.Printf("pinboard: Importer returned %v.", err) + return +} + +func (im imp) ServeHTTP(w http.ResponseWriter, r *http.Request) { + httputil.BadRequestError(w, "Unexpected path: %s", r.URL.Path) +} + +type run struct { + *importer.RunContext + im imp + postGate *syncutil.Gate + + // Return only bookmarks created before this time (exclusive bound) + nextCursor string + + // We should not fetch the next batch until this time (exclusive bound) + nextAfter time.Time + + // This gets set to pauseInterval at the beginning of each run and + // after each successful fetch. Every time we get a 429 back from + // pinboard, it gets doubled. It will be used to calculate the + // next time we fetch from pinboard. + lastPause time.Duration +} + +func (r *run) getPostsNode() (*importer.Object, error) { + username := extractUsername(r.AccountNode().Attr(attrAuthToken)) + root := r.RootNode() + rootTitle := fmt.Sprintf("%s's Pinboard Account", username) + log.Printf("pinboard: root title = %q; want %q.", root.Attr(nodeattr.Title), rootTitle) + if err := root.SetAttr(nodeattr.Title, rootTitle); err != nil { + return nil, err + } + obj, err := root.ChildPathObject("posts") + if err != nil { + return nil, err + } + title := fmt.Sprintf("%s's Posts", username) + return obj, obj.SetAttr(nodeattr.Title, title) +} + +func (r *run) importPosts() (*importer.Object, error) { + authToken := r.AccountNode().Attr(attrAuthToken) + parent, err := r.getPostsNode() + if err != nil { + return nil, err + } + + keepTrying := true + for keepTrying { + keepTrying, err = r.importBatch(authToken, parent) + if err != nil { + return nil, err + } + } + + return parent, nil +} + +// Used to parse json +type apiPost struct { + Href string + Description string + Extended string + Meta string + Hash string + Time string + Shared string + ToRead string + Tags string +} + +func (r *run) importBatch(authToken string, parent *importer.Object) (keepTrying bool, err error) { + sleepDuration := r.nextAfter.Sub(time.Now()) + // block until we either get canceled or until it is time to run + select { + case <-r.Done(): + log.Printf("pinboard: Importer interrupted.") + return false, context.ErrCanceled + case <-time.After(sleepDuration): + // just proceed + } + start := time.Now() + + u := fmt.Sprintf(fetchUrl, authToken, batchLimit, r.nextCursor) + resp, err := r.HTTPClient().Get(u) + if err != nil { + return false, err + } + defer resp.Body.Close() + switch { + case resp.StatusCode == StatusTooManyRequests: + r.lastPause = r.lastPause * 2 + r.nextAfter = time.Now().Add(r.lastPause) + return true, nil + case resp.StatusCode != http.StatusOK: + return false, fmt.Errorf("Unexpected status code %v fetching %v", resp.StatusCode, u) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return false, err + } + + var postBatch []apiPost + if err = json.Unmarshal(body, &postBatch); err != nil { + return false, err + } + + if err != nil { + return false, err + } + + postCount := len(postBatch) + if postCount == 0 { + // we are done! + return false, nil + } + + log.Printf("pinboard: Importing %d posts...", postCount) + var grp syncutil.Group + for _, post := range postBatch { + if r.Context.IsCanceled() { + log.Printf("pinboard: Importer interrupted") + return false, context.ErrCanceled + } + + post := post + r.postGate.Start() + grp.Go(func() error { + defer r.postGate.Done() + return r.importPost(&post, parent) + }) + } + + log.Printf("pinboard: Imported batch of %d posts in %s.", postCount, time.Now().Sub(start)) + + r.nextCursor = postBatch[postCount-1].Time + r.lastPause = pauseInterval + r.nextAfter = time.Now().Add(pauseInterval) + tryAgain := postCount == batchLimit + return tryAgain, grp.Err() +} + +func (r *run) importPost(post *apiPost, parent *importer.Object) error { + postNode, err := parent.ChildPathObject(post.Hash) + if err != nil { + return err + } + + t, err := time.Parse(timeFormat, post.Time) + if err != nil { + return err + } + + attrs := []string{ + "pinboard.in:hash", post.Hash, + nodeattr.Type, "pinboard.in:post", + nodeattr.DateCreated, schema.RFC3339FromTime(t), + nodeattr.Title, post.Description, + nodeattr.URL, post.Href, + "pinboard.in:extended", post.Extended, + "pinboard.in:meta", post.Meta, + "pinboard.in:shared", post.Shared, + "pinboard.in:toread", post.ToRead, + } + if err = postNode.SetAttrs(attrs...); err != nil { + return err + } + if err = postNode.SetAttrValues("tag", strings.Split(post.Tags, " ")); err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/pinboard/pinboard_test.go b/vendor/github.com/camlistore/camlistore/pkg/importer/pinboard/pinboard_test.go new file mode 100644 index 00000000..5eb696aa --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/pinboard/pinboard_test.go @@ -0,0 +1,196 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pinboard + +import ( + "fmt" + "net/http" + "os" + "strings" + "testing" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/client" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/schema/nodeattr" + "camlistore.org/pkg/test" +) + +func verifyUsername(t *testing.T, apiToken string, expected string) { + extracted := extractUsername(apiToken) + if extracted != expected { + t.Errorf("Testing %q: user name is %q when we want %q", apiToken, extracted, expected) + } +} + +func TestExtractUsername(t *testing.T) { + verifyUsername(t, "gina:foo", "gina") + verifyUsername(t, "", "") +} + +func findChildRefs(parent *importer.Object) ([]blob.Ref, error) { + childRefs := []blob.Ref{} + var err error + parent.ForeachAttr(func(key, value string) { + if strings.HasPrefix(key, "camliPath:") { + if br, ok := blob.Parse(value); ok { + childRefs = append(childRefs, br) + return + } + if err == nil { + err = fmt.Errorf("invalid blobRef for %s attribute of %v: %q", key, parent, value) + } + } + }) + return childRefs, err +} + +func getRequiredChildPathObj(parent *importer.Object, path string) (*importer.Object, error) { + return parent.ChildPathObjectOrFunc(path, func() (*importer.Object, error) { + return nil, fmt.Errorf("Unable to locate child path %s of node %v", path, parent.PermanodeRef()) + }) +} + +func setupClient(w *test.World) (*client.Client, error) { + // Do the silly env vars dance to avoid the "non-hermetic use of host config panic". + if err := os.Setenv("CAMLI_KEYID", w.ClientIdentity()); err != nil { + return nil, err + } + if err := os.Setenv("CAMLI_SECRET_RING", w.SecretRingFile()); err != nil { + return nil, err + } + osutil.AddSecretRingFlag() + cl := client.New(w.ServerBaseURL()) + // This permanode is not needed in itself, but that takes care of uploading + // behind the scenes the public key to the blob server. A bit gross, but + // it's just for a test anyway. + if _, err := cl.UploadNewPermanode(); err != nil { + return nil, err + } + return cl, nil +} + +// Verify that a batch import of 3 posts works +func TestIntegrationRun(t *testing.T) { + const importerPrefix = "/importer/" + const authToken = "gina:foo" + const attrKey = "key" + const attrValue = "value" + + w := test.GetWorld(t) + baseURL := w.ServerBaseURL() + + // TODO(mpl): add a utility in integration package to provide a client that + // just works with World. + cl, err := setupClient(w) + if err != nil { + t.Fatal(err) + } + signer, err := cl.Signer() + if err != nil { + t.Fatal(err) + } + clientId := map[string]string{ + "pinboard": "fakeStaticClientId", + } + clientSecret := map[string]string{ + "pinboard": "fakeStaticClientSecret", + } + + responder := httputil.FileResponder("testdata/batchresponse.json") + transport, err := httputil.NewRegexpFakeTransport([]*httputil.Matcher{ + &httputil.Matcher{`^https\://api\.pinboard\.in/v1/posts/all\?auth_token=gina:foo&format=json&results=10000&todt=\d\d\d\d.*`, responder}, + }) + if err != nil { + t.Fatal(err) + } + httpClient := &http.Client{ + Transport: transport, + } + + hc := importer.HostConfig{ + BaseURL: baseURL, + Prefix: importerPrefix, + Target: cl, + BlobSource: cl, + Signer: signer, + Search: cl, + ClientId: clientId, + ClientSecret: clientSecret, + HTTPClient: httpClient, + } + + host, err := importer.NewHost(hc) + if err != nil { + t.Fatal(err) + } + rc, err := importer.CreateAccount(host, "pinboard") + if err != nil { + t.Fatal(err) + } + err = rc.AccountNode().SetAttrs(attrAuthToken, authToken) + if err != nil { + t.Fatal(err) + } + + testee := imp{} + if err := testee.Run(rc); err != nil { + t.Fatal(err) + } + + postsNode, err := getRequiredChildPathObj(rc.RootNode(), "posts") + if err != nil { + t.Fatal(err) + } + + childRefs, err := findChildRefs(postsNode) + if err != nil { + t.Fatal(err) + } + + expectedPosts := map[string]string{ + `https://wiki.archlinux.org/index.php/xorg#Display_size_and_DPI`: "Xorg - ArchWiki", + `http://www.harihareswara.net/sumana/2014/08/17/0`: "One Way Confidence Will Look", + `http://www.wikiart.org/en/marcus-larson/fishing-near-the-fjord-by-moonlight-1862`: "Fishing Near The Fjord By Moonlight - Marcus Larson - WikiArt.org", + } + + if len(childRefs) != len(expectedPosts) { + t.Fatalf("After import, found %d child refs, want %d: %v", len(childRefs), len(expectedPosts), childRefs) + } + + for _, ref := range childRefs { + childNode, err := host.ObjectFromRef(ref) + if err != nil { + t.Fatal(err) + } + foundURL := childNode.Attr(nodeattr.URL) + expectedTitle, ok := expectedPosts[foundURL] + if !ok { + t.Fatalf("Found unexpected child node %v with url %q", childNode, foundURL) + } + foundTitle := childNode.Attr(nodeattr.Title) + if foundTitle != expectedTitle { + t.Fatalf("Found unexpected child node %v with title %q when we want %q", childNode, foundTitle, expectedTitle) + } + delete(expectedPosts, foundURL) + } + if len(expectedPosts) != 0 { + t.Fatalf("The following entries were expected but not found: %#v", expectedPosts) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/pinboard/testdata/batchresponse.json b/vendor/github.com/camlistore/camlistore/pkg/importer/pinboard/testdata/batchresponse.json new file mode 100644 index 00000000..0cef87f2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/pinboard/testdata/batchresponse.json @@ -0,0 +1,35 @@ +[ + { + "href": "https:\/\/wiki.archlinux.org\/index.php\/xorg#Display_size_and_DPI", + "description": "Xorg - ArchWiki", + "extended": "", + "meta": "595781d94cd21c7fba5c67738313f6ff", + "hash": "98ec0d964f002c48877e256e8c737be5", + "time": "2014-08-28T16:46:31Z", + "shared": "yes", + "toread": "yes", + "tags": "" + }, + { + "href": "http:\/\/www.harihareswara.net\/sumana\/2014\/08\/17\/0", + "description": "One Way Confidence Will Look", + "extended": "", + "meta": "728598533614784d426eaa49ee8aa0cf", + "hash": "0ad4cd6bd5b5c318694bcb759a4d24e1", + "time": "2014-08-18T17:22:10Z", + "shared": "yes", + "toread": "yes", + "tags": "feminism" + }, + { + "href": "http:\/\/www.wikiart.org\/en\/marcus-larson\/fishing-near-the-fjord-by-moonlight-1862", + "description": "Fishing Near The Fjord By Moonlight - Marcus Larson - WikiArt.org", + "extended": "", + "meta": "b2db4fe15bf13db76f76db9e4b1fbe98", + "hash": "1cf0c0bf2f4873f209e3ae71aef60e07", + "time": "2014-07-10T17:19:38Z", + "shared": "no", + "toread": "no", + "tags": "art" + } +] diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/README b/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/README new file mode 100644 index 00000000..08d0a525 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/README @@ -0,0 +1,6 @@ +Twitter Importer +=================== + +This is a Camlistore importer for Twitter. + +Go to http[s]://your_camlistore_server/importer/twitter diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/testdata.go b/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/testdata.go new file mode 100644 index 00000000..075d6c98 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/testdata.go @@ -0,0 +1,267 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package twitter + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "strconv" + "time" + + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + "camlistore.org/pkg/osutil" +) + +var _ importer.TestDataMaker = (*imp)(nil) + +func (im *imp) SetTestAccount(acctNode *importer.Object) error { + return acctNode.SetAttrs( + importer.AcctAttrAccessToken, "fakeAccessToken", + importer.AcctAttrAccessTokenSecret, "fakeAccessSecret", + importer.AcctAttrUserID, "fakeUserID", + importer.AcctAttrName, "fakeName", + importer.AcctAttrUserName, "fakeScreenName", + ) +} + +func (im *imp) MakeTestData() http.RoundTripper { + const ( + fakeMaxId = int64(486450108201201664) // Most recent tweet. + nTweets = 300 // Arbitrary number of tweets generated. + ) + fakeMinId := fakeMaxId - nTweets // Oldest tweet in our timeline. + + timeLineURL := apiURL + userTimeLineAPIPath + timeLineCached := make(map[int64]string) + okHeader := `HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 + +` + timeLineResponse := okHeader + fakeTimeLine(fakeMaxId, fakeMinId, timeLineCached) + + fakePic := fakePicture() + responses := map[string]func() *http.Response{ + timeLineURL: httputil.StaticResponder(timeLineResponse), + fmt.Sprintf("%s?count=%d&user_id=fakeUserID", timeLineURL, tweetRequestLimit): httputil.StaticResponder(timeLineResponse), + "https://twitpic.com/show/large/bar": httputil.FileResponder(fakePic), + "https://i.imgur.com/foo.gif": httputil.FileResponder(fakePic), + } + + // register all the user_timeline calls (max_id varies) that should occur, + responses[fmt.Sprintf("%s?count=%d&max_id=%d&user_id=fakeUserID", timeLineURL, tweetRequestLimit, fakeMaxId-nTweets+1)] = httputil.StaticResponder(okHeader + fakeTimeLine(fakeMaxId-nTweets+1, fakeMinId, timeLineCached)) + if nTweets > tweetRequestLimit { + // that is, once every tweetRequestLimit-1, going down from fakeMaxId. + for i := fakeMaxId; i > fakeMinId; i -= tweetRequestLimit - 1 { + responses[fmt.Sprintf("%s?count=%d&max_id=%d&user_id=fakeUserID", timeLineURL, tweetRequestLimit, i)] = httputil.StaticResponder(okHeader + fakeTimeLine(i, fakeMinId, timeLineCached)) + } + } + + // register all the possible combinations of media twimg + for _, scheme := range []string{"http://", "https://"} { + for _, picsize := range []string{"thumb", "small", "medium", "large"} { + responses[fmt.Sprintf("%spbs.twimg.com/media/foo.jpg:%s", scheme, picsize)] = httputil.FileResponder(fakePic) + responses[fmt.Sprintf("%spbs.twimg.com/media/bar.png:%s", scheme, picsize)] = httputil.FileResponder(fakePic) + } + } + + return httputil.NewFakeTransport(responses) +} + +// fakeTimeLine returns a JSON user timeline of tweetRequestLimit tweets, starting +// with maxId as the most recent tweet id. It stops before tweetRequestLimit if +// minId is reached. The returned timeline is saved in cached. +func fakeTimeLine(maxId, minId int64, cached map[int64]string) string { + if tl, ok := cached[maxId]; ok { + return tl + } + min := maxId - int64(tweetRequestLimit) + if min <= minId { + min = minId + } + var tweets []*apiTweetItem + entitiesCounter := 0 + geoCounter := 0 + for i := maxId; i > min; i-- { + tweet := &apiTweetItem{ + Id: strconv.FormatInt(i, 10), + TextStr: fmt.Sprintf("fakeText %d", i), + CreatedAtStr: time.Now().Format(time.RubyDate), + Entities: fakeEntities(entitiesCounter), + } + geo, coords := fakeGeo(geoCounter) + tweet.Geo = geo + tweet.Coordinates = coords + tweets = append(tweets, tweet) + entitiesCounter++ + geoCounter++ + if entitiesCounter == 10 { + entitiesCounter = 0 + } + if geoCounter == 5 { + geoCounter = 0 + } + } + userTimeLine, err := json.MarshalIndent(tweets, "", " ") + if err != nil { + log.Fatalf("%v", err) + } + cached[maxId] = string(userTimeLine) + return cached[maxId] +} + +func fakeGeo(counter int) (*geo, *coords) { + sf := []float64{37.7447124, -122.4341914} + gre := []float64{45.1822842, 5.7141854} + switch counter { + case 0: + return nil, nil + case 1: + return &geo{sf}, nil + case 2: + return nil, &coords{[]float64{gre[1], gre[0]}} + case 3: + return &geo{gre}, &coords{[]float64{sf[1], sf[0]}} + default: + return nil, nil + } +} + +func fakeEntities(counter int) entities { + sizes := func() map[string]mediaSize { + return map[string]mediaSize{ + "medium": {W: 591, H: 332, Resize: "fit"}, + "large": {W: 591, H: 332, Resize: "fit"}, + "small": {W: 338, H: 190, Resize: "fit"}, + "thumb": {W: 150, H: 150, Resize: "crop"}, + } + } + mediaTwimg1 := func() *media { + return &media{ + Id: "1", + IdNum: 1, + MediaURL: `http://pbs.twimg.com/media/foo.jpg`, + MediaURLHTTPS: `https://pbs.twimg.com/media/foo.jpg`, + Sizes: sizes(), + } + } + mediaTwimg2 := func() *media { + return &media{ + Id: "2", + IdNum: 2, + MediaURL: `http://pbs.twimg.com/media/bar.png`, + MediaURLHTTPS: `https://pbs.twimg.com/media/bar.png`, + Sizes: sizes(), + } + } + notPicURL := func() *urlEntity { + return &urlEntity{ + URL: `http://t.co/whatever`, + ExpandedURL: `http://camlistore.org`, + DisplayURL: `camlistore.org`, + } + } + imgurURL := func() *urlEntity { + return &urlEntity{ + URL: `http://t.co/whatever2`, + ExpandedURL: `http://imgur.com/foo`, + DisplayURL: `imgur.com/foo`, + } + } + twitpicURL := func() *urlEntity { + return &urlEntity{ + URL: `http://t.co/whatever3`, + ExpandedURL: `http://twitpic.com/bar`, + DisplayURL: `twitpic.com/bar`, + } + } + + // if you add another case, make sure the entities counter reset + // in fakeTimeLine allows for that case to happen. + // We could use global vars instead, but don't want to pollute the + // twitter pkg namespace. + switch counter { + case 0: + return entities{} + case 1: + return entities{ + Media: []*media{ + mediaTwimg1(), + mediaTwimg2(), + }, + } + case 2: + return entities{ + URLs: []*urlEntity{ + notPicURL(), + }, + } + case 3: + return entities{ + URLs: []*urlEntity{ + notPicURL(), + imgurURL(), + }, + } + case 4: + return entities{ + URLs: []*urlEntity{ + twitpicURL(), + imgurURL(), + }, + } + case 5: + return entities{ + Media: []*media{ + mediaTwimg2(), + mediaTwimg1(), + }, + URLs: []*urlEntity{ + notPicURL(), + twitpicURL(), + }, + } + case 6: + return entities{ + Media: []*media{ + mediaTwimg1(), + mediaTwimg2(), + }, + URLs: []*urlEntity{ + imgurURL(), + twitpicURL(), + }, + } + default: + return entities{} + } +} + +func fakePicture() string { + camliDir, err := osutil.GoPackagePath("camlistore.org") + if err == os.ErrNotExist { + log.Fatal("Directory \"camlistore.org\" not found under GOPATH/src; are you not running with devcam?") + } + if err != nil { + log.Fatalf("Error searching for \"camlistore.org\" under GOPATH: %v", err) + } + return filepath.Join(camliDir, filepath.FromSlash("third_party/glitch/npc_piggy__x1_walk_png_1354829432.png")) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/testdata/verify_credentials-res.json b/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/testdata/verify_credentials-res.json new file mode 100755 index 00000000..8d06b457 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/testdata/verify_credentials-res.json @@ -0,0 +1,9 @@ +{ + "id":2325935334, + "id_str":"2325935334", + "name":"Mathieu Lonjaret", + "screen_name":"lejatorn", + "location":"", + "description":"potato, clever label, trendy word", + "url":"https:\/\/t.co\/TF5K7idMNj" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/twitter.go b/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/twitter.go new file mode 100644 index 00000000..372e3578 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/twitter.go @@ -0,0 +1,886 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package twitter implements a twitter.com importer. +package twitter + +import ( + "archive/zip" + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/schema/nodeattr" + "camlistore.org/pkg/syncutil" + "camlistore.org/third_party/github.com/garyburd/go-oauth/oauth" +) + +const ( + apiURL = "https://api.twitter.com/1.1/" + temporaryCredentialRequestURL = "https://api.twitter.com/oauth/request_token" + resourceOwnerAuthorizationURL = "https://api.twitter.com/oauth/authorize" + tokenRequestURL = "https://api.twitter.com/oauth/access_token" + userInfoAPIPath = "account/verify_credentials.json" + userTimeLineAPIPath = "statuses/user_timeline.json" + + // runCompleteVersion is a cache-busting version number of the + // importer code. It should be incremented whenever the + // behavior of this importer is updated enough to warrant a + // complete run. Otherwise, if the importer runs to + // completion, this version number is recorded on the account + // permanode and subsequent importers can stop early. + runCompleteVersion = "4" + + // acctAttrTweetZip specifies an optional attribte for the account permanode. + // If set, it should be of a "file" schema blob referencing the tweets.zip + // file that Twitter makes available for the full archive download. + // The Twitter API doesn't go back forever in time, so if you started using + // the Camlistore importer too late, you need to "camput file tweets.zip" + // once downloading it from Twitter, and then: + // $ camput attr twitterArchiveZipFileRef + // ... and re-do an import. + acctAttrTweetZip = "twitterArchiveZipFileRef" + + // acctAttrZipDoneVersion is updated at the end of a successful zip import and + // is used to determine whether the zip file needs to be re-imported in a future run. + acctAttrZipDoneVersion = "twitterZipDoneVersion" // == ":" + + // Per-tweet note of how we imported it: either "zip" or "api" + attrImportMethod = "twitterImportMethod" + + tweetRequestLimit = 200 // max number of tweets we can get in a user_timeline request + tweetsAtOnce = 20 // how many tweets to import at once +) + +var oAuthURIs = importer.OAuthURIs{ + TemporaryCredentialRequestURI: temporaryCredentialRequestURL, + ResourceOwnerAuthorizationURI: resourceOwnerAuthorizationURL, + TokenRequestURI: tokenRequestURL, +} + +func init() { + importer.Register("twitter", &imp{}) +} + +var _ importer.ImporterSetupHTMLer = (*imp)(nil) + +type imp struct { + importer.OAuth1 // for CallbackRequestAccount and CallbackURLParameters +} + +func (im *imp) NeedsAPIKey() bool { return true } +func (im *imp) SupportsIncremental() bool { return true } + +func (im *imp) IsAccountReady(acctNode *importer.Object) (ok bool, err error) { + if acctNode.Attr(importer.AcctAttrUserID) != "" && acctNode.Attr(importer.AcctAttrAccessToken) != "" { + return true, nil + } + return false, nil +} + +func (im *imp) SummarizeAccount(acct *importer.Object) string { + ok, err := im.IsAccountReady(acct) + if err != nil { + return "Not configured; error = " + err.Error() + } + if !ok { + return "Not configured" + } + s := fmt.Sprintf("@%s (%s), twitter id %s", + acct.Attr(importer.AcctAttrUserName), + acct.Attr(importer.AcctAttrName), + acct.Attr(importer.AcctAttrUserID), + ) + if acct.Attr(acctAttrTweetZip) != "" { + s += " + zip file" + } + return s +} + +func (im *imp) AccountSetupHTML(host *importer.Host) string { + base := host.ImporterBaseURL() + "twitter" + return fmt.Sprintf(` +

    Configuring Twitter

    +

    Visit https://apps.twitter.com/ and click "Create New App".

    +

    Use the following settings:

    +
      +
    • Name: Does not matter. (camlistore-importer).
    • +
    • Description: Does not matter. (imports twitter data into camlistore).
    • +
    • Website: %s
    • +
    • Callback URL: %s
    • +
    +

    Click "Create your Twitter application".You should be redirected to the Application Management page of your newly created application. +
    Go to the API Keys tab. Copy the "API key" and "API secret" into the "Client ID" and "Client Secret" boxes above.

    +`, base, base+"/callback") +} + +// A run is our state for a given run of the importer. +type run struct { + *importer.RunContext + im *imp + incremental bool // whether we've completed a run in the past + + oauthClient *oauth.Client // No need to guard, used read-only. + accessCreds *oauth.Credentials // No need to guard, used read-only. + + mu sync.Mutex // guards anyErr + anyErr bool +} + +var forceFullImport, _ = strconv.ParseBool(os.Getenv("CAMLI_TWITTER_FULL_IMPORT")) + +func (im *imp) Run(ctx *importer.RunContext) error { + clientId, secret, err := ctx.Credentials() + if err != nil { + return fmt.Errorf("no API credentials: %v", err) + } + acctNode := ctx.AccountNode() + accessToken := acctNode.Attr(importer.AcctAttrAccessToken) + accessSecret := acctNode.Attr(importer.AcctAttrAccessTokenSecret) + if accessToken == "" || accessSecret == "" { + return errors.New("access credentials not found") + } + r := &run{ + RunContext: ctx, + im: im, + incremental: !forceFullImport && acctNode.Attr(importer.AcctAttrCompletedVersion) == runCompleteVersion, + + oauthClient: &oauth.Client{ + TemporaryCredentialRequestURI: temporaryCredentialRequestURL, + ResourceOwnerAuthorizationURI: resourceOwnerAuthorizationURL, + TokenRequestURI: tokenRequestURL, + Credentials: oauth.Credentials{ + Token: clientId, + Secret: secret, + }, + }, + accessCreds: &oauth.Credentials{ + Token: accessToken, + Secret: accessSecret, + }, + } + + userID := acctNode.Attr(importer.AcctAttrUserID) + if userID == "" { + return errors.New("UserID hasn't been set by account setup.") + } + + skipAPITweets, _ := strconv.ParseBool(os.Getenv("CAMLI_TWITTER_SKIP_API_IMPORT")) + if !skipAPITweets { + if err := r.importTweets(userID); err != nil { + return err + } + } + + zipRef := acctNode.Attr(acctAttrTweetZip) + zipDoneVal := zipRef + ":" + runCompleteVersion + if zipRef != "" && !(r.incremental && acctNode.Attr(acctAttrZipDoneVersion) == zipDoneVal) { + zipbr, ok := blob.Parse(zipRef) + if !ok { + return fmt.Errorf("invalid zip file blobref %q", zipRef) + } + fr, err := schema.NewFileReader(r.Host.BlobSource(), zipbr) + if err != nil { + return fmt.Errorf("error opening zip %v: %v", zipbr, err) + } + defer fr.Close() + zr, err := zip.NewReader(fr, fr.Size()) + if err != nil { + return fmt.Errorf("Error opening twitter zip file %v: %v", zipRef, err) + } + if err := r.importTweetsFromZip(userID, zr); err != nil { + return err + } + if err := acctNode.SetAttrs(acctAttrZipDoneVersion, zipDoneVal); err != nil { + return err + } + } + + r.mu.Lock() + anyErr := r.anyErr + r.mu.Unlock() + + if !anyErr { + if err := acctNode.SetAttrs(importer.AcctAttrCompletedVersion, runCompleteVersion); err != nil { + return err + } + } + + return nil +} + +func (r *run) errorf(format string, args ...interface{}) { + log.Printf(format, args...) + r.mu.Lock() + defer r.mu.Unlock() + r.anyErr = true +} + +func (r *run) doAPI(result interface{}, apiPath string, keyval ...string) error { + return importer.OAuthContext{ + r.Context, + r.oauthClient, + r.accessCreds}.PopulateJSONFromURL(result, apiURL+apiPath, keyval...) +} + +func (r *run) importTweets(userID string) error { + maxId := "" + continueRequests := true + + tweetsNode, err := r.getTopLevelNode("tweets") + if err != nil { + return err + } + + numTweets := 0 + sawTweet := map[string]bool{} + + // If attrs is changed, so should the expected responses accordingly for the + // RoundTripper of MakeTestData (testdata.go). + attrs := []string{ + "user_id", userID, + "count", strconv.Itoa(tweetRequestLimit), + } + for continueRequests { + if r.Context.IsCanceled() { + r.errorf("Twitter importer: interrupted") + return context.ErrCanceled + } + + var resp []*apiTweetItem + var err error + if maxId == "" { + log.Printf("Fetching tweets for userid %s", userID) + err = r.doAPI(&resp, userTimeLineAPIPath, attrs...) + } else { + log.Printf("Fetching tweets for userid %s with max ID %s", userID, maxId) + err = r.doAPI(&resp, userTimeLineAPIPath, + append(attrs, "max_id", maxId)...) + } + if err != nil { + return err + } + + var ( + newThisBatch = 0 + allDupMu sync.Mutex + allDups = true + gate = syncutil.NewGate(tweetsAtOnce) + grp syncutil.Group + ) + for i := range resp { + tweet := resp[i] + + // Dup-suppression. + if sawTweet[tweet.Id] { + continue + } + sawTweet[tweet.Id] = true + newThisBatch++ + maxId = tweet.Id + + gate.Start() + grp.Go(func() error { + defer gate.Done() + dup, err := r.importTweet(tweetsNode, tweet, true) + if !dup { + allDupMu.Lock() + allDups = false + allDupMu.Unlock() + } + if err != nil { + r.errorf("Twitter importer: error importing tweet %s %v", tweet.Id, err) + } + return err + }) + } + if err := grp.Err(); err != nil { + return err + } + numTweets += newThisBatch + log.Printf("Imported %d tweets this batch; %d total.", newThisBatch, numTweets) + if r.incremental && allDups { + log.Printf("twitter incremental import found end batch") + break + } + continueRequests = newThisBatch > 0 + } + log.Printf("Successfully did full run of importing %d tweets", numTweets) + return nil +} + +func tweetsFromZipFile(zf *zip.File) (tweets []*zipTweetItem, err error) { + rc, err := zf.Open() + if err != nil { + return nil, err + } + slurp, err := ioutil.ReadAll(rc) + rc.Close() + if err != nil { + return nil, err + } + i := bytes.IndexByte(slurp, '[') + if i < 0 { + return nil, errors.New("No '[' found in zip file") + } + slurp = slurp[i:] + if err := json.Unmarshal(slurp, &tweets); err != nil { + return nil, fmt.Errorf("JSON error: %v", err) + } + return +} + +func (r *run) importTweetsFromZip(userID string, zr *zip.Reader) error { + log.Printf("Processing zip file with %d files", len(zr.File)) + + tweetsNode, err := r.getTopLevelNode("tweets") + if err != nil { + return err + } + + var ( + gate = syncutil.NewGate(tweetsAtOnce) + grp syncutil.Group + ) + total := 0 + for _, zf := range zr.File { + if !(strings.HasPrefix(zf.Name, "data/js/tweets/2") && strings.HasSuffix(zf.Name, ".js")) { + continue + } + tweets, err := tweetsFromZipFile(zf) + if err != nil { + return fmt.Errorf("error reading tweets from %s: %v", zf.Name, err) + } + + for i := range tweets { + total++ + tweet := tweets[i] + gate.Start() + grp.Go(func() error { + defer gate.Done() + _, err := r.importTweet(tweetsNode, tweet, false) + return err + }) + } + } + err = grp.Err() + log.Printf("zip import of tweets: %d total, err = %v", total, err) + return err +} + +func timeParseFirstFormat(timeStr string, format ...string) (t time.Time, err error) { + if len(format) == 0 { + panic("need more than 1 format") + } + for _, f := range format { + t, err = time.Parse(f, timeStr) + if err == nil { + break + } + } + return +} + +// viaAPI is true if it came via the REST API, or false if it came via a zip file. +func (r *run) importTweet(parent *importer.Object, tweet tweetItem, viaAPI bool) (dup bool, err error) { + if r.Context.IsCanceled() { + r.errorf("Twitter importer: interrupted") + return false, context.ErrCanceled + } + id := tweet.ID() + tweetNode, err := parent.ChildPathObject(id) + if err != nil { + return false, err + } + + // Because the zip format and the API format differ a bit, and + // might diverge more in the future, never use the zip content + // to overwrite data fetched via the API. If we add new + // support for different fields in the future, we might want + // to revisit this decision. Be wary of flip/flopping data if + // modifying this, though. + if tweetNode.Attr(attrImportMethod) == "api" && !viaAPI { + return true, nil + } + + // e.g. "2014-06-12 19:11:51 +0000" + createdTime, err := timeParseFirstFormat(tweet.CreatedAt(), time.RubyDate, "2006-01-02 15:04:05 -0700") + if err != nil { + return false, fmt.Errorf("could not parse time %q: %v", tweet.CreatedAt(), err) + } + + url := fmt.Sprintf("https://twitter.com/%s/status/%v", + r.AccountNode().Attr(importer.AcctAttrUserName), + id) + + attrs := []string{ + "twitterId", id, + nodeattr.Type, "twitter.com:tweet", + nodeattr.StartDate, schema.RFC3339FromTime(createdTime), + nodeattr.Content, tweet.Text(), + nodeattr.URL, url, + } + if lat, long, ok := tweet.LatLong(); ok { + attrs = append(attrs, + nodeattr.Latitude, fmt.Sprint(lat), + nodeattr.Longitude, fmt.Sprint(long), + ) + } + if viaAPI { + attrs = append(attrs, attrImportMethod, "api") + } else { + attrs = append(attrs, attrImportMethod, "zip") + } + + for i, m := range tweet.Media() { + filename := m.BaseFilename() + if tweetNode.Attr("camliPath:"+filename) != "" && (i > 0 || tweetNode.Attr("camliContentImage") != "") { + // Don't re-import media we've already fetched. + continue + } + tried, gotMedia := 0, false + for _, mediaURL := range m.URLs() { + tried++ + res, err := r.HTTPClient().Get(mediaURL) + if err != nil { + return false, fmt.Errorf("Error fetching %s for tweet %s : %v", mediaURL, url, err) + } + if res.StatusCode == http.StatusNotFound { + continue + } + if res.StatusCode != 200 { + return false, fmt.Errorf("HTTP status %d fetching %s for tweet %s", res.StatusCode, mediaURL, url) + } + if !viaAPI { + log.Printf("For zip tweet %s, reading %v", url, mediaURL) + } + fileRef, err := schema.WriteFileFromReader(r.Host.Target(), filename, res.Body) + res.Body.Close() + if err != nil { + return false, fmt.Errorf("Error fetching media %s for tweet %s: %v", mediaURL, url, err) + } + attrs = append(attrs, "camliPath:"+filename, fileRef.String()) + if i == 0 { + attrs = append(attrs, "camliContentImage", fileRef.String()) + } + log.Printf("Slurped %s as %s for tweet %s (%v)", mediaURL, fileRef.String(), url, tweetNode.PermanodeRef()) + gotMedia = true + break + } + if !gotMedia && tried > 0 { + return false, fmt.Errorf("All media URLs 404s for tweet %s", url) + } + } + + changes, err := tweetNode.SetAttrs2(attrs...) + if err == nil && changes { + log.Printf("Imported tweet %s", url) + } + return !changes, err +} + +// The path be one of "tweets". +// In the future: "lists", "direct_messages", etc. +func (r *run) getTopLevelNode(path string) (*importer.Object, error) { + acctNode := r.AccountNode() + + root := r.RootNode() + rootTitle := fmt.Sprintf("%s's Twitter Data", acctNode.Attr(importer.AcctAttrUserName)) + log.Printf("root title = %q; want %q", root.Attr(nodeattr.Title), rootTitle) + if err := root.SetAttr(nodeattr.Title, rootTitle); err != nil { + return nil, err + } + + obj, err := root.ChildPathObject(path) + if err != nil { + return nil, err + } + var title string + switch path { + case "tweets": + title = fmt.Sprintf("%s's Tweets", acctNode.Attr(importer.AcctAttrUserName)) + } + return obj, obj.SetAttr(nodeattr.Title, title) +} + +type userInfo struct { + ID string `json:"id_str"` + ScreenName string `json:"screen_name"` + Name string `json:"name,omitempty"` +} + +func getUserInfo(ctx importer.OAuthContext) (userInfo, error) { + var ui userInfo + if err := ctx.PopulateJSONFromURL(&ui, apiURL+userInfoAPIPath); err != nil { + return ui, err + } + if ui.ID == "" { + return ui, fmt.Errorf("No userid returned") + } + return ui, nil +} + +func (im *imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error { + oauthClient, err := ctx.NewOAuthClient(oAuthURIs) + if err != nil { + err = fmt.Errorf("error getting OAuth client: %v", err) + httputil.ServeError(w, r, err) + return err + } + tempCred, err := oauthClient.RequestTemporaryCredentials(ctx.HTTPClient(), ctx.CallbackURL(), nil) + if err != nil { + err = fmt.Errorf("Error getting temp cred: %v", err) + httputil.ServeError(w, r, err) + return err + } + if err := ctx.AccountNode.SetAttrs( + importer.AcctAttrTempToken, tempCred.Token, + importer.AcctAttrTempSecret, tempCred.Secret, + ); err != nil { + err = fmt.Errorf("Error saving temp creds: %v", err) + httputil.ServeError(w, r, err) + return err + } + + authURL := oauthClient.AuthorizationURL(tempCred, nil) + http.Redirect(w, r, authURL, 302) + return nil +} + +func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) { + tempToken := ctx.AccountNode.Attr(importer.AcctAttrTempToken) + tempSecret := ctx.AccountNode.Attr(importer.AcctAttrTempSecret) + if tempToken == "" || tempSecret == "" { + log.Printf("twitter: no temp creds in callback") + httputil.BadRequestError(w, "no temp creds in callback") + return + } + if tempToken != r.FormValue("oauth_token") { + log.Printf("unexpected oauth_token: got %v, want %v", r.FormValue("oauth_token"), tempToken) + httputil.BadRequestError(w, "unexpected oauth_token") + return + } + oauthClient, err := ctx.NewOAuthClient(oAuthURIs) + if err != nil { + err = fmt.Errorf("error getting OAuth client: %v", err) + httputil.ServeError(w, r, err) + return + } + tokenCred, vals, err := oauthClient.RequestToken( + ctx.Context.HTTPClient(), + &oauth.Credentials{ + Token: tempToken, + Secret: tempSecret, + }, + r.FormValue("oauth_verifier"), + ) + if err != nil { + httputil.ServeError(w, r, fmt.Errorf("Error getting request token: %v ", err)) + return + } + userid := vals.Get("user_id") + if userid == "" { + httputil.ServeError(w, r, fmt.Errorf("Couldn't get user id: %v", err)) + return + } + if err := ctx.AccountNode.SetAttrs( + importer.AcctAttrAccessToken, tokenCred.Token, + importer.AcctAttrAccessTokenSecret, tokenCred.Secret, + ); err != nil { + httputil.ServeError(w, r, fmt.Errorf("Error setting token attributes: %v", err)) + return + } + + u, err := getUserInfo(importer.OAuthContext{ctx.Context, oauthClient, tokenCred}) + if err != nil { + httputil.ServeError(w, r, fmt.Errorf("Couldn't get user info: %v", err)) + return + } + if err := ctx.AccountNode.SetAttrs( + importer.AcctAttrUserID, u.ID, + importer.AcctAttrName, u.Name, + importer.AcctAttrUserName, u.ScreenName, + nodeattr.Title, fmt.Sprintf("%s's Twitter Account", u.ScreenName), + ); err != nil { + httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err)) + return + } + http.Redirect(w, r, ctx.AccountURL(), http.StatusFound) +} + +type tweetItem interface { + ID() string + LatLong() (lat, long float64, ok bool) + CreatedAt() string + Text() string + Media() []tweetMedia +} + +type tweetMedia interface { + URLs() []string // use first non-404 one + BaseFilename() string +} + +type apiTweetItem struct { + Id string `json:"id_str"` + TextStr string `json:"text"` + CreatedAtStr string `json:"created_at"` + Entities entities `json:"entities"` + + // One or both might be present: + Geo *geo `json:"geo"` // lat, long + Coordinates *coords `json:"coordinates"` // geojson: long, lat +} + +// zipTweetItem is like apiTweetItem, but twitter is annoying and the schema for the JSON inside zip files is slightly different. +type zipTweetItem struct { + Id string `json:"id_str"` + TextStr string `json:"text"` + CreatedAtStr string `json:"created_at"` + + // One or both might be present: + Geo *geo `json:"geo"` // lat, long + Coordinates *coords `json:"coordinates"` // geojson: long, lat + Entities zipEntities `json:"entities"` +} + +func (t *apiTweetItem) ID() string { + if t.Id == "" { + panic("empty id") + } + return t.Id +} + +func (t *zipTweetItem) ID() string { + if t.Id == "" { + panic("empty id") + } + return t.Id +} + +func (t *apiTweetItem) CreatedAt() string { return t.CreatedAtStr } +func (t *zipTweetItem) CreatedAt() string { return t.CreatedAtStr } + +func (t *apiTweetItem) Text() string { return t.TextStr } +func (t *zipTweetItem) Text() string { return t.TextStr } + +func (t *apiTweetItem) LatLong() (lat, long float64, ok bool) { + return latLong(t.Geo, t.Coordinates) +} + +func (t *zipTweetItem) LatLong() (lat, long float64, ok bool) { + return latLong(t.Geo, t.Coordinates) +} + +func latLong(g *geo, c *coords) (lat, long float64, ok bool) { + if g != nil && len(g.Coordinates) == 2 { + co := g.Coordinates + if co[0] != 0 && co[1] != 0 { + return co[0], co[1], true + } + } + if c != nil && len(c.Coordinates) == 2 { + co := c.Coordinates + if co[0] != 0 && co[1] != 0 { + return co[1], co[0], true + } + } + return +} + +func (t *zipTweetItem) Media() (ret []tweetMedia) { + for _, m := range t.Entities.Media { + ret = append(ret, m) + } + ret = append(ret, getImagesFromURLs(t.Entities.URLs)...) + return +} + +func (t *apiTweetItem) Media() (ret []tweetMedia) { + for _, m := range t.Entities.Media { + ret = append(ret, m) + } + ret = append(ret, getImagesFromURLs(t.Entities.URLs)...) + return +} + +type geo struct { + Coordinates []float64 `json:"coordinates"` // lat,long +} + +type coords struct { + Coordinates []float64 `json:"coordinates"` // long,lat +} + +type entities struct { + Media []*media `json:"media"` + URLs []*urlEntity `json:"urls"` +} + +type zipEntities struct { + Media []*zipMedia `json:"media"` + URLs []*urlEntity `json:"urls"` +} + +// e.g. { +// "indices" : [ 105, 125 ], +// "url" : "http:\/\/t.co\/gbGO8Qep", +// "expanded_url" : "http:\/\/twitpic.com\/6mdqac", +// "display_url" : "twitpic.com\/6mdqac" +// } +type urlEntity struct { + URL string `json:"url"` + ExpandedURL string `json:"expanded_url"` + DisplayURL string `json:"display_url"` +} + +var ( + twitpicRx = regexp.MustCompile(`\btwitpic\.com/(\w\w\w+)`) + imgurRx = regexp.MustCompile(`\bimgur\.com/(\w\w\w+)`) +) + +func getImagesFromURLs(urls []*urlEntity) (ret []tweetMedia) { + // TODO: extract these regexps from tweet text too. Happens in + // a few cases I've seen in my history. + for _, u := range urls { + if strings.HasPrefix(u.DisplayURL, "twitpic.com") { + ret = append(ret, twitpicImage(strings.TrimPrefix(u.DisplayURL, "twitpic.com/"))) + continue + } + if m := imgurRx.FindStringSubmatch(u.DisplayURL); m != nil { + ret = append(ret, imgurImage(m[1])) + continue + } + } + return +} + +// The Media entity from the Rest API. See also: zipMedia. +type media struct { + Id string `json:"id_str"` + IdNum int64 `json:"id"` + MediaURL string `json:"media_url"` + MediaURLHTTPS string `json:"media_url_https"` + Sizes map[string]mediaSize `json:"sizes"` + Type_ string `json:"type"` +} + +// The Media entity from the zip file JSON. Similar but different to +// media. Thanks, Twitter. +type zipMedia struct { + Id string `json:"id_str"` + IdNum int64 `json:"id"` + MediaURL string `json:"media_url"` + MediaURLHTTPS string `json:"media_url_https"` + Sizes []mediaSize `json:"sizes"` // without a key! useless. +} + +func (m *media) URLs() []string { + u := m.baseURL() + if u == "" { + return nil + } + return []string{u + m.largestMediaSuffix(), u} +} + +func (m *zipMedia) URLs() []string { + // We don't get any suffix names, so just try some common + // ones. The first non-404 will be used: + u := m.baseURL() + if u == "" { + return nil + } + return []string{ + u + ":large", + u, + } +} + +func (m *media) baseURL() string { + if v := m.MediaURLHTTPS; v != "" { + return v + } + return m.MediaURL +} + +func (m *zipMedia) baseURL() string { + if v := m.MediaURLHTTPS; v != "" { + return v + } + return m.MediaURL +} + +func (m *media) BaseFilename() string { + return path.Base(m.baseURL()) +} + +func (m *zipMedia) BaseFilename() string { + return path.Base(m.baseURL()) +} + +func (m *media) largestMediaSuffix() string { + bestPixels := 0 + bestSuffix := "" + for k, sz := range m.Sizes { + if px := sz.W * sz.H; px > bestPixels { + bestPixels = px + bestSuffix = ":" + k + } + } + return bestSuffix +} + +type mediaSize struct { + W int `json:"w"` + H int `json:"h"` + Resize string `json:"resize"` +} + +// An image from twitpic. +type twitpicImage string + +func (im twitpicImage) BaseFilename() string { return string(im) } + +func (im twitpicImage) URLs() []string { + return []string{"https://twitpic.com/show/large/" + string(im)} +} + +// An image from imgur +type imgurImage string + +func (im imgurImage) BaseFilename() string { return string(im) } + +func (im imgurImage) URLs() []string { + // Imgur ignores the suffix if it's .gif, .png, or .jpg. So just pick .gif. + // The actual content will be returned. + return []string{"https://i.imgur.com/" + string(im) + ".gif"} +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/twitter_test.go b/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/twitter_test.go new file mode 100644 index 00000000..e17a1069 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/importer/twitter/twitter_test.go @@ -0,0 +1,50 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package twitter + +import ( + "net/http" + "path/filepath" + "testing" + + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + + "camlistore.org/third_party/github.com/garyburd/go-oauth/oauth" +) + +func TestGetUserID(t *testing.T) { + ctx := context.New(context.WithHTTPClient(&http.Client{ + Transport: httputil.NewFakeTransport(map[string]func() *http.Response{ + apiURL + userInfoAPIPath: httputil.FileResponder(filepath.FromSlash("testdata/verify_credentials-res.json")), + }), + })) + defer ctx.Cancel() + inf, err := getUserInfo(importer.OAuthContext{ctx, &oauth.Client{}, &oauth.Credentials{}}) + if err != nil { + t.Fatal(err) + } + want := userInfo{ + ID: "2325935334", + ScreenName: "lejatorn", + Name: "Mathieu Lonjaret", + } + if inf != want { + t.Errorf("user info = %+v; want %+v", inf, want) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/corpus.go b/vendor/github.com/camlistore/camlistore/pkg/index/corpus.go new file mode 100644 index 00000000..51acafa2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/corpus.go @@ -0,0 +1,1262 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "bytes" + "errors" + "fmt" + "log" + "os" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/context" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/schema/nodeattr" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/strutil" + "camlistore.org/pkg/syncutil" + "camlistore.org/pkg/types/camtypes" +) + +// Corpus is an in-memory summary of all of a user's blobs' metadata. +type Corpus struct { + mu sync.RWMutex + //mu syncutil.RWMutexTracker // when debugging + + // building is true at start while scanning all rows in the + // index. While building, certain invariants (like things + // being sorted) can be temporarily violated and fixed at the + // end of scan. + building bool + + // gen is incremented on every blob received. + // It's used as a query cache invalidator. + gen int64 + + strs map[string]string // interned strings + brOfStr map[string]blob.Ref // blob.Parse fast path + brInterns int64 // blob.Ref -> blob.Ref, via br method + + blobs map[blob.Ref]*camtypes.BlobMeta + sumBlobBytes int64 + + // camlBlobs maps from camliType ("file") to blobref to the meta. + // The value is the same one in blobs. + camBlobs map[string]map[blob.Ref]*camtypes.BlobMeta + + // TODO: add GoLLRB to third_party; keep sorted BlobMeta + keyId map[blob.Ref]string + files map[blob.Ref]camtypes.FileInfo + permanodes map[blob.Ref]*PermanodeMeta + imageInfo map[blob.Ref]camtypes.ImageInfo // keyed by fileref (not wholeref) + fileWholeRef map[blob.Ref]blob.Ref // fileref -> its wholeref (TODO: multi-valued?) + gps map[blob.Ref]latLong // wholeRef -> GPS coordinates + + // edge tracks "forward" edges. e.g. from a directory's static-set to + // its members. Permanodes' camliMembers aren't tracked, since they + // can be obtained from permanodes.Claims. + // TODO: implement + edge map[blob.Ref][]edge + + // edgeBack tracks "backward" edges. e.g. from a file back to + // any directories it's part of. + // The map is from target (e.g. file) => owner (static-set). + // This only tracks static data structures, not permanodes. + // TODO: implement + edgeBack map[blob.Ref]map[blob.Ref]bool + + // claimBack allows hopping backwards from a Claim's Value + // when the Value is a blobref. It allows, for example, + // finding the parents of camliMember claims. If a permanode + // parent set A has a camliMembers B and C, it allows finding + // A from either B and C. + // The slice is not sorted. + claimBack map[blob.Ref][]*camtypes.Claim + + // TOOD: use deletedCache instead? + deletedBy map[blob.Ref]blob.Ref // key is deleted by value + // deletes tracks deletions of claims and permanodes. The key is + // the blobref of a claim or permanode. The values, sorted newest first, + // contain the blobref of the claim responsible for the deletion, as well + // as the date when that deletion happened. + deletes map[blob.Ref][]deletion + + mediaTags map[blob.Ref]map[string]string // wholeref -> "album" -> "foo" + + permanodesByTime *lazySortedPermanodes // cache of permanodes sorted by creation time. + permanodesByModtime *lazySortedPermanodes // cache of permanodes sorted by modtime. + + // scratch string slice + ss []string +} + +type latLong struct { + lat, long float64 +} + +// RLock locks the Corpus for reads. It must be used for any "Locked" methods. +func (c *Corpus) RLock() { c.mu.RLock() } + +// RUnlock unlocks the Corpus for reads. +func (c *Corpus) RUnlock() { c.mu.RUnlock() } + +// IsDeleted reports whether the provided blobref (of a permanode or claim) should be considered deleted. +func (c *Corpus) IsDeleted(br blob.Ref) bool { + c.RLock() + defer c.RUnlock() + return c.IsDeletedLocked(br) +} + +// IsDeletedLocked is the version of IsDeleted that assumes the Corpus is already locked with RLock. +func (c *Corpus) IsDeletedLocked(br blob.Ref) bool { + for _, v := range c.deletes[br] { + if !c.IsDeletedLocked(v.deleter) { + return true + } + } + return false +} + +type edge struct { + edgeType string + peer blob.Ref +} + +type PermanodeMeta struct { + // TODO: OwnerKeyId string + Claims []*camtypes.Claim // sorted by camtypes.ClaimsByDate +} + +func newCorpus() *Corpus { + c := &Corpus{ + blobs: make(map[blob.Ref]*camtypes.BlobMeta), + camBlobs: make(map[string]map[blob.Ref]*camtypes.BlobMeta), + files: make(map[blob.Ref]camtypes.FileInfo), + permanodes: make(map[blob.Ref]*PermanodeMeta), + imageInfo: make(map[blob.Ref]camtypes.ImageInfo), + deletedBy: make(map[blob.Ref]blob.Ref), + keyId: make(map[blob.Ref]string), + brOfStr: make(map[string]blob.Ref), + fileWholeRef: make(map[blob.Ref]blob.Ref), + gps: make(map[blob.Ref]latLong), + mediaTags: make(map[blob.Ref]map[string]string), + deletes: make(map[blob.Ref][]deletion), + claimBack: make(map[blob.Ref][]*camtypes.Claim), + } + c.permanodesByModtime = &lazySortedPermanodes{ + c: c, + pnTime: c.PermanodeModtimeLocked, + } + c.permanodesByTime = &lazySortedPermanodes{ + c: c, + pnTime: c.PermanodeAnyTimeLocked, + } + return c +} + +func NewCorpusFromStorage(s sorted.KeyValue) (*Corpus, error) { + if s == nil { + return nil, errors.New("storage is nil") + } + c := newCorpus() + return c, c.scanFromStorage(s) +} + +func (x *Index) KeepInMemory() (*Corpus, error) { + var err error + x.corpus, err = NewCorpusFromStorage(x.s) + return x.corpus, err +} + +// PreventStorageAccessForTesting causes any access to the index's underlying +// Storage interface to panic. +func (x *Index) PreventStorageAccessForTesting() { + x.s = crashStorage{} +} + +type crashStorage struct { + sorted.KeyValue +} + +func (crashStorage) Get(key string) (string, error) { + panic(fmt.Sprintf("unexpected KeyValue.Get(%q) called", key)) +} + +func (crashStorage) Find(start, end string) sorted.Iterator { + panic(fmt.Sprintf("unexpected KeyValue.Find(%q, %q) called", start, end)) +} + +// *********** Updating the corpus + +var corpusMergeFunc = map[string]func(c *Corpus, k, v []byte) error{ + "have": nil, // redundant with "meta" + "recpn": nil, // unneeded. + "meta": (*Corpus).mergeMetaRow, + "signerkeyid": (*Corpus).mergeSignerKeyIdRow, + "claim": (*Corpus).mergeClaimRow, + "fileinfo": (*Corpus).mergeFileInfoRow, + "filetimes": (*Corpus).mergeFileTimesRow, + "imagesize": (*Corpus).mergeImageSizeRow, + "wholetofile": (*Corpus).mergeWholeToFileRow, + "exifgps": (*Corpus).mergeEXIFGPSRow, + "exiftag": nil, // not using any for now + "signerattrvalue": nil, // ignoring for now + "mediatag": (*Corpus).mergeMediaTag, +} + +func memstats() *runtime.MemStats { + ms := new(runtime.MemStats) + runtime.GC() + runtime.ReadMemStats(ms) + return ms +} + +var logCorpusStats = true // set to false in tests + +var slurpPrefixes = []string{ + "meta:", // must be first + "signerkeyid:", + "claim|", + "fileinfo|", + "filetimes|", + "imagesize|", + "wholetofile|", + "exifgps|", + "mediatag|", +} + +// Key types (without trailing punctuation) that we slurp to memory at start. +var slurpedKeyType = make(map[string]bool) + +func init() { + for _, prefix := range slurpPrefixes { + slurpedKeyType[typeOfKey(prefix)] = true + } +} + +func (c *Corpus) scanFromStorage(s sorted.KeyValue) error { + c.building = true + + var ms0 *runtime.MemStats + if logCorpusStats { + ms0 = memstats() + log.Printf("Slurping corpus to memory from index...") + log.Printf("Slurping corpus to memory from index... (1/%d: meta rows)", len(slurpPrefixes)) + } + + // We do the "meta" rows first, before the prefixes below, because it + // populates the blobs map (used for blobref interning) and the camBlobs + // map (used for hinting the size of other maps) + if err := c.scanPrefix(s, "meta:"); err != nil { + return err + } + c.files = make(map[blob.Ref]camtypes.FileInfo, len(c.camBlobs["file"])) + c.permanodes = make(map[blob.Ref]*PermanodeMeta, len(c.camBlobs["permanode"])) + cpu0 := osutil.CPUUsage() + + var grp syncutil.Group + for i, prefix := range slurpPrefixes[1:] { + if logCorpusStats { + log.Printf("Slurping corpus to memory from index... (%d/%d: prefix %q)", i+2, len(slurpPrefixes), + prefix[:len(prefix)-1]) + } + prefix := prefix + grp.Go(func() error { return c.scanPrefix(s, prefix) }) + } + if err := grp.Err(); err != nil { + return err + } + + // Post-load optimizations and restoration of invariants. + for _, pm := range c.permanodes { + // Restore invariants violated during building: + sort.Sort(camtypes.ClaimPtrsByDate(pm.Claims)) + + // And intern some stuff. + for _, cl := range pm.Claims { + cl.BlobRef = c.br(cl.BlobRef) + cl.Signer = c.br(cl.Signer) + cl.Permanode = c.br(cl.Permanode) + cl.Target = c.br(cl.Target) + } + + } + c.brOfStr = nil // drop this now. + c.building = false + // log.V(1).Printf("interned blob.Ref = %d", c.brInterns) + + if err := c.initDeletes(s); err != nil { + return fmt.Errorf("Could not populate the corpus deletes: %v", err) + } + + if logCorpusStats { + cpu := osutil.CPUUsage() - cpu0 + ms1 := memstats() + memUsed := ms1.Alloc - ms0.Alloc + if ms1.Alloc < ms0.Alloc { + memUsed = 0 + } + log.Printf("Corpus stats: %.3f MiB mem: %d blobs (%.3f GiB) (%d schema (%d permanode, %d file (%d image), ...)", + float64(memUsed)/(1<<20), + len(c.blobs), + float64(c.sumBlobBytes)/(1<<30), + c.numSchemaBlobsLocked(), + len(c.permanodes), + len(c.files), + len(c.imageInfo)) + log.Printf("Corpus scanning CPU usage: %v", cpu) + } + + return nil +} + +// initDeletes populates the corpus deletes from the delete entries in s. +func (c *Corpus) initDeletes(s sorted.KeyValue) (err error) { + it := queryPrefix(s, keyDeleted) + defer closeIterator(it, &err) + for it.Next() { + cl, ok := kvDeleted(it.Key()) + if !ok { + return fmt.Errorf("Bogus keyDeleted entry key: want |\"deleted\"||||, got %q", it.Key()) + } + targetDeletions := append(c.deletes[cl.Target], + deletion{ + deleter: cl.BlobRef, + when: cl.Date, + }) + sort.Sort(sort.Reverse(byDeletionDate(targetDeletions))) + c.deletes[cl.Target] = targetDeletions + } + return err +} + +func (c *Corpus) numSchemaBlobsLocked() (n int64) { + for _, m := range c.camBlobs { + n += int64(len(m)) + } + return +} + +func (c *Corpus) scanPrefix(s sorted.KeyValue, prefix string) (err error) { + typeKey := typeOfKey(prefix) + fn, ok := corpusMergeFunc[typeKey] + if !ok { + panic("No registered merge func for prefix " + prefix) + } + + n, t0 := 0, time.Now() + it := queryPrefixString(s, prefix) + defer closeIterator(it, &err) + for it.Next() { + n++ + if n == 1 { + // Let the query be sent off and responses start flowing in before + // we take the lock. And if no rows: no lock. + c.mu.Lock() + defer c.mu.Unlock() + } + if err := fn(c, it.KeyBytes(), it.ValueBytes()); err != nil { + return err + } + } + if logCorpusStats { + d := time.Since(t0) + log.Printf("Scanned prefix %q: %d rows, %v", prefix[:len(prefix)-1], n, d) + } + return nil +} + +func (c *Corpus) addBlob(br blob.Ref, mm *mutationMap) error { + c.mu.Lock() + defer c.mu.Unlock() + if _, dup := c.blobs[br]; dup { + return nil + } + c.gen++ + for k, v := range mm.kv { + kt := typeOfKey(k) + if !slurpedKeyType[kt] { + continue + } + if err := corpusMergeFunc[kt](c, []byte(k), []byte(v)); err != nil { + return err + } + } + for _, cl := range mm.deletes { + if err := c.updateDeletes(cl); err != nil { + return fmt.Errorf("Could not update the deletes cache after deletion from %v: %v", cl, err) + } + } + return nil +} + +// updateDeletes updates the corpus deletes with the delete claim deleteClaim. +// deleteClaim is trusted to be a valid delete Claim. +func (c *Corpus) updateDeletes(deleteClaim schema.Claim) error { + target := c.br(deleteClaim.Target()) + deleter := deleteClaim.Blob() + when, err := deleter.ClaimDate() + if err != nil { + return fmt.Errorf("Could not get date of delete claim %v: %v", deleteClaim, err) + } + del := deletion{ + deleter: c.br(deleter.BlobRef()), + when: when, + } + for _, v := range c.deletes[target] { + if v == del { + return nil + } + } + targetDeletions := append(c.deletes[target], del) + sort.Sort(sort.Reverse(byDeletionDate(targetDeletions))) + c.deletes[target] = targetDeletions + return nil +} + +func (c *Corpus) mergeMetaRow(k, v []byte) error { + bm, ok := kvBlobMeta_bytes(k, v) + if !ok { + return fmt.Errorf("bogus meta row: %q -> %q", k, v) + } + return c.mergeBlobMeta(bm) +} + +func (c *Corpus) mergeBlobMeta(bm camtypes.BlobMeta) error { + if _, dup := c.blobs[bm.Ref]; dup { + panic("dup blob seen") + } + bm.CamliType = c.str(bm.CamliType) + + c.blobs[bm.Ref] = &bm + c.sumBlobBytes += int64(bm.Size) + if bm.CamliType != "" { + m, ok := c.camBlobs[bm.CamliType] + if !ok { + m = make(map[blob.Ref]*camtypes.BlobMeta) + c.camBlobs[bm.CamliType] = m + } + m[bm.Ref] = &bm + } + return nil +} + +func (c *Corpus) mergeSignerKeyIdRow(k, v []byte) error { + br, ok := blob.ParseBytes(k[len("signerkeyid:"):]) + if !ok { + return fmt.Errorf("bogus signerid row: %q -> %q", k, v) + } + c.keyId[br] = string(v) + return nil +} + +func (c *Corpus) mergeClaimRow(k, v []byte) error { + // TODO: update kvClaim to take []byte instead of string + cl, ok := kvClaim(string(k), string(v), c.blobParse) + if !ok || !cl.Permanode.Valid() { + return fmt.Errorf("bogus claim row: %q -> %q", k, v) + } + cl.Type = c.str(cl.Type) + cl.Attr = c.str(cl.Attr) + cl.Value = c.str(cl.Value) // less likely to intern, but some (tags) do + + pn := c.br(cl.Permanode) + pm, ok := c.permanodes[pn] + if !ok { + pm = new(PermanodeMeta) + c.permanodes[pn] = pm + } + pm.Claims = append(pm.Claims, &cl) + if !c.building { + // Unless we're still starting up (at which we sort at + // the end instead), keep this sorted. + sort.Sort(camtypes.ClaimPtrsByDate(pm.Claims)) + } + + if vbr, ok := blob.Parse(cl.Value); ok { + c.claimBack[vbr] = append(c.claimBack[vbr], &cl) + } + return nil +} + +func (c *Corpus) mergeFileInfoRow(k, v []byte) error { + // fileinfo|sha1-579f7f246bd420d486ddeb0dadbb256cfaf8bf6b" "5|some-stuff.txt|" + pipe := bytes.IndexByte(k, '|') + if pipe < 0 { + return fmt.Errorf("unexpected fileinfo key %q", k) + } + br, ok := blob.ParseBytes(k[pipe+1:]) + if !ok { + return fmt.Errorf("unexpected fileinfo blobref in key %q", k) + } + + // TODO: could at least use strutil.ParseUintBytes to not stringify and retain + // the length bytes of v. + c.ss = strutil.AppendSplitN(c.ss[:0], string(v), "|", 4) + if len(c.ss) != 3 && len(c.ss) != 4 { + return fmt.Errorf("unexpected fileinfo value %q", v) + } + size, err := strconv.ParseInt(c.ss[0], 10, 64) + if err != nil { + return fmt.Errorf("unexpected fileinfo value %q", v) + } + var wholeRef blob.Ref + if len(c.ss) == 4 && c.ss[3] != "" { // checking for "" because of special files such as symlinks. + var ok bool + wholeRef, ok = blob.Parse(urld(c.ss[3])) + if !ok { + return fmt.Errorf("invalid wholeRef blobref in value %q for fileinfo key %q", v, k) + } + } + c.mutateFileInfo(br, func(fi *camtypes.FileInfo) { + fi.Size = size + fi.FileName = c.str(urld(c.ss[1])) + fi.MIMEType = c.str(urld(c.ss[2])) + fi.WholeRef = wholeRef + }) + return nil +} + +func (c *Corpus) mergeFileTimesRow(k, v []byte) error { + if len(v) == 0 { + return nil + } + // "filetimes|sha1-579f7f246bd420d486ddeb0dadbb256cfaf8bf6b" "1970-01-01T00%3A02%3A03Z" + pipe := bytes.IndexByte(k, '|') + if pipe < 0 { + return fmt.Errorf("unexpected fileinfo key %q", k) + } + br, ok := blob.ParseBytes(k[pipe+1:]) + if !ok { + return fmt.Errorf("unexpected filetimes blobref in key %q", k) + } + c.ss = strutil.AppendSplitN(c.ss[:0], urld(string(v)), ",", -1) + times := c.ss + c.mutateFileInfo(br, func(fi *camtypes.FileInfo) { + updateFileInfoTimes(fi, times) + }) + return nil +} + +func (c *Corpus) mutateFileInfo(br blob.Ref, fn func(*camtypes.FileInfo)) { + br = c.br(br) + fi := c.files[br] // use zero value if not present + fn(&fi) + c.files[br] = fi +} + +func (c *Corpus) mergeImageSizeRow(k, v []byte) error { + br, okk := blob.ParseBytes(k[len("imagesize|"):]) + ii, okv := kvImageInfo(v) + if !okk || !okv { + return fmt.Errorf("bogus row %q = %q", k, v) + } + br = c.br(br) + c.imageInfo[br] = ii + return nil +} + +// "wholetofile|sha1-17b53c7c3e664d3613dfdce50ef1f2a09e8f04b5|sha1-fb88f3eab3acfcf3cfc8cd77ae4366f6f975d227" -> "1" +func (c *Corpus) mergeWholeToFileRow(k, v []byte) error { + pair := k[len("wholetofile|"):] + pipe := bytes.IndexByte(pair, '|') + if pipe < 0 { + return fmt.Errorf("bogus row %q = %q", k, v) + } + wholeRef, ok1 := blob.ParseBytes(pair[:pipe]) + fileRef, ok2 := blob.ParseBytes(pair[pipe+1:]) + if !ok1 || !ok2 { + return fmt.Errorf("bogus row %q = %q", k, v) + } + c.fileWholeRef[fileRef] = wholeRef + return nil +} + +// "mediatag|sha1-2b219be9d9691b4f8090e7ee2690098097f59566|album" = "Some+Album+Name" +func (c *Corpus) mergeMediaTag(k, v []byte) error { + f := strings.Split(string(k), "|") + if len(f) != 3 { + return fmt.Errorf("unexpected key %q", k) + } + wholeRef, ok := blob.Parse(f[1]) + if !ok { + return fmt.Errorf("failed to parse wholeref from key %q", k) + } + tm, ok := c.mediaTags[wholeRef] + if !ok { + tm = make(map[string]string) + c.mediaTags[wholeRef] = tm + } + tm[c.str(f[2])] = c.str(urld(string(v))) + return nil +} + +// "exifgps|sha1-17b53c7c3e664d3613dfdce50ef1f2a09e8f04b5" -> "-122.39897155555556|37.61952208333334" +func (c *Corpus) mergeEXIFGPSRow(k, v []byte) error { + wholeRef, ok := blob.ParseBytes(k[len("exifgps|"):]) + pipe := bytes.IndexByte(v, '|') + if pipe < 0 || !ok { + return fmt.Errorf("bogus row %q = %q", k, v) + } + lat, err := strconv.ParseFloat(string(v[:pipe]), 64) + long, err1 := strconv.ParseFloat(string(v[pipe+1:]), 64) + if err != nil || err1 != nil { + return fmt.Errorf("bogus row %q = %q", k, v) + } + c.gps[wholeRef] = latLong{lat, long} + return nil +} + +// This enables the blob.Parse fast path cache, which reduces CPU (via +// reduced GC from new garbage), but increases memory usage, even +// though it shouldn't. The GC should fully discard the brOfStr map +// (which we nil out at the end of parsing), but the Go GC doesn't +// seem to clear it all. +// TODO: investigate / file bugs. +const useBlobParseCache = false + +func (c *Corpus) blobParse(v string) (br blob.Ref, ok bool) { + if useBlobParseCache { + br, ok = c.brOfStr[v] + if ok { + return + } + } + return blob.Parse(v) +} + +// str returns s, interned. +func (c *Corpus) str(s string) string { + if s == "" { + return "" + } + if s, ok := c.strs[s]; ok { + return s + } + if c.strs == nil { + c.strs = make(map[string]string) + } + c.strs[s] = s + return s +} + +// br returns br, interned. +func (c *Corpus) br(br blob.Ref) blob.Ref { + if bm, ok := c.blobs[br]; ok { + c.brInterns++ + return bm.Ref + } + return br +} + +// *********** Reading from the corpus + +// EnumerateCamliBlobsLocked sends just camlistore meta blobs to ch. +// +// The Corpus must already be locked with RLock. +// +// If camType is empty, all camlistore blobs are sent, otherwise it specifies +// the camliType to send. +// ch is closed at the end. The err will either be nil or context.ErrCanceled. +func (c *Corpus) EnumerateCamliBlobsLocked(ctx *context.Context, camType string, ch chan<- camtypes.BlobMeta) error { + defer close(ch) + for t, m := range c.camBlobs { + if camType != "" && camType != t { + continue + } + for _, bm := range m { + select { + case ch <- *bm: + case <-ctx.Done(): + return context.ErrCanceled + } + } + } + return nil +} + +// EnumerateBlobMetaLocked sends all known blobs to ch, or until the context is canceled. +// +// The Corpus must already be locked with RLock. +func (c *Corpus) EnumerateBlobMetaLocked(ctx *context.Context, ch chan<- camtypes.BlobMeta) error { + defer close(ch) + for _, bm := range c.blobs { + select { + case ch <- *bm: + case <-ctx.Done(): + return context.ErrCanceled + } + } + return nil +} + +// pnAndTime is a value type wrapping a permanode blobref and its modtime. +// It's used by EnumeratePermanodesLastModified and EnumeratePermanodesCreated. +type pnAndTime struct { + pn blob.Ref + t time.Time +} + +type byPermanodeTime []pnAndTime + +func (s byPermanodeTime) Len() int { return len(s) } +func (s byPermanodeTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byPermanodeTime) Less(i, j int) bool { + if s[i].t.Equal(s[j].t) { + return s[i].pn.Less(s[j].pn) + } + return s[i].t.Before(s[j].t) +} + +type lazySortedPermanodes struct { + c *Corpus + pnTime func(blob.Ref) (time.Time, bool) // returns permanode's time (if any) to sort on + + mu sync.Mutex // guards sortedCache and ofGen + sortedCache []pnAndTime // nil if invalidated + sortedCacheReversed []pnAndTime // nil if invalidated + ofGen int64 // the Corpus.gen from which sortedCache was built +} + +func reversedCopy(original []pnAndTime) []pnAndTime { + l := len(original) + reversed := make([]pnAndTime, l) + for k, v := range original { + reversed[l-1-k] = v + } + return reversed +} + +// The Corpus must already be locked with RLock. +func (lsp *lazySortedPermanodes) sorted(reverse bool) []pnAndTime { + lsp.mu.Lock() + defer lsp.mu.Unlock() + if lsp.ofGen == lsp.c.gen { + // corpus hasn't changed -> caches are still valid, if they exist. + if reverse { + if lsp.sortedCacheReversed != nil { + return lsp.sortedCacheReversed + } + if lsp.sortedCache != nil { + // using sortedCache to quickly build sortedCacheReversed + lsp.sortedCacheReversed = reversedCopy(lsp.sortedCache) + return lsp.sortedCacheReversed + } + } + if !reverse { + if lsp.sortedCache != nil { + return lsp.sortedCache + } + if lsp.sortedCacheReversed != nil { + // using sortedCacheReversed to quickly build sortedCache + lsp.sortedCache = reversedCopy(lsp.sortedCacheReversed) + return lsp.sortedCache + } + } + } + // invalidate the caches + lsp.sortedCache = nil + lsp.sortedCacheReversed = nil + pns := make([]pnAndTime, 0, len(lsp.c.permanodes)) + for pn := range lsp.c.permanodes { + if lsp.c.IsDeletedLocked(pn) { + continue + } + if pt, ok := lsp.pnTime(pn); ok { + pns = append(pns, pnAndTime{pn, pt}) + } + } + // and rebuild one of them + if reverse { + sort.Sort(sort.Reverse(byPermanodeTime(pns))) + lsp.sortedCacheReversed = pns + } else { + sort.Sort(byPermanodeTime(pns)) + lsp.sortedCache = pns + } + lsp.ofGen = lsp.c.gen + return pns +} + +// corpus must be (read) locked. +func (c *Corpus) sendPermanodes(ctx *context.Context, ch chan<- camtypes.BlobMeta, pns []pnAndTime) error { + for _, cand := range pns { + bm := c.blobs[cand.pn] + if bm == nil { + continue + } + select { + case ch <- *bm: + continue + case <-ctx.Done(): + return context.ErrCanceled + } + } + return nil +} + +// EnumeratePermanodesLastModified sends all permanodes, sorted by most recently modified first, to ch, +// or until ctx is done. +// +// The Corpus must already be locked with RLock. +func (c *Corpus) EnumeratePermanodesLastModifiedLocked(ctx *context.Context, ch chan<- camtypes.BlobMeta) error { + defer close(ch) + + return c.sendPermanodes(ctx, ch, c.permanodesByModtime.sorted(true)) +} + +// EnumeratePermanodesCreatedLocked sends all permanodes to ch, or until ctx is done. +// They are sorted using the contents creation date if any, the permanode modtime +// otherwise, and in the order specified by newestFirst. +// +// The Corpus must already be locked with RLock. +func (c *Corpus) EnumeratePermanodesCreatedLocked(ctx *context.Context, ch chan<- camtypes.BlobMeta, newestFirst bool) error { + defer close(ch) + + return c.sendPermanodes(ctx, ch, c.permanodesByTime.sorted(newestFirst)) +} + +func (c *Corpus) GetBlobMeta(br blob.Ref) (camtypes.BlobMeta, error) { + c.mu.RLock() + defer c.mu.RUnlock() + return c.GetBlobMetaLocked(br) +} + +func (c *Corpus) GetBlobMetaLocked(br blob.Ref) (camtypes.BlobMeta, error) { + bm, ok := c.blobs[br] + if !ok { + return camtypes.BlobMeta{}, os.ErrNotExist + } + return *bm, nil +} + +func (c *Corpus) KeyId(signer blob.Ref) (string, error) { + c.mu.RLock() + defer c.mu.RUnlock() + if v, ok := c.keyId[signer]; ok { + return v, nil + } + return "", sorted.ErrNotFound +} + +var ( + errUnsupportedNodeType = errors.New("unsupported nodeType") + errNoNodeAttr = errors.New("attribute not found") +) + +func (c *Corpus) pnTimeAttrLocked(pn blob.Ref, attr string) (t time.Time, ok bool) { + if v := c.PermanodeAttrValueLocked(pn, attr, time.Time{}, blob.Ref{}); v != "" { + if t, err := time.Parse(time.RFC3339, v); err == nil { + return t, true + } + } + return +} + +// PermanodeTimeLocked returns the time of the content in permanode. +func (c *Corpus) PermanodeTimeLocked(pn blob.Ref) (t time.Time, ok bool) { + // TODO(bradfitz): keep this time property cached on the permanode / files + // TODO(bradfitz): finish implmenting all these + + // Priorities: + // -- Permanode explicit "camliTime" property + // -- EXIF GPS time + // -- Exif camera time - this one is actually already in the FileInfo, + // because we use schema.FileTime (which returns the EXIF time, if available) + // to index the time when receiving a file. + // -- File time + // -- File modtime + // -- camliContent claim set time + + if t, ok = c.pnTimeAttrLocked(pn, nodeattr.StartDate); ok { + return + } + if t, ok = c.pnTimeAttrLocked(pn, nodeattr.DateCreated); ok { + return + } + var fi camtypes.FileInfo + ccRef, ccTime, ok := c.pnCamliContentLocked(pn) + if ok { + fi, _ = c.files[ccRef] + } + if fi.Time != nil { + return time.Time(*fi.Time), true + } + + if t, ok = c.pnTimeAttrLocked(pn, nodeattr.DatePublished); ok { + return + } + if t, ok = c.pnTimeAttrLocked(pn, nodeattr.DateModified); ok { + return + } + if fi.ModTime != nil { + return time.Time(*fi.ModTime), true + } + if ok { + return ccTime, true + } + return time.Time{}, false +} + +// PermanodeAnyTimeLocked returns the time that best qualifies the permanode. +// It tries content-specific times first, the permanode modtime otherwise. +func (c *Corpus) PermanodeAnyTimeLocked(pn blob.Ref) (t time.Time, ok bool) { + if t, ok := c.PermanodeTimeLocked(pn); ok { + return t, ok + } + return c.PermanodeModtimeLocked(pn) +} + +func (c *Corpus) pnCamliContentLocked(pn blob.Ref) (cc blob.Ref, t time.Time, ok bool) { + // TODO(bradfitz): keep this property cached + pm, ok := c.permanodes[pn] + if !ok { + return + } + for _, cl := range pm.Claims { + if cl.Attr != "camliContent" { + continue + } + // TODO: pass down the 'PermanodeConstraint.At' parameter, and then do: if cl.Date.After(at) { continue } + switch cl.Type { + case string(schema.DelAttributeClaim): + cc = blob.Ref{} + t = time.Time{} + case string(schema.SetAttributeClaim): + cc = blob.ParseOrZero(cl.Value) + t = cl.Date + } + } + return cc, t, cc.Valid() + +} + +// PermanodeModtime returns the latest modification time of the given +// permanode. +// +// The ok value is true only if the permanode is known and has any +// non-deleted claims. A deleted claim is ignored and neither its +// claim date nor the date of the delete claim affect the modtime of +// the permanode. +func (c *Corpus) PermanodeModtime(pn blob.Ref) (t time.Time, ok bool) { + // TODO: figure out behavior wrt mutations by different people + c.mu.RLock() + defer c.mu.RUnlock() + return c.PermanodeModtimeLocked(pn) +} + +// PermanodeModtimeLocked is like PermanodeModtime but for when the Corpus is +// already locked via RLock. +func (c *Corpus) PermanodeModtimeLocked(pn blob.Ref) (t time.Time, ok bool) { + pm, ok := c.permanodes[pn] + if !ok { + return + } + + // Note: We intentionally don't try to derive any information + // (except the owner, elsewhere) from the permanode blob + // itself. Even though the permanode blob sometimes has the + // GPG signature time, we intentionally ignore it. + for _, cl := range pm.Claims { + if c.IsDeletedLocked(cl.BlobRef) { + continue + } + if cl.Date.After(t) { + t = cl.Date + } + } + return t, !t.IsZero() +} + +// AppendPermanodeAttrValues appends to dst all the values for the attribute +// attr set on permaNode. +// signerFilter is optional. +// dst must start with length 0 (laziness, mostly) +func (c *Corpus) AppendPermanodeAttrValues(dst []string, + permaNode blob.Ref, + attr string, + at time.Time, + signerFilter blob.Ref) []string { + c.mu.RLock() + defer c.mu.RUnlock() + return c.AppendPermanodeAttrValuesLocked(dst, permaNode, attr, at, signerFilter) +} + +// PermanodeAttrValueLocked returns a single-valued attribute or "". +func (c *Corpus) PermanodeAttrValueLocked(permaNode blob.Ref, + attr string, + at time.Time, + signerFilter blob.Ref) string { + pm, ok := c.permanodes[permaNode] + if !ok { + return "" + } + if at.IsZero() { + at = time.Now() + } + var v string + for _, cl := range pm.Claims { + if cl.Attr != attr || cl.Date.After(at) { + continue + } + if signerFilter.Valid() && signerFilter != cl.Signer { + continue + } + switch cl.Type { + case string(schema.DelAttributeClaim): + if cl.Value == "" { + v = "" + } else if v == cl.Value { + v = "" + } + case string(schema.SetAttributeClaim): + v = cl.Value + case string(schema.AddAttributeClaim): + if v == "" { + v = cl.Value + } + } + } + return v +} + +func (c *Corpus) AppendPermanodeAttrValuesLocked(dst []string, + permaNode blob.Ref, + attr string, + at time.Time, + signerFilter blob.Ref) []string { + if len(dst) > 0 { + panic("len(dst) must be 0") + } + pm, ok := c.permanodes[permaNode] + if !ok { + return dst + } + if at.IsZero() { + at = time.Now() + } + for _, cl := range pm.Claims { + if cl.Attr != attr || cl.Date.After(at) { + continue + } + if signerFilter.Valid() && signerFilter != cl.Signer { + continue + } + switch cl.Type { + case string(schema.DelAttributeClaim): + if cl.Value == "" { + dst = dst[:0] // delete all + } else { + for i := 0; i < len(dst); i++ { + v := dst[i] + if v == cl.Value { + copy(dst[i:], dst[i+1:]) + dst = dst[:len(dst)-1] + i-- + } + } + } + case string(schema.SetAttributeClaim): + dst = append(dst[:0], cl.Value) + case string(schema.AddAttributeClaim): + dst = append(dst, cl.Value) + } + } + return dst +} + +func (c *Corpus) AppendClaims(dst []camtypes.Claim, permaNode blob.Ref, + signerFilter blob.Ref, + attrFilter string) ([]camtypes.Claim, error) { + c.mu.RLock() + defer c.mu.RUnlock() + pm, ok := c.permanodes[permaNode] + if !ok { + return nil, nil + } + for _, cl := range pm.Claims { + if c.IsDeletedLocked(cl.BlobRef) { + continue + } + if signerFilter.Valid() && cl.Signer != signerFilter { + continue + } + if attrFilter != "" && cl.Attr != attrFilter { + continue + } + dst = append(dst, *cl) + } + return dst, nil +} + +func (c *Corpus) GetFileInfo(fileRef blob.Ref) (fi camtypes.FileInfo, err error) { + c.mu.RLock() + defer c.mu.RUnlock() + return c.GetFileInfoLocked(fileRef) +} + +func (c *Corpus) GetFileInfoLocked(fileRef blob.Ref) (fi camtypes.FileInfo, err error) { + fi, ok := c.files[fileRef] + if !ok { + err = os.ErrNotExist + } + return +} + +func (c *Corpus) GetImageInfo(fileRef blob.Ref) (ii camtypes.ImageInfo, err error) { + c.mu.RLock() + defer c.mu.RUnlock() + return c.GetImageInfoLocked(fileRef) +} + +func (c *Corpus) GetImageInfoLocked(fileRef blob.Ref) (ii camtypes.ImageInfo, err error) { + ii, ok := c.imageInfo[fileRef] + if !ok { + err = os.ErrNotExist + } + return +} + +func (c *Corpus) GetMediaTags(fileRef blob.Ref) (map[string]string, error) { + c.mu.RLock() + defer c.mu.RUnlock() + return c.GetMediaTagsLocked(fileRef) +} + +func (c *Corpus) GetMediaTagsLocked(fileRef blob.Ref) (map[string]string, error) { + wholeRef, ok := c.fileWholeRef[fileRef] + if !ok { + return nil, os.ErrNotExist + } + tags, ok := c.mediaTags[wholeRef] + if !ok { + return nil, os.ErrNotExist + } + return tags, nil +} + +func (c *Corpus) GetWholeRefLocked(fileRef blob.Ref) (wholeRef blob.Ref, ok bool) { + wholeRef, ok = c.fileWholeRef[fileRef] + return +} + +func (c *Corpus) FileLatLongLocked(fileRef blob.Ref) (lat, long float64, ok bool) { + wholeRef, ok := c.fileWholeRef[fileRef] + if !ok { + return + } + ll, ok := c.gps[wholeRef] + if !ok { + return + } + return ll.lat, ll.long, true +} + +// zero value of at means current +func (c *Corpus) PermanodeLatLongLocked(pn blob.Ref, at time.Time) (lat, long float64, ok bool) { + nodeType := c.PermanodeAttrValueLocked(pn, "camliNodeType", at, blob.Ref{}) + if nodeType == "" { + return + } + // TODO: make these pluggable, e.g. registered from an importer or something? + // How will that work when they're out-of-process? + if nodeType == "foursquare.com:checkin" { + venuePn, hasVenue := blob.Parse(c.PermanodeAttrValueLocked(pn, "foursquareVenuePermanode", at, blob.Ref{})) + if !hasVenue { + return + } + return c.PermanodeLatLongLocked(venuePn, at) + } + if nodeType == "foursquare.com:venue" || nodeType == "twitter.com:tweet" { + var err error + lat, err = strconv.ParseFloat(c.PermanodeAttrValueLocked(pn, "latitude", at, blob.Ref{}), 64) + if err != nil { + return + } + long, err = strconv.ParseFloat(c.PermanodeAttrValueLocked(pn, "longitude", at, blob.Ref{}), 64) + if err != nil { + return + } + return lat, long, true + } + return +} + +// ForeachClaimBackLocked calls fn for each claim with a value referencing br. +// If at is zero, all claims are yielded. +// If at is non-zero, claims after that point are skipped. +// If fn returns false, iteration ends. +// Iteration is in an undefined order. +func (c *Corpus) ForeachClaimBackLocked(value blob.Ref, at time.Time, fn func(*camtypes.Claim) bool) { + for _, cl := range c.claimBack[value] { + if !at.IsZero() && cl.Date.After(at) { + continue + } + if !fn(cl) { + return + } + } +} + +// PermanodeHasAttrValueLocked reports whether the permanode pn at +// time at (zero means now) has the given attribute with the given +// value. If the attribute is multi-valued, any may match. +func (c *Corpus) PermanodeHasAttrValueLocked(pn blob.Ref, at time.Time, attr, val string) bool { + pm, ok := c.permanodes[pn] + if !ok { + return false + } + if at.IsZero() { + at = time.Now() + } + ret := false + for _, cl := range pm.Claims { + if cl.Attr != attr { + continue + } + if cl.Date.After(at) { + break + } + switch cl.Type { + case string(schema.DelAttributeClaim): + if cl.Value == "" || cl.Value == val { + ret = false + } + case string(schema.SetAttributeClaim): + ret = (cl.Value == val) + case string(schema.AddAttributeClaim): + if cl.Value == val { + return true + } + } + } + return ret +} + +// SetVerboseCorpusLogging controls corpus setup verbosity. It's on by default +// but used to disable verbose logging in tests. +func SetVerboseCorpusLogging(v bool) { + logCorpusStats = v +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/corpus_bench_test.go b/vendor/github.com/camlistore/camlistore/pkg/index/corpus_bench_test.go new file mode 100644 index 00000000..835a79d1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/corpus_bench_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2013 The Camlistore AUTHORS + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index_test + +import ( + "fmt" + "sync" + "testing" + "time" + + "camlistore.org/pkg/index" + "camlistore.org/pkg/index/indextest" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/test" +) + +var ( + buildKvOnce sync.Once + kvForBenchmark sorted.KeyValue +) + +func BenchmarkCorpusFromStorage(b *testing.B) { + defer test.TLog(b)() + buildKvOnce.Do(func() { + kvForBenchmark = sorted.NewMemoryKeyValue() + idx, err := index.New(kvForBenchmark) + if err != nil { + b.Fatal(err) + } + id := indextest.NewIndexDeps(idx) + id.Fataler = b + for i := 0; i < 10; i++ { + fileRef, _ := id.UploadFile("file.txt", fmt.Sprintf("some file %d", i), time.Unix(1382073153, 0)) + pn := id.NewPlannedPermanode(fmt.Sprint(i)) + id.SetAttribute(pn, "camliContent", fileRef.String()) + } + }) + defer index.SetVerboseCorpusLogging(true) + index.SetVerboseCorpusLogging(false) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := index.NewCorpusFromStorage(kvForBenchmark) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/corpus_test.go b/vendor/github.com/camlistore/camlistore/pkg/index/corpus_test.go new file mode 100644 index 00000000..8227a0af --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/corpus_test.go @@ -0,0 +1,485 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index_test + +import ( + "fmt" + "reflect" + "testing" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/context" + "camlistore.org/pkg/index" + "camlistore.org/pkg/index/indextest" + "camlistore.org/pkg/types" + "camlistore.org/pkg/types/camtypes" +) + +func TestCorpusAppendPermanodeAttrValues(t *testing.T) { + c := index.ExpNewCorpus() + pn := blob.MustParse("abc-123") + tm := time.Unix(99, 0) + claim := func(verb, attr, val string) *camtypes.Claim { + tm = tm.Add(time.Second) + return &camtypes.Claim{ + Type: verb + "-attribute", + Attr: attr, + Value: val, + Date: tm, + } + } + s := func(s ...string) []string { return s } + + c.SetClaims(pn, &index.PermanodeMeta{ + Claims: []*camtypes.Claim{ + claim("set", "foo", "foov"), // time 100 + + claim("add", "tag", "a"), // time 101 + claim("add", "tag", "b"), // time 102 + claim("del", "tag", ""), + claim("add", "tag", "c"), + claim("add", "tag", "d"), + claim("add", "tag", "e"), + claim("del", "tag", "d"), + + claim("add", "DelAll", "a"), + claim("add", "DelAll", "b"), + claim("add", "DelAll", "c"), + claim("del", "DelAll", ""), + + claim("add", "DelOne", "a"), + claim("add", "DelOne", "b"), + claim("add", "DelOne", "c"), + claim("add", "DelOne", "d"), + claim("del", "DelOne", "d"), + claim("del", "DelOne", "a"), + + claim("add", "SetAfterAdd", "a"), + claim("add", "SetAfterAdd", "b"), + claim("set", "SetAfterAdd", "setv"), + }, + }) + + tests := []struct { + attr string + want []string + t time.Time + }{ + {attr: "not-exist", want: s()}, + {attr: "DelAll", want: s()}, + {attr: "DelOne", want: s("b", "c")}, + {attr: "foo", want: s("foov")}, + {attr: "tag", want: s("c", "e")}, + {attr: "tag", want: s("a", "b"), t: time.Unix(102, 0)}, + {attr: "SetAfterAdd", want: s("setv")}, + } + for i, tt := range tests { + got := c.AppendPermanodeAttrValues(nil, pn, tt.attr, tt.t, blob.Ref{}) + if len(got) == 0 && len(tt.want) == 0 { + continue + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("%d. attr %q = %q; want %q", + i, tt.attr, got, tt.want) + } + } + +} + +func TestKVClaimAllocs(t *testing.T) { + n := testing.AllocsPerRun(20, func() { + index.ExpKvClaim("claim|sha1-b380b3080f9c71faa5c1d82bbd4d583a473bc77d|2931A67C26F5ABDA|2011-11-28T01:32:37.000123456Z|sha1-b3d93daee62e40d36237ff444022f42d7d0e43f2", + "set-attribute|tag|foo1|sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007", + blob.Parse) + }) + t.Logf("%v allocations", n) +} + +func TestKVClaim(t *testing.T) { + tests := []struct { + k, v string + ok bool + want camtypes.Claim + }{ + { + k: "claim|sha1-b380b3080f9c71faa5c1d82bbd4d583a473bc77d|2931A67C26F5ABDA|2011-11-28T01:32:37.000123456Z|sha1-b3d93daee62e40d36237ff444022f42d7d0e43f2", + v: "set-attribute|tag|foo1|sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007", + ok: true, + want: camtypes.Claim{ + BlobRef: blob.MustParse("sha1-b3d93daee62e40d36237ff444022f42d7d0e43f2"), + Signer: blob.MustParse("sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007"), + Permanode: blob.MustParse("sha1-b380b3080f9c71faa5c1d82bbd4d583a473bc77d"), + Type: "set-attribute", + Attr: "tag", + Value: "foo1", + Date: time.Time(types.ParseTime3339OrZero("2011-11-28T01:32:37.000123456Z")), + }, + }, + } + for _, tt := range tests { + got, ok := index.ExpKvClaim(tt.k, tt.v, blob.Parse) + if ok != tt.ok { + t.Errorf("kvClaim(%q, %q) = ok %v; want %v", tt.k, tt.v, ok, tt.ok) + continue + } + if got != tt.want { + t.Errorf("kvClaim(%q, %q) = %+v; want %+v", tt.k, tt.v, got, tt.want) + continue + } + } +} + +func TestDeletePermanode_Modtime(t *testing.T) { + testDeletePermanodes(t, + func(c *index.Corpus, ctx *context.Context, ch chan<- camtypes.BlobMeta) error { + return c.EnumeratePermanodesLastModifiedLocked(ctx, ch) + }, + ) +} + +func TestDeletePermanode_CreateTime(t *testing.T) { + testDeletePermanodes(t, + func(c *index.Corpus, ctx *context.Context, ch chan<- camtypes.BlobMeta) error { + return c.EnumeratePermanodesCreatedLocked(ctx, ch, true) + }, + ) +} + +func testDeletePermanodes(t *testing.T, + enumFunc func(*index.Corpus, *context.Context, chan<- camtypes.BlobMeta) error) { + idx := index.NewMemoryIndex() + idxd := indextest.NewIndexDeps(idx) + + foopn := idxd.NewPlannedPermanode("foo") + idxd.SetAttribute(foopn, "tag", "foo") + barpn := idxd.NewPlannedPermanode("bar") + idxd.SetAttribute(barpn, "tag", "bar") + bazpn := idxd.NewPlannedPermanode("baz") + idxd.SetAttribute(bazpn, "tag", "baz") + idxd.Delete(barpn) + c, err := idxd.Index.KeepInMemory() + if err != nil { + t.Fatalf("error slurping index to memory: %v", err) + } + + // check that we initially only find permanodes foo and baz, + // because bar is already marked as deleted. + want := []blob.Ref{foopn, bazpn} + ch := make(chan camtypes.BlobMeta, 10) + var got []camtypes.BlobMeta + errc := make(chan error, 1) + c.RLock() + go func() { errc <- enumFunc(c, context.TODO(), ch) }() + for blobMeta := range ch { + got = append(got, blobMeta) + } + err = <-errc + c.RUnlock() + if err != nil { + t.Fatalf("Could not enumerate permanodes: %v", err) + } + if len(got) != len(want) { + t.Fatalf("Saw %d permanodes in corpus; want %d", len(got), len(want)) + } + for _, bm := range got { + found := false + for _, perm := range want { + if bm.Ref == perm { + found = true + break + } + } + if !found { + t.Fatalf("permanode %v was not found in corpus", bm.Ref) + } + } + + // now add a delete claim for permanode baz, and check that we're only left with foo permanode + delbaz := idxd.Delete(bazpn) + want = []blob.Ref{foopn} + got = got[:0] + ch = make(chan camtypes.BlobMeta, 10) + c.RLock() + go func() { errc <- enumFunc(c, context.TODO(), ch) }() + for blobMeta := range ch { + got = append(got, blobMeta) + } + err = <-errc + c.RUnlock() + if err != nil { + t.Fatalf("Could not enumerate permanodes: %v", err) + } + if len(got) != len(want) { + t.Fatalf("Saw %d permanodes in corpus; want %d", len(got), len(want)) + } + if got[0].Ref != foopn { + t.Fatalf("Wrong permanode found in corpus. Wanted %v, got %v", foopn, got[0].Ref) + } + + // baz undeletion. delete delbaz. + idxd.Delete(delbaz) + want = []blob.Ref{foopn, bazpn} + got = got[:0] + ch = make(chan camtypes.BlobMeta, 10) + c.RLock() + go func() { errc <- enumFunc(c, context.TODO(), ch) }() + for blobMeta := range ch { + got = append(got, blobMeta) + } + err = <-errc + c.RUnlock() + if err != nil { + t.Fatalf("Could not enumerate permanodes: %v", err) + } + if len(got) != len(want) { + t.Fatalf("Saw %d permanodes in corpus; want %d", len(got), len(want)) + } + for _, bm := range got { + found := false + for _, perm := range want { + if bm.Ref == perm { + found = true + break + } + } + if !found { + t.Fatalf("permanode %v was not found in corpus", bm.Ref) + } + } +} + +func TestEnumerateOrder_Modtime(t *testing.T) { + testEnumerateOrder(t, + func(c *index.Corpus, ctx *context.Context, ch chan<- camtypes.BlobMeta) error { + return c.EnumeratePermanodesLastModifiedLocked(ctx, ch) + }, + modtimeOrder, + ) +} + +func TestEnumerateOrder_CreateTime(t *testing.T) { + testEnumerateOrder(t, + func(c *index.Corpus, ctx *context.Context, ch chan<- camtypes.BlobMeta) error { + return c.EnumeratePermanodesCreatedLocked(ctx, ch, true) + }, + createOrder, + ) +} + +const ( + modtimeOrder = iota + createOrder +) + +func testEnumerateOrder(t *testing.T, + enumFunc func(*index.Corpus, *context.Context, chan<- camtypes.BlobMeta) error, + order int) { + idx := index.NewMemoryIndex() + idxd := indextest.NewIndexDeps(idx) + + // permanode with no contents + foopn := idxd.NewPlannedPermanode("foo") + idxd.SetAttribute(foopn, "tag", "foo") + // permanode with file contents + // we set the time of the contents 1 second older than the modtime of foopn + fooModTime := idxd.LastTime() + fileTime := fooModTime.Add(-1 * time.Second) + fileRef, _ := idxd.UploadFile("foo.html", "I am an html file.", fileTime) + barpn := idxd.NewPlannedPermanode("bar") + idxd.SetAttribute(barpn, "camliContent", fileRef.String()) + + c, err := idxd.Index.KeepInMemory() + if err != nil { + t.Fatalf("error slurping index to memory: %v", err) + } + + // check that we get a different order whether with enumerate according to + // contents time, or to permanode modtime. + var want []blob.Ref + if order == modtimeOrder { + // modtime. + want = []blob.Ref{barpn, foopn} + } else { + // creation time. + want = []blob.Ref{foopn, barpn} + } + ch := make(chan camtypes.BlobMeta, 10) + var got []camtypes.BlobMeta + errc := make(chan error, 1) + c.RLock() + go func() { errc <- enumFunc(c, context.TODO(), ch) }() + for blobMeta := range ch { + got = append(got, blobMeta) + } + err = <-errc + c.RUnlock() + if err != nil { + t.Fatalf("Could not enumerate permanodes: %v", err) + } + if len(got) != len(want) { + t.Fatalf("Saw %d permanodes in corpus; want %d", len(got), len(want)) + } + for k, v := range got { + if v.Ref != want[k] { + t.Fatalf("Wrong result from enumeration. Got %v, wanted %v.", v.Ref, want[k]) + } + } +} + +// should be run with -race +func TestCacheSortedPermanodes_ModtimeRace(t *testing.T) { + testCacheSortedPermanodesRace(t, + func(c *index.Corpus, ctx *context.Context, ch chan<- camtypes.BlobMeta) error { + return c.EnumeratePermanodesLastModifiedLocked(ctx, ch) + }, + ) +} + +// should be run with -race +func TestCacheSortedPermanodes_CreateTimeRace(t *testing.T) { + testCacheSortedPermanodesRace(t, + func(c *index.Corpus, ctx *context.Context, ch chan<- camtypes.BlobMeta) error { + return c.EnumeratePermanodesCreatedLocked(ctx, ch, true) + }, + ) +} + +func testCacheSortedPermanodesRace(t *testing.T, + enumFunc func(*index.Corpus, *context.Context, chan<- camtypes.BlobMeta) error) { + idx := index.NewMemoryIndex() + idxd := indextest.NewIndexDeps(idx) + idxd.Fataler = t + c, err := idxd.Index.KeepInMemory() + if err != nil { + t.Fatalf("error slurping index to memory: %v", err) + } + donec := make(chan struct{}) + go func() { + for i := 0; i < 100; i++ { + nth := fmt.Sprintf("%d", i) + pn := idxd.NewPlannedPermanode(nth) + idxd.SetAttribute(pn, "tag", nth) + } + donec <- struct{}{} + }() + go func() { + for i := 0; i < 10; i++ { + ch := make(chan camtypes.BlobMeta, 10) + errc := make(chan error, 1) + c.RLock() + go func() { errc <- enumFunc(c, context.TODO(), ch) }() + for _ = range ch { + } + err := <-errc + c.RUnlock() + if err != nil { + t.Fatalf("Could not enumerate permanodes: %v", err) + } + } + donec <- struct{}{} + }() + <-donec + <-donec +} + +func TestLazySortedPermanodes(t *testing.T) { + idx := index.NewMemoryIndex() + idxd := indextest.NewIndexDeps(idx) + idxd.Fataler = t + c, err := idxd.Index.KeepInMemory() + if err != nil { + t.Fatalf("error slurping index to memory: %v", err) + } + + lsp := c.Exp_LSPByTime(false) + if len(lsp) != 0 { + t.Fatal("LazySortedPermanodes cache should be empty on startup") + } + + pn := idxd.NewPlannedPermanode("one") + idxd.SetAttribute(pn, "tag", "one") + + enum := func(reverse bool) { + ch := make(chan camtypes.BlobMeta, 10) + errc := make(chan error, 1) + c.RLock() + go func() { errc <- c.EnumeratePermanodesCreatedLocked(context.TODO(), ch, reverse) }() + for _ = range ch { + } + err := <-errc + c.RUnlock() + if err != nil { + t.Fatalf("Could not enumerate permanodes: %v", err) + } + } + enum(false) + lsp = c.Exp_LSPByTime(false) + if len(lsp) != 1 { + t.Fatalf("LazySortedPermanodes after 1st enum: got %v items, wanted 1", len(lsp)) + } + lsp = c.Exp_LSPByTime(true) + if len(lsp) != 0 { + t.Fatalf("LazySortedPermanodes reversed after 1st enum: got %v items, wanted 0", len(lsp)) + } + + enum(true) + lsp = c.Exp_LSPByTime(false) + if len(lsp) != 1 { + t.Fatalf("LazySortedPermanodes after 2nd enum: got %v items, wanted 1", len(lsp)) + } + lsp = c.Exp_LSPByTime(true) + if len(lsp) != 1 { + t.Fatalf("LazySortedPermanodes reversed after 2nd enum: got %v items, wanted 1", len(lsp)) + } + + pn = idxd.NewPlannedPermanode("two") + idxd.SetAttribute(pn, "tag", "two") + + enum(true) + lsp = c.Exp_LSPByTime(false) + if len(lsp) != 0 { + t.Fatalf("LazySortedPermanodes after 2nd permanode: got %v items, wanted 0 because of cache invalidation", len(lsp)) + } + lsp = c.Exp_LSPByTime(true) + if len(lsp) != 2 { + t.Fatalf("LazySortedPermanodes reversed after 2nd permanode: got %v items, wanted 2", len(lsp)) + } + + pn = idxd.NewPlannedPermanode("three") + idxd.SetAttribute(pn, "tag", "three") + + enum(false) + lsp = c.Exp_LSPByTime(true) + if len(lsp) != 0 { + t.Fatalf("LazySortedPermanodes reversed after 3rd permanode: got %v items, wanted 0 because of cache invalidation", len(lsp)) + } + lsp = c.Exp_LSPByTime(false) + if len(lsp) != 3 { + t.Fatalf("LazySortedPermanodes after 3rd permanode: got %v items, wanted 3", len(lsp)) + } + + enum(true) + lsp = c.Exp_LSPByTime(false) + if len(lsp) != 3 { + t.Fatalf("LazySortedPermanodes after 5th enum: got %v items, wanted 3", len(lsp)) + } + lsp = c.Exp_LSPByTime(true) + if len(lsp) != 3 { + t.Fatalf("LazySortedPermanodes reversed after 5th enum: got %v items, wanted 3", len(lsp)) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/doc.go b/vendor/github.com/camlistore/camlistore/pkg/index/doc.go new file mode 100644 index 00000000..bd61bcbe --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/doc.go @@ -0,0 +1,46 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package index provides a generic indexing system on top of the abstract Storage interface. + +The following keys & values are populated by receiving blobs and queried +for search operations: + + * Recent Permanodes + "recpn|||" -> "" + where reverse-modtime flips each digit to '9'- and prepends "rt" (for reverse time) + "2011-11-27T01:23:45Z" = "rt7988-88-72T98:76:54Z" + + * signer blobref of ascii public key -> gpg key id + "signerkeyid:sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007" = "2931A67C26F5ABDA" + + * PermanodeOfSignerAttrValue: + "signerattrvalue|||||" -> "" + e.g. + "signerattrvalue|2931A67C26F5ABDA|camliRoot|rootval|"+ + "rt7988-88-71T98:67:60.999876543Z|sha1-bf115940641f1aae2e007edcf36b3b18c17256d9" = + "sha1-7a14cce982aa73ab519e63050f82e2a2adfcf039" + + * Other: + "meta:" -> "|" + "have:" -> "" (used for enumeration, which doesn't need mime type) + + * For GetOwnerClaims(permanode, signer): + "claim||||" -> "||" + +*/ +package index diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/enumstat.go b/vendor/github.com/camlistore/camlistore/pkg/index/enumstat.go new file mode 100644 index 00000000..969161f9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/enumstat.go @@ -0,0 +1,96 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "fmt" + "strconv" + "strings" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/context" + "camlistore.org/pkg/sorted" +) + +func (ix *Index) EnumerateBlobs(ctx *context.Context, dest chan<- blob.SizedRef, after string, limit int) (err error) { + defer close(dest) + it := ix.s.Find("have:"+after, "have~") + defer func() { + closeErr := it.Close() + if err == nil { + err = closeErr + } + }() + + afterKey := "have:" + after + n := int(0) + for n < limit && it.Next() { + k := it.Key() + if k <= afterKey { + continue + } + if !strings.HasPrefix(k, "have:") { + break + } + n++ + br, ok := blob.Parse(k[len("have:"):]) + if !ok { + continue + } + size, err := parseHaveVal(it.Value()) + if err == nil { + select { + case dest <- blob.SizedRef{br, uint32(size)}: + case <-ctx.Done(): + return context.ErrCanceled + } + } + } + return nil +} + +func (ix *Index) StatBlobs(dest chan<- blob.SizedRef, blobs []blob.Ref) error { + for _, br := range blobs { + key := "have:" + br.String() + v, err := ix.s.Get(key) + if err == sorted.ErrNotFound { + continue + } + if err != nil { + return fmt.Errorf("error looking up key %q: %v", key, err) + } + size, err := parseHaveVal(v) + if err != nil { + return fmt.Errorf("invalid size for key %q = %q", key, v) + } + dest <- blob.SizedRef{br, uint32(size)} + } + return nil +} + +// parseHaveVal takes the value part of an "have" index row and returns +// the blob size found in that value. Examples: +// parseHaveVal("324|indexed") == 324 +// parseHaveVal("654") == 654 +func parseHaveVal(val string) (size uint64, err error) { + pipei := strings.Index(val, "|") + if pipei >= 0 { + // filter out the "indexed" suffix + val = val[:pipei] + } + return strconv.ParseUint(val, 10, 32) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/export_test.go b/vendor/github.com/camlistore/camlistore/pkg/index/export_test.go new file mode 100644 index 00000000..d2148d20 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/export_test.go @@ -0,0 +1,121 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "testing" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/types/camtypes" +) + +func ExpReverseTimeString(s string) string { + return reverseTimeString(s) +} + +func ExpUnreverseTimeString(s string) string { + return unreverseTimeString(s) +} + +func ExpNewCorpus() *Corpus { + return newCorpus() +} + +func (c *Corpus) Exp_mergeFileInfoRow(k, v string) error { + return c.mergeFileInfoRow([]byte(k), []byte(v)) +} + +func (c *Corpus) Exp_files(br blob.Ref) camtypes.FileInfo { + return c.files[br] +} + +func ExpKvClaim(k, v string, blobParse func(string) (blob.Ref, bool)) (c camtypes.Claim, ok bool) { + return kvClaim(k, v, blobParse) +} + +func (c *Corpus) SetClaims(pn blob.Ref, claims *PermanodeMeta) { + c.permanodes[pn] = claims +} + +func (x *Index) NeededMapsForTest() (needs, neededBy map[blob.Ref][]blob.Ref, ready map[blob.Ref]bool) { + return x.needs, x.neededBy, x.readyReindex +} + +func Exp_missingKey(have, missing blob.Ref) string { + return keyMissing.Key(have, missing) +} + +func Exp_schemaVersion() int { return requiredSchemaVersion } + +func (x *Index) Exp_noteBlobIndexed(br blob.Ref) { + x.noteBlobIndexed(br) +} + +func (x *Index) Exp_AwaitReindexing(t *testing.T) { + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + x.mu.Lock() + n := len(x.readyReindex) + x.mu.Unlock() + if n == 0 { + return + } + time.Sleep(50 * time.Millisecond) + } + t.Fatal("timeout waiting for readyReindex to drain") +} + +type ExpPnAndTime pnAndTime + +// Exp_LSPByTime returns the sorted cache lazySortedPermanodes for +// permanodesByTime (or the reverse sorted one). +func (c *Corpus) Exp_LSPByTime(reverse bool) []ExpPnAndTime { + if c.permanodesByTime == nil { + return nil + } + var pn []ExpPnAndTime + if reverse { + if c.permanodesByTime.sortedCacheReversed != nil { + for _, v := range c.permanodesByTime.sortedCacheReversed { + pn = append(pn, ExpPnAndTime(v)) + } + return pn + } + } else { + if c.permanodesByTime.sortedCache != nil { + for _, v := range c.permanodesByTime.sortedCache { + pn = append(pn, ExpPnAndTime(v)) + } + return pn + } + } + return nil +} + +func (x *Index) Exp_BlobSource() blobserver.FetcherEnumerator { + x.mu.Lock() + defer x.mu.Unlock() + return x.blobSource +} + +func (x *Index) Exp_FixMissingWholeRef(fetcher blob.Fetcher) (err error) { + return x.fixMissingWholeRef(fetcher) +} + +var Exp_ErrMissingWholeRef = errMissingWholeRef diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/index.go b/vendor/github.com/camlistore/camlistore/pkg/index/index.go new file mode 100644 index 00000000..420ef5ed --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/index.go @@ -0,0 +1,1516 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "bytes" + "errors" + "fmt" + "io" + "log" + "os" + "sort" + "strconv" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/context" + "camlistore.org/pkg/env" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/strutil" + "camlistore.org/pkg/types" + "camlistore.org/pkg/types/camtypes" +) + +func init() { + blobserver.RegisterStorageConstructor("index", newFromConfig) +} + +type Index struct { + *blobserver.NoImplStorage + + s sorted.KeyValue + + KeyFetcher blob.Fetcher // for verifying claims + + // TODO(mpl): do not init and use deletes when we have a corpus. Since corpus has its own deletes now, they are redundant. + + // deletes is a cache to keep track of the deletion status (deleted vs undeleted) + // of the blobs in the index. It makes for faster reads than the otherwise + // recursive calls on the index. + deletes *deletionCache + + corpus *Corpus // or nil, if not being kept in memory + + mu sync.RWMutex // guards following + // needs maps from a blob to the missing blobs it needs to + // finish indexing. + needs map[blob.Ref][]blob.Ref + // neededBy is the inverse of needs. The keys are missing blobs + // and the value(s) are blobs waiting to be reindexed. + neededBy map[blob.Ref][]blob.Ref + readyReindex map[blob.Ref]bool // set of things ready to be re-indexed + oooRunning bool // whether outOfOrderIndexerLoop is running. + // blobSource is used for fetching blobs when indexing files and other + // blobs types that reference other objects. + // The only write access to blobSource should be its initialization (transition + // from nil to non-nil), once, and protected by mu. + blobSource blobserver.FetcherEnumerator + + tickleOoo chan bool // tickle out-of-order reindex loop, whenever readyReindex is added to +} + +var ( + _ blobserver.Storage = (*Index)(nil) + _ Interface = (*Index)(nil) +) + +var aboutToReindex = false + +// SetImpendingReindex notes that the user ran the camlistored binary with the --reindex flag. +// Because the index is about to be wiped, schema version checks should be suppressed. +func SetImpendingReindex() { + // TODO: remove this function, once we refactor how indexes are created. + // They'll probably not all have their own storage constructor registered. + aboutToReindex = true +} + +// MustNew is wraps New and fails with a Fatal error on t if New +// returns an error. +func MustNew(t types.TB, s sorted.KeyValue) *Index { + ix, err := New(s) + if err != nil { + t.Fatalf("Error creating index: %v", err) + } + return ix +} + +// InitBlobSource sets the index's blob source and starts the background +// out-of-order indexing loop. It panics if the blobSource is already set. +// If the index's key fetcher is nil, it is also set to the blobSource +// argument. +func (x *Index) InitBlobSource(blobSource blobserver.FetcherEnumerator) { + x.mu.Lock() + defer x.mu.Unlock() + if x.blobSource != nil { + panic("blobSource of Index already set") + } + x.blobSource = blobSource + if x.oooRunning { + panic("outOfOrderIndexerLoop should never have previously started without a blobSource") + } + if x.KeyFetcher == nil { + x.KeyFetcher = blobSource + } + if disableOoo, _ := strconv.ParseBool(os.Getenv("CAMLI_TESTREINDEX_DISABLE_OOO")); disableOoo { + // For Reindex test in pkg/index/indextest/tests.go + return + } + go x.outOfOrderIndexerLoop() +} + +// New returns a new index using the provided key/value storage implementation. +func New(s sorted.KeyValue) (*Index, error) { + idx := &Index{ + s: s, + needs: make(map[blob.Ref][]blob.Ref), + neededBy: make(map[blob.Ref][]blob.Ref), + readyReindex: make(map[blob.Ref]bool), + tickleOoo: make(chan bool, 1), + } + if aboutToReindex { + idx.deletes = newDeletionCache() + return idx, nil + } + + schemaVersion := idx.schemaVersion() + switch { + case schemaVersion == 0 && idx.isEmpty(): + // New index. + err := idx.s.Set(keySchemaVersion.name, fmt.Sprint(requiredSchemaVersion)) + if err != nil { + return nil, fmt.Errorf("Could not write index schema version %q: %v", requiredSchemaVersion, err) + } + case schemaVersion != requiredSchemaVersion: + tip := "" + if env.IsDev() { + // Good signal that we're using the devcam server, so help out + // the user with a more useful tip: + tip = `(For the dev server, run "devcam server --wipe" to wipe both your blobs and index)` + } else { + if is4To5SchemaBump(schemaVersion) { + return idx, errMissingWholeRef + } + tip = "Run 'camlistored --reindex' (it might take awhile, but shows status). Alternative: 'camtool dbinit' (or just delete the file for a file based index), and then 'camtool sync --all'" + } + return nil, fmt.Errorf("index schema version is %d; required one is %d. You need to reindex. %s", + schemaVersion, requiredSchemaVersion, tip) + } + if err := idx.initDeletesCache(); err != nil { + return nil, fmt.Errorf("Could not initialize index's deletes cache: %v", err) + } + if err := idx.initNeededMaps(); err != nil { + return nil, fmt.Errorf("Could not initialize index's missing blob maps: %v", err) + } + return idx, nil +} + +func is4To5SchemaBump(schemaVersion int) bool { + return schemaVersion == 4 && requiredSchemaVersion == 5 +} + +var errMissingWholeRef = errors.New("missing wholeRef field in fileInfo rows") + +// fixMissingWholeRef appends the wholeRef to all the keyFileInfo rows values. It should +// only be called to upgrade a version 4 index schema to version 5. +func (x *Index) fixMissingWholeRef(fetcher blob.Fetcher) (err error) { + // We did that check from the caller, but double-check again to prevent from misuse + // of that function. + if x.schemaVersion() != 4 || requiredSchemaVersion != 5 { + panic("fixMissingWholeRef should only be used when upgrading from v4 to v5 of the index schema") + } + log.Println("index: fixing the missing wholeRef in the fileInfo rows...") + defer func() { + if err != nil { + log.Printf("index: fixing the fileInfo rows failed: %v", err) + return + } + log.Print("index: successfully fixed wholeRef in FileInfo rows.") + }() + + // first build a reverted keyWholeToFileRef map, so we can get the wholeRef from the fileRef easily. + fileRefToWholeRef := make(map[blob.Ref]blob.Ref) + it := x.queryPrefix(keyWholeToFileRef) + var keyA [3]string + for it.Next() { + keyPart := strutil.AppendSplitN(keyA[:0], it.Key(), "|", 3) + if len(keyPart) != 3 { + return fmt.Errorf("bogus keyWholeToFileRef key: got %q, wanted \"wholetofile|wholeRef|fileRef\"", it.Key()) + } + wholeRef, ok1 := blob.Parse(keyPart[1]) + fileRef, ok2 := blob.Parse(keyPart[2]) + if !ok1 || !ok2 { + return fmt.Errorf("bogus part in keyWholeToFileRef key: %q", it.Key()) + } + fileRefToWholeRef[fileRef] = wholeRef + } + if err := it.Close(); err != nil { + return err + } + + // We record the mutations and set them all after the iteration because of the sqlite locking: + // since BeginBatch takes a lock, and Find too, we would deadlock at queryPrefix if we + // started a batch mutation before. + mutations := make(map[string]string) + keyPrefix := keyFileInfo.name + "|" + it = x.queryPrefix(keyFileInfo) + defer it.Close() + var valA [3]string + for it.Next() { + br, ok := blob.ParseBytes(it.KeyBytes()[len(keyPrefix):]) + if !ok { + return fmt.Errorf("invalid blobRef %q", it.KeyBytes()[len(keyPrefix):]) + } + wholeRef, ok := fileRefToWholeRef[br] + if !ok { + log.Printf("WARNING: wholeRef for %v not found in index. You should probably rebuild the whole index.", br) + continue + } + valPart := strutil.AppendSplitN(valA[:0], it.Value(), "|", 3) + // The old format we're fixing should be: size|filename|mimetype + if len(valPart) != 3 { + return fmt.Errorf("bogus keyFileInfo value: got %q, wanted \"size|filename|mimetype\"", it.Value()) + } + size_s, filename, mimetype := valPart[0], valPart[1], urld(valPart[2]) + if strings.Contains(mimetype, "|") { + // I think this can only happen for people migrating from a commit at least as recent as + // 8229c1985079681a652cb65551b4e80a10d135aa, when wholeRef was introduced to keyFileInfo + // but there was no migration code yet. + // For the "production" migrations between 0.8 and 0.9, the index should not have any wholeRef + // in the keyFileInfo entries. So if something goes wrong and is somehow linked to that happening, + // I'd like to know about it, hence the logging. + log.Printf("%v: %v already has a wholeRef, not fixing it", it.Key(), it.Value()) + continue + } + size, err := strconv.Atoi(size_s) + if err != nil { + return fmt.Errorf("bogus size in keyFileInfo value %v: %v", it.Value(), err) + } + mutations[keyFileInfo.Key(br)] = keyFileInfo.Val(size, filename, mimetype, wholeRef) + } + if err := it.Close(); err != nil { + return err + } + bm := x.s.BeginBatch() + for k, v := range mutations { + bm.Set(k, v) + } + bm.Set(keySchemaVersion.name, "5") + if err := x.s.CommitBatch(bm); err != nil { + return err + } + return nil +} + +func newFromConfig(ld blobserver.Loader, config jsonconfig.Obj) (blobserver.Storage, error) { + blobPrefix := config.RequiredString("blobSource") + kvConfig := config.RequiredObject("storage") + if err := config.Validate(); err != nil { + return nil, err + } + kv, err := sorted.NewKeyValue(kvConfig) + if err != nil { + return nil, err + } + sto, err := ld.GetStorage(blobPrefix) + if err != nil { + return nil, err + } + + ix, err := New(kv) + // TODO(mpl): next time we need to do another fix, make a new error + // type that lets us apply the needed fix depending on its value or + // something. For now just one value/fix. + if err == errMissingWholeRef { + // TODO: maybe we don't want to do that automatically. Brad says + // we have to think about the case on GCE/CoreOS in particular. + if err := ix.fixMissingWholeRef(sto); err != nil { + ix.Close() + return nil, fmt.Errorf("could not fix missing wholeRef entries: %v", err) + } + ix, err = New(kv) + } + if err != nil { + return nil, err + } + ix.InitBlobSource(sto) + + return ix, err +} + +func (x *Index) String() string { + return fmt.Sprintf("Camlistore index, using key/value implementation %T", x.s) +} + +func (x *Index) isEmpty() bool { + iter := x.s.Find("", "") + hasRows := iter.Next() + if err := iter.Close(); err != nil { + panic(err) + } + return !hasRows +} + +// reindexMaxProcs is the number of concurrent goroutines that will be used for reindexing. +var reindexMaxProcs = struct { + sync.RWMutex + v int +}{v: 4} + +// SetReindexMaxProcs sets the maximum number of concurrent goroutines that are +// used during reindexing. +func SetReindexMaxProcs(n int) { + reindexMaxProcs.Lock() + defer reindexMaxProcs.Unlock() + reindexMaxProcs.v = n +} + +// ReindexMaxProcs returns the maximum number of concurrent goroutines that are +// used during reindexing. +func ReindexMaxProcs() int { + reindexMaxProcs.RLock() + defer reindexMaxProcs.RUnlock() + return reindexMaxProcs.v +} + +func (x *Index) Reindex() error { + reindexMaxProcs.RLock() + defer reindexMaxProcs.RUnlock() + ctx := context.TODO() + + wiper, ok := x.s.(sorted.Wiper) + if !ok { + return fmt.Errorf("index's storage type %T doesn't support sorted.Wiper", x.s) + } + log.Printf("Wiping index storage type %T ...", x.s) + if err := wiper.Wipe(); err != nil { + return fmt.Errorf("error wiping index's sorted key/value type %T: %v", x.s, err) + } + log.Printf("Index wiped. Rebuilding...") + + reindexStart, _ := blob.Parse(os.Getenv("CAMLI_REINDEX_START")) + + err := x.s.Set(keySchemaVersion.name, fmt.Sprintf("%d", requiredSchemaVersion)) + if err != nil { + return err + } + + var nerrmu sync.Mutex + nerr := 0 + + blobc := make(chan blob.Ref, 32) + + enumCtx := ctx.New() + enumErr := make(chan error, 1) + go func() { + defer close(blobc) + donec := enumCtx.Done() + var lastTick time.Time + enumErr <- blobserver.EnumerateAll(enumCtx, x.blobSource, func(sb blob.SizedRef) error { + now := time.Now() + if lastTick.Before(now.Add(-1 * time.Second)) { + log.Printf("Reindexing at %v", sb.Ref) + lastTick = now + } + if reindexStart.Valid() && sb.Ref.Less(reindexStart) { + return nil + } + select { + case <-donec: + return context.ErrCanceled + case blobc <- sb.Ref: + return nil + } + }) + }() + var wg sync.WaitGroup + for i := 0; i < reindexMaxProcs.v; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for br := range blobc { + if err := x.indexBlob(br); err != nil { + log.Printf("Error reindexing %v: %v", br, err) + nerrmu.Lock() + nerr++ + nerrmu.Unlock() + // TODO: flag (or default?) to stop the EnumerateAll above once + // there's any error with reindexing? + } + } + }() + } + if err := <-enumErr; err != nil { + return err + } + + wg.Wait() + + x.mu.Lock() + readyCount := len(x.readyReindex) + x.mu.Unlock() + if readyCount > 0 { + return fmt.Errorf("%d blobs were ready to reindex in out-of-order queue, but not yet ran", readyCount) + } + + log.Printf("Index rebuild complete.") + nerrmu.Lock() // no need to unlock + if nerr != 0 { + return fmt.Errorf("%d blobs failed to re-index", nerr) + } + if err := x.initDeletesCache(); err != nil { + return err + } + return nil +} + +func queryPrefixString(s sorted.KeyValue, prefix string) sorted.Iterator { + if prefix == "" { + return s.Find("", "") + } + lastByte := prefix[len(prefix)-1] + if lastByte == 0xff { + panic("unsupported query prefix ending in 0xff") + } + end := prefix[:len(prefix)-1] + string(lastByte+1) + return s.Find(prefix, end) +} + +func (x *Index) queryPrefixString(prefix string) sorted.Iterator { + return queryPrefixString(x.s, prefix) +} + +func queryPrefix(s sorted.KeyValue, key *keyType, args ...interface{}) sorted.Iterator { + return queryPrefixString(s, key.Prefix(args...)) +} + +func (x *Index) queryPrefix(key *keyType, args ...interface{}) sorted.Iterator { + return x.queryPrefixString(key.Prefix(args...)) +} + +func closeIterator(it sorted.Iterator, perr *error) { + err := it.Close() + if err != nil && *perr == nil { + *perr = err + } +} + +// schemaVersion returns the version of schema as it is found +// in the currently used index. If not found, it returns 0. +func (x *Index) schemaVersion() int { + schemaVersionStr, err := x.s.Get(keySchemaVersion.name) + if err != nil { + if err == sorted.ErrNotFound { + return 0 + } + panic(fmt.Sprintf("Could not get index schema version: %v", err)) + } + schemaVersion, err := strconv.Atoi(schemaVersionStr) + if err != nil { + panic(fmt.Sprintf("Bogus index schema version: %q", schemaVersionStr)) + } + return schemaVersion +} + +type deletion struct { + deleter blob.Ref + when time.Time +} + +type byDeletionDate []deletion + +func (d byDeletionDate) Len() int { return len(d) } +func (d byDeletionDate) Swap(i, j int) { d[i], d[j] = d[j], d[i] } +func (d byDeletionDate) Less(i, j int) bool { return d[i].when.Before(d[j].when) } + +type deletionCache struct { + sync.RWMutex + m map[blob.Ref][]deletion +} + +func newDeletionCache() *deletionCache { + return &deletionCache{ + m: make(map[blob.Ref][]deletion), + } +} + +// initDeletesCache creates and populates the deletion status cache used by the index +// for faster calls to IsDeleted and DeletedAt. It is called by New. +func (x *Index) initDeletesCache() (err error) { + x.deletes = newDeletionCache() + it := x.queryPrefix(keyDeleted) + defer closeIterator(it, &err) + for it.Next() { + cl, ok := kvDeleted(it.Key()) + if !ok { + return fmt.Errorf("Bogus keyDeleted entry key: want |\"deleted\"||||, got %q", it.Key()) + } + targetDeletions := append(x.deletes.m[cl.Target], + deletion{ + deleter: cl.BlobRef, + when: cl.Date, + }) + sort.Sort(sort.Reverse(byDeletionDate(targetDeletions))) + x.deletes.m[cl.Target] = targetDeletions + } + return err +} + +func kvDeleted(k string) (c camtypes.Claim, ok bool) { + // TODO(bradfitz): garbage + keyPart := strings.Split(k, "|") + if len(keyPart) != 4 { + return + } + if keyPart[0] != "deleted" { + return + } + target, ok := blob.Parse(keyPart[1]) + if !ok { + return + } + claimRef, ok := blob.Parse(keyPart[3]) + if !ok { + return + } + date, err := time.Parse(time.RFC3339, unreverseTimeString(keyPart[2])) + if err != nil { + return + } + return camtypes.Claim{ + BlobRef: claimRef, + Target: target, + Date: date, + Type: string(schema.DeleteClaim), + }, true +} + +// IsDeleted reports whether the provided blobref (of a permanode or +// claim) should be considered deleted. +func (x *Index) IsDeleted(br blob.Ref) bool { + if x.deletes == nil { + // We still allow the slow path, in case someone creates + // their own Index without a deletes cache. + return x.isDeletedNoCache(br) + } + x.deletes.RLock() + defer x.deletes.RUnlock() + return x.isDeleted(br) +} + +// The caller must hold x.deletes.mu for read. +func (x *Index) isDeleted(br blob.Ref) bool { + deletes, ok := x.deletes.m[br] + if !ok { + return false + } + for _, v := range deletes { + if !x.isDeleted(v.deleter) { + return true + } + } + return false +} + +// Used when the Index has no deletes cache (x.deletes is nil). +func (x *Index) isDeletedNoCache(br blob.Ref) bool { + var err error + it := x.queryPrefix(keyDeleted, br) + for it.Next() { + cl, ok := kvDeleted(it.Key()) + if !ok { + panic(fmt.Sprintf("Bogus keyDeleted entry key: want |\"deleted\"||||, got %q", it.Key())) + } + if !x.isDeletedNoCache(cl.BlobRef) { + closeIterator(it, &err) + if err != nil { + // TODO: Do better? + panic(fmt.Sprintf("Could not close iterator on keyDeleted: %v", err)) + } + return true + } + } + closeIterator(it, &err) + if err != nil { + // TODO: Do better? + panic(fmt.Sprintf("Could not close iterator on keyDeleted: %v", err)) + } + return false +} + +// GetRecentPermanodes sends results to dest filtered by owner, limit, and +// before. A zero value for before will default to the current time. The +// results will have duplicates supressed, with most recent permanode +// returned. +// Note, permanodes more recent than before will still be fetched from the +// index then skipped. This means runtime scales linearly with the number of +// nodes more recent than before. +func (x *Index) GetRecentPermanodes(dest chan<- camtypes.RecentPermanode, owner blob.Ref, limit int, before time.Time) (err error) { + defer close(dest) + + keyId, err := x.KeyId(owner) + if err == sorted.ErrNotFound { + log.Printf("No recent permanodes because keyId for owner %v not found", owner) + return nil + } + if err != nil { + log.Printf("Error fetching keyId for owner %v: %v", owner, err) + return err + } + + sent := 0 + var seenPermanode dupSkipper + + if before.IsZero() { + before = time.Now() + } + // TODO(bradfitz): handle before efficiently. don't use queryPrefix. + it := x.queryPrefix(keyRecentPermanode, keyId) + defer closeIterator(it, &err) + for it.Next() { + permaStr := it.Value() + parts := strings.SplitN(it.Key(), "|", 4) + if len(parts) != 4 { + continue + } + mTime, _ := time.Parse(time.RFC3339, unreverseTimeString(parts[2])) + permaRef, ok := blob.Parse(permaStr) + if !ok { + continue + } + if x.IsDeleted(permaRef) { + continue + } + if seenPermanode.Dup(permaStr) { + continue + } + // Skip entries with an mTime less than or equal to before. + if !mTime.Before(before) { + continue + } + dest <- camtypes.RecentPermanode{ + Permanode: permaRef, + Signer: owner, // TODO(bradfitz): kinda. usually. for now. + LastModTime: mTime, + } + sent++ + if sent == limit { + break + } + } + return nil +} + +func (x *Index) AppendClaims(dst []camtypes.Claim, permaNode blob.Ref, + signerFilter blob.Ref, + attrFilter string) ([]camtypes.Claim, error) { + if x.corpus != nil { + return x.corpus.AppendClaims(dst, permaNode, signerFilter, attrFilter) + } + var ( + keyId string + err error + it sorted.Iterator + ) + if signerFilter.Valid() { + keyId, err = x.KeyId(signerFilter) + if err == sorted.ErrNotFound { + return nil, nil + } + if err != nil { + return nil, err + } + it = x.queryPrefix(keyPermanodeClaim, permaNode, keyId) + } else { + it = x.queryPrefix(keyPermanodeClaim, permaNode) + } + defer closeIterator(it, &err) + + // In the common case, an attribute filter is just a plain + // token ("camliContent") unescaped. If so, fast path that + // check to skip the row before we even split it. + var mustHave string + if attrFilter != "" && urle(attrFilter) == attrFilter { + mustHave = attrFilter + } + + for it.Next() { + val := it.Value() + if mustHave != "" && !strings.Contains(val, mustHave) { + continue + } + cl, ok := kvClaim(it.Key(), val, blob.Parse) + if !ok { + continue + } + if x.IsDeleted(cl.BlobRef) { + continue + } + if attrFilter != "" && cl.Attr != attrFilter { + continue + } + if signerFilter.Valid() && cl.Signer != signerFilter { + continue + } + dst = append(dst, cl) + } + return dst, nil +} + +func kvClaim(k, v string, blobParse func(string) (blob.Ref, bool)) (c camtypes.Claim, ok bool) { + const nKeyPart = 5 + const nValPart = 4 + var keya [nKeyPart]string + var vala [nValPart]string + keyPart := strutil.AppendSplitN(keya[:0], k, "|", -1) + valPart := strutil.AppendSplitN(vala[:0], v, "|", -1) + if len(keyPart) < nKeyPart || len(valPart) < nValPart { + return + } + signerRef, ok := blobParse(valPart[3]) + if !ok { + return + } + permaNode, ok := blobParse(keyPart[1]) + if !ok { + return + } + claimRef, ok := blobParse(keyPart[4]) + if !ok { + return + } + date, err := time.Parse(time.RFC3339, keyPart[3]) + if err != nil { + return + } + return camtypes.Claim{ + BlobRef: claimRef, + Signer: signerRef, + Permanode: permaNode, + Date: date, + Type: urld(valPart[0]), + Attr: urld(valPart[1]), + Value: urld(valPart[2]), + }, true +} + +func (x *Index) GetBlobMeta(br blob.Ref) (camtypes.BlobMeta, error) { + if x.corpus != nil { + return x.corpus.GetBlobMeta(br) + } + key := "meta:" + br.String() + meta, err := x.s.Get(key) + if err == sorted.ErrNotFound { + err = os.ErrNotExist + } + if err != nil { + return camtypes.BlobMeta{}, err + } + pos := strings.Index(meta, "|") + if pos < 0 { + panic(fmt.Sprintf("Bogus index row for key %q: got value %q", key, meta)) + } + size, err := strconv.ParseUint(meta[:pos], 10, 32) + if err != nil { + return camtypes.BlobMeta{}, err + } + mime := meta[pos+1:] + return camtypes.BlobMeta{ + Ref: br, + Size: uint32(size), + CamliType: camliTypeFromMIME(mime), + }, nil +} + +func (x *Index) KeyId(signer blob.Ref) (string, error) { + if x.corpus != nil { + return x.corpus.KeyId(signer) + } + return x.s.Get("signerkeyid:" + signer.String()) +} + +func (x *Index) PermanodeOfSignerAttrValue(signer blob.Ref, attr, val string) (permaNode blob.Ref, err error) { + keyId, err := x.KeyId(signer) + if err == sorted.ErrNotFound { + return blob.Ref{}, os.ErrNotExist + } + if err != nil { + return blob.Ref{}, err + } + it := x.queryPrefix(keySignerAttrValue, keyId, attr, val) + defer closeIterator(it, &err) + for it.Next() { + permaRef, ok := blob.Parse(it.Value()) + if ok && !x.IsDeleted(permaRef) { + return permaRef, nil + } + } + return blob.Ref{}, os.ErrNotExist +} + +// This is just like PermanodeOfSignerAttrValue except we return multiple and dup-suppress. +// If request.Query is "", it is not used in the prefix search. +func (x *Index) SearchPermanodesWithAttr(dest chan<- blob.Ref, request *camtypes.PermanodeByAttrRequest) (err error) { + defer close(dest) + if request.FuzzyMatch { + // TODO(bradfitz): remove this for now? figure out how to handle it generically? + return errors.New("TODO: SearchPermanodesWithAttr: generic indexer doesn't support FuzzyMatch on PermanodeByAttrRequest") + } + if request.Attribute == "" { + return errors.New("index: missing Attribute in SearchPermanodesWithAttr") + } + + keyId, err := x.KeyId(request.Signer) + if err == sorted.ErrNotFound { + return nil + } + if err != nil { + return err + } + seen := make(map[string]bool) + var it sorted.Iterator + if request.Query == "" { + it = x.queryPrefix(keySignerAttrValue, keyId, request.Attribute) + } else { + it = x.queryPrefix(keySignerAttrValue, keyId, request.Attribute, request.Query) + } + defer closeIterator(it, &err) + for it.Next() { + cl, ok := kvSignerAttrValue(it.Key(), it.Value()) + if !ok { + continue + } + if x.IsDeleted(cl.BlobRef) { + continue + } + if x.IsDeleted(cl.Permanode) { + continue + } + pnstr := cl.Permanode.String() + if seen[pnstr] { + continue + } + seen[pnstr] = true + + dest <- cl.Permanode + if len(seen) == request.MaxResults { + break + } + } + return nil +} + +func kvSignerAttrValue(k, v string) (c camtypes.Claim, ok bool) { + // TODO(bradfitz): garbage + keyPart := strings.Split(k, "|") + valPart := strings.Split(v, "|") + if len(keyPart) != 6 || len(valPart) != 1 { + // TODO(mpl): use glog + log.Printf("bogus keySignerAttrValue index entry: %q = %q", k, v) + return + } + if keyPart[0] != "signerattrvalue" { + return + } + date, err := time.Parse(time.RFC3339, unreverseTimeString(keyPart[4])) + if err != nil { + log.Printf("bogus time in keySignerAttrValue index entry: %q", keyPart[4]) + return + } + claimRef, ok := blob.Parse(keyPart[5]) + if !ok { + log.Printf("bogus claim in keySignerAttrValue index entry: %q", keyPart[5]) + return + } + permaNode, ok := blob.Parse(valPart[0]) + if !ok { + log.Printf("bogus permanode in keySignerAttrValue index entry: %q", valPart[0]) + return + } + return camtypes.Claim{ + BlobRef: claimRef, + Permanode: permaNode, + Date: date, + Attr: urld(keyPart[2]), + Value: urld(keyPart[3]), + }, true +} + +func (x *Index) PathsOfSignerTarget(signer, target blob.Ref) (paths []*camtypes.Path, err error) { + paths = []*camtypes.Path{} + keyId, err := x.KeyId(signer) + if err != nil { + if err == sorted.ErrNotFound { + err = nil + } + return + } + + mostRecent := make(map[string]*camtypes.Path) + maxClaimDates := make(map[string]time.Time) + + it := x.queryPrefix(keyPathBackward, keyId, target) + defer closeIterator(it, &err) + for it.Next() { + p, ok, active := kvPathBackward(it.Key(), it.Value()) + if !ok { + continue + } + if x.IsDeleted(p.Claim) { + continue + } + if x.IsDeleted(p.Base) { + continue + } + + key := p.Base.String() + "/" + p.Suffix + if p.ClaimDate.After(maxClaimDates[key]) { + maxClaimDates[key] = p.ClaimDate + if active { + mostRecent[key] = &p + } else { + delete(mostRecent, key) + } + } + } + for _, v := range mostRecent { + paths = append(paths, v) + } + return paths, nil +} + +func kvPathBackward(k, v string) (p camtypes.Path, ok bool, active bool) { + // TODO(bradfitz): garbage + keyPart := strings.Split(k, "|") + valPart := strings.Split(v, "|") + if len(keyPart) != 4 || len(valPart) != 4 { + // TODO(mpl): use glog + log.Printf("bogus keyPathBackward index entry: %q = %q", k, v) + return + } + if keyPart[0] != "signertargetpath" { + return + } + target, ok := blob.Parse(keyPart[2]) + if !ok { + log.Printf("bogus target in keyPathBackward index entry: %q", keyPart[2]) + return + } + claim, ok := blob.Parse(keyPart[3]) + if !ok { + log.Printf("bogus claim in keyPathBackward index entry: %q", keyPart[3]) + return + } + date, err := time.Parse(time.RFC3339, valPart[0]) + if err != nil { + log.Printf("bogus date in keyPathBackward index entry: %q", valPart[0]) + return + } + base, ok := blob.Parse(valPart[1]) + if !ok { + log.Printf("bogus base in keyPathBackward index entry: %q", valPart[1]) + return + } + if valPart[2] == "Y" { + active = true + } + return camtypes.Path{ + Claim: claim, + Base: base, + Target: target, + ClaimDate: date, + Suffix: urld(valPart[3]), + }, true, active +} + +func (x *Index) PathsLookup(signer, base blob.Ref, suffix string) (paths []*camtypes.Path, err error) { + paths = []*camtypes.Path{} + keyId, err := x.KeyId(signer) + if err != nil { + if err == sorted.ErrNotFound { + err = nil + } + return + } + + it := x.queryPrefix(keyPathForward, keyId, base, suffix) + defer closeIterator(it, &err) + for it.Next() { + p, ok, active := kvPathForward(it.Key(), it.Value()) + if !ok { + continue + } + if x.IsDeleted(p.Claim) { + continue + } + if x.IsDeleted(p.Target) { + continue + } + + // TODO(bradfitz): investigate what's up with deleted + // forward path claims here. Needs docs with the + // interface too, and tests. + _ = active + + paths = append(paths, &p) + } + return +} + +func kvPathForward(k, v string) (p camtypes.Path, ok bool, active bool) { + // TODO(bradfitz): garbage + keyPart := strings.Split(k, "|") + valPart := strings.Split(v, "|") + if len(keyPart) != 6 || len(valPart) != 2 { + // TODO(mpl): use glog + log.Printf("bogus keyPathForward index entry: %q = %q", k, v) + return + } + if keyPart[0] != "path" { + return + } + base, ok := blob.Parse(keyPart[2]) + if !ok { + log.Printf("bogus base in keyPathForward index entry: %q", keyPart[2]) + return + } + date, err := time.Parse(time.RFC3339, unreverseTimeString(keyPart[4])) + if err != nil { + log.Printf("bogus date in keyPathForward index entry: %q", keyPart[4]) + return + } + claim, ok := blob.Parse(keyPart[5]) + if !ok { + log.Printf("bogus claim in keyPathForward index entry: %q", keyPart[5]) + return + } + if valPart[0] == "Y" { + active = true + } + target, ok := blob.Parse(valPart[1]) + if !ok { + log.Printf("bogus target in keyPathForward index entry: %q", valPart[1]) + return + } + return camtypes.Path{ + Claim: claim, + Base: base, + Target: target, + ClaimDate: date, + Suffix: urld(keyPart[3]), + }, true, active +} + +func (x *Index) PathLookup(signer, base blob.Ref, suffix string, at time.Time) (*camtypes.Path, error) { + paths, err := x.PathsLookup(signer, base, suffix) + if err != nil { + return nil, err + } + var ( + newest = int64(0) + atSeconds = int64(0) + best *camtypes.Path + ) + + if !at.IsZero() { + atSeconds = at.Unix() + } + + for _, path := range paths { + t := path.ClaimDate + secs := t.Unix() + if atSeconds != 0 && secs > atSeconds { + // Too new + continue + } + if newest > secs { + // Too old + continue + } + // Just right + newest, best = secs, path + } + if best == nil { + return nil, os.ErrNotExist + } + return best, nil +} + +func (x *Index) ExistingFileSchemas(wholeRef blob.Ref) (schemaRefs []blob.Ref, err error) { + it := x.queryPrefix(keyWholeToFileRef, wholeRef) + defer closeIterator(it, &err) + for it.Next() { + keyPart := strings.Split(it.Key(), "|")[1:] + if len(keyPart) < 2 { + continue + } + ref, ok := blob.Parse(keyPart[1]) + if ok { + schemaRefs = append(schemaRefs, ref) + } + } + return schemaRefs, nil +} + +func (x *Index) loadKey(key string, val *string, err *error, wg *sync.WaitGroup) { + defer wg.Done() + *val, *err = x.s.Get(key) +} + +func (x *Index) GetFileInfo(fileRef blob.Ref) (camtypes.FileInfo, error) { + if x.corpus != nil { + return x.corpus.GetFileInfo(fileRef) + } + ikey := "fileinfo|" + fileRef.String() + tkey := "filetimes|" + fileRef.String() + // TODO: switch this to use syncutil.Group + wg := new(sync.WaitGroup) + wg.Add(2) + var iv, tv string // info value, time value + var ierr, terr error + go x.loadKey(ikey, &iv, &ierr, wg) + go x.loadKey(tkey, &tv, &terr, wg) + wg.Wait() + + if ierr == sorted.ErrNotFound { + return camtypes.FileInfo{}, os.ErrNotExist + } + if ierr != nil { + return camtypes.FileInfo{}, ierr + } + valPart := strings.Split(iv, "|") + if len(valPart) < 3 { + log.Printf("index: bogus key %q = %q", ikey, iv) + return camtypes.FileInfo{}, os.ErrNotExist + } + var wholeRef blob.Ref + if len(valPart) >= 4 { + wholeRef, _ = blob.Parse(valPart[3]) + } + size, err := strconv.ParseInt(valPart[0], 10, 64) + if err != nil { + log.Printf("index: bogus integer at position 0 in key %q = %q", ikey, iv) + return camtypes.FileInfo{}, os.ErrNotExist + } + fileName := urld(valPart[1]) + fi := camtypes.FileInfo{ + Size: size, + FileName: fileName, + MIMEType: urld(valPart[2]), + WholeRef: wholeRef, + } + + if tv != "" { + times := strings.Split(urld(tv), ",") + updateFileInfoTimes(&fi, times) + } + + return fi, nil +} + +func updateFileInfoTimes(fi *camtypes.FileInfo, times []string) { + if len(times) == 0 { + return + } + fi.Time = types.ParseTime3339OrNil(times[0]) + if len(times) == 2 { + fi.ModTime = types.ParseTime3339OrNil(times[1]) + } +} + +// v is "width|height" +func kvImageInfo(v []byte) (ii camtypes.ImageInfo, ok bool) { + pipei := bytes.IndexByte(v, '|') + if pipei < 0 { + return + } + w, err := strutil.ParseUintBytes(v[:pipei], 10, 16) + if err != nil { + return + } + h, err := strutil.ParseUintBytes(v[pipei+1:], 10, 16) + if err != nil { + return + } + ii.Width = uint16(w) + ii.Height = uint16(h) + return ii, true +} + +func (x *Index) GetImageInfo(fileRef blob.Ref) (camtypes.ImageInfo, error) { + if x.corpus != nil { + return x.corpus.GetImageInfo(fileRef) + } + // it might be that the key does not exist because image.DecodeConfig failed earlier + // (because of unsupported JPEG features like progressive mode). + key := keyImageSize.Key(fileRef.String()) + v, err := x.s.Get(key) + if err == sorted.ErrNotFound { + err = os.ErrNotExist + } + if err != nil { + return camtypes.ImageInfo{}, err + } + ii, ok := kvImageInfo([]byte(v)) + if !ok { + return camtypes.ImageInfo{}, fmt.Errorf("index: bogus key %q = %q", key, v) + } + return ii, nil +} + +func (x *Index) GetMediaTags(fileRef blob.Ref) (tags map[string]string, err error) { + if x.corpus != nil { + return x.corpus.GetMediaTags(fileRef) + } + fi, err := x.GetFileInfo(fileRef) + if err != nil { + return nil, err + } + it := x.queryPrefix(keyMediaTag, fi.WholeRef.String()) + defer closeIterator(it, &err) + for it.Next() { + tags[it.Key()] = it.Value() + } + return tags, nil +} + +func (x *Index) EdgesTo(ref blob.Ref, opts *camtypes.EdgesToOpts) (edges []*camtypes.Edge, err error) { + it := x.queryPrefix(keyEdgeBackward, ref) + defer closeIterator(it, &err) + permanodeParents := make(map[string]*camtypes.Edge) + for it.Next() { + edge, ok := kvEdgeBackward(it.Key(), it.Value()) + if !ok { + continue + } + if x.IsDeleted(edge.From) { + continue + } + if x.IsDeleted(edge.BlobRef) { + continue + } + edge.To = ref + if edge.FromType == "permanode" { + permanodeParents[edge.From.String()] = edge + } else { + edges = append(edges, edge) + } + } + for _, e := range permanodeParents { + edges = append(edges, e) + } + return edges, nil +} + +func kvEdgeBackward(k, v string) (edge *camtypes.Edge, ok bool) { + // TODO(bradfitz): garbage + keyPart := strings.Split(k, "|") + valPart := strings.Split(v, "|") + if len(keyPart) != 4 || len(valPart) != 2 { + // TODO(mpl): use glog + log.Printf("bogus keyEdgeBackward index entry: %q = %q", k, v) + return + } + if keyPart[0] != "edgeback" { + return + } + parentRef, ok := blob.Parse(keyPart[2]) + if !ok { + log.Printf("bogus parent in keyEdgeBackward index entry: %q", keyPart[2]) + return + } + blobRef, ok := blob.Parse(keyPart[3]) + if !ok { + log.Printf("bogus blobref in keyEdgeBackward index entry: %q", keyPart[3]) + return + } + return &camtypes.Edge{ + From: parentRef, + FromType: valPart[0], + FromTitle: valPart[1], + BlobRef: blobRef, + }, true +} + +// GetDirMembers sends on dest the children of the static directory dir. +func (x *Index) GetDirMembers(dir blob.Ref, dest chan<- blob.Ref, limit int) (err error) { + defer close(dest) + + sent := 0 + it := x.queryPrefix(keyStaticDirChild, dir.String()) + defer closeIterator(it, &err) + for it.Next() { + keyPart := strings.Split(it.Key(), "|") + if len(keyPart) != 3 { + return fmt.Errorf("index: bogus key keyStaticDirChild = %q", it.Key()) + } + + child, ok := blob.Parse(keyPart[2]) + if !ok { + continue + } + dest <- child + sent++ + if sent == limit { + break + } + } + return nil +} + +func kvBlobMeta(k, v string) (bm camtypes.BlobMeta, ok bool) { + refStr := k[len("meta:"):] + br, ok := blob.Parse(refStr) + if !ok { + return + } + pipe := strings.Index(v, "|") + if pipe < 0 { + return + } + size, err := strconv.ParseUint(v[:pipe], 10, 32) + if err != nil { + return + } + return camtypes.BlobMeta{ + Ref: br, + Size: uint32(size), + CamliType: camliTypeFromMIME(v[pipe+1:]), + }, true +} + +func kvBlobMeta_bytes(k, v []byte) (bm camtypes.BlobMeta, ok bool) { + ref := k[len("meta:"):] + br, ok := blob.ParseBytes(ref) + if !ok { + return + } + pipe := bytes.IndexByte(v, '|') + if pipe < 0 { + return + } + size, err := strutil.ParseUintBytes(v[:pipe], 10, 32) + if err != nil { + return + } + return camtypes.BlobMeta{ + Ref: br, + Size: uint32(size), + CamliType: camliTypeFromMIME_bytes(v[pipe+1:]), + }, true +} + +func enumerateBlobMeta(s sorted.KeyValue, cb func(camtypes.BlobMeta) error) (err error) { + it := queryPrefixString(s, "meta:") + defer closeIterator(it, &err) + for it.Next() { + bm, ok := kvBlobMeta(it.Key(), it.Value()) + if !ok { + continue + } + if err := cb(bm); err != nil { + return err + } + } + return nil +} + +func enumerateSignerKeyId(s sorted.KeyValue, cb func(blob.Ref, string)) (err error) { + const pfx = "signerkeyid:" + it := queryPrefixString(s, pfx) + defer closeIterator(it, &err) + for it.Next() { + if br, ok := blob.Parse(strings.TrimPrefix(it.Key(), pfx)); ok { + cb(br, it.Value()) + } + } + return +} + +// EnumerateBlobMeta sends all metadata about all known blobs to ch and then closes ch. +func (x *Index) EnumerateBlobMeta(ctx *context.Context, ch chan<- camtypes.BlobMeta) (err error) { + if x.corpus != nil { + x.corpus.RLock() + defer x.corpus.RUnlock() + return x.corpus.EnumerateBlobMetaLocked(ctx, ch) + } + defer close(ch) + return enumerateBlobMeta(x.s, func(bm camtypes.BlobMeta) error { + select { + case ch <- bm: + case <-ctx.Done(): + return context.ErrCanceled + } + return nil + }) +} + +// Storage returns the index's underlying Storage implementation. +func (x *Index) Storage() sorted.KeyValue { return x.s } + +// Close closes the underlying sorted.KeyValue, if the storage has a Close method. +// The return value is the return value of the underlying Close, or +// nil otherwise. +func (x *Index) Close() error { + if cl, ok := x.s.(io.Closer); ok { + return cl.Close() + } + close(x.tickleOoo) + return nil +} + +// initNeededMaps initializes x.needs and x.neededBy on start-up. +func (x *Index) initNeededMaps() (err error) { + x.deletes = newDeletionCache() + it := x.queryPrefix(keyMissing) + defer closeIterator(it, &err) + for it.Next() { + key := it.KeyBytes() + pair := key[len("missing|"):] + pipe := bytes.IndexByte(pair, '|') + if pipe < 0 { + return fmt.Errorf("Bogus missing key %q", key) + } + have, ok1 := blob.ParseBytes(pair[:pipe]) + missing, ok2 := blob.ParseBytes(pair[pipe+1:]) + if !ok1 || !ok2 { + return fmt.Errorf("Bogus missing key %q", key) + } + x.noteNeededMemory(have, missing) + } + return +} + +func (x *Index) noteNeeded(have, missing blob.Ref) error { + if err := x.s.Set(keyMissing.Key(have, missing), "1"); err != nil { + return err + } + x.noteNeededMemory(have, missing) + return nil +} + +func (x *Index) noteNeededMemory(have, missing blob.Ref) { + x.mu.Lock() + x.needs[have] = append(x.needs[have], missing) + x.neededBy[missing] = append(x.neededBy[missing], have) + x.mu.Unlock() +} + +const camliTypeMIMEPrefix = "application/json; camliType=" + +var camliTypeMIMEPrefixBytes = []byte(camliTypeMIMEPrefix) + +// "application/json; camliType=file" => "file" +// "image/gif" => "" +func camliTypeFromMIME(mime string) string { + if v := strings.TrimPrefix(mime, camliTypeMIMEPrefix); v != mime { + return v + } + return "" +} + +func camliTypeFromMIME_bytes(mime []byte) string { + if v := bytes.TrimPrefix(mime, camliTypeMIMEPrefixBytes); len(v) != len(mime) { + return strutil.StringFromBytes(v) + } + return "" +} + +// TODO(bradfitz): rename this? This is really about signer-attr-value +// (PermanodeOfSignerAttrValue), and not about indexed attributes in general. +func IsIndexedAttribute(attr string) bool { + switch attr { + case "camliRoot", "camliImportRoot", "tag", "title": + return true + } + return false +} + +// IsBlobReferenceAttribute returns whether attr is an attribute whose +// value is a blob reference (e.g. camliMember) and thus something the +// indexers should keep inverted indexes on for parent/child-type +// relationships. +func IsBlobReferenceAttribute(attr string) bool { + switch attr { + case "camliMember": + return true + } + return false +} + +func IsFulltextAttribute(attr string) bool { + switch attr { + case "tag", "title": + return true + } + return false +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/index_test.go b/vendor/github.com/camlistore/camlistore/pkg/index/index_test.go new file mode 100644 index 00000000..8bca1929 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/index_test.go @@ -0,0 +1,500 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index_test + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/index" + "camlistore.org/pkg/index/indextest" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/test" + "camlistore.org/pkg/types/camtypes" +) + +func TestReverseTimeString(t *testing.T) { + in := "2011-11-27T01:23:45Z" + got := index.ExpReverseTimeString(in) + want := "rt7988-88-72T98:76:54Z" + if got != want { + t.Fatalf("reverseTimeString = %q, want %q", got, want) + } + back := index.ExpUnreverseTimeString(got) + if back != in { + t.Fatalf("unreverseTimeString = %q, want %q", back, in) + } +} + +func TestIndex_Memory(t *testing.T) { + indextest.Index(t, index.NewMemoryIndex) +} + +func TestPathsOfSignerTarget_Memory(t *testing.T) { + indextest.PathsOfSignerTarget(t, index.NewMemoryIndex) +} + +func TestFiles_Memory(t *testing.T) { + indextest.Files(t, index.NewMemoryIndex) +} + +func TestEdgesTo_Memory(t *testing.T) { + indextest.EdgesTo(t, index.NewMemoryIndex) +} + +func TestDelete_Memory(t *testing.T) { + indextest.Delete(t, index.NewMemoryIndex) +} + +var ( + // those test files are not specific to an indexer implementation + // hence we do not want to check them. + notAnIndexer = []string{ + "corpus_bench_test.go", + "corpus_test.go", + "export_test.go", + "index_test.go", + "keys_test.go", + } + // A map is used in hasAllRequiredTests to note which required + // tests have been found in a package, by setting the corresponding + // booleans to true. Those are the keys for this map. + requiredTests = []string{"TestIndex_", "TestPathsOfSignerTarget_", "TestFiles_", "TestEdgesTo_"} +) + +// This function checks that all the functions using the tests +// defined in indextest, namely: +// TestIndex_, TestPathOfSignerTarget_, TestFiles_ +// do exist in the provided test file. +func hasAllRequiredTests(name string, t *testing.T) error { + tests := make(map[string]bool) + for _, v := range requiredTests { + tests[v] = false + } + + if !strings.HasSuffix(name, "_test.go") || skipFromList(name) { + return nil + } + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, name, nil, 0) + if err != nil { + t.Fatalf("%v: %v", name, err) + } + ast.Inspect(f, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.FuncDecl: + name := x.Name.Name + for k, _ := range tests { + if strings.HasPrefix(name, k) { + tests[k] = true + } + } + } + return true + }) + + for k, v := range tests { + if !v { + return fmt.Errorf("%v not implemented in %v", k, name) + } + } + return nil +} + +// For each test file dedicated to an indexer implementation, this checks that +// all the required tests are present in its test suite. +func TestIndexerTestsCompleteness(t *testing.T) { + cwd, err := os.Open(".") + if err != nil { + t.Fatal(err) + } + defer cwd.Close() + files, err := cwd.Readdir(-1) + if err != nil { + t.Fatal(err) + } + + for _, file := range files { + name := file.Name() + if file.IsDir() || strings.HasPrefix(name, ".") { + continue + } + if err := hasAllRequiredTests(name, t); err != nil { + t.Error(err) + } + } + // special case for sqlite as it is the only one left in its own package + if err := hasAllRequiredTests(filepath.FromSlash("sqlite/sqlite_test.go"), t); err != nil { + t.Error(err) + } +} + +func skipFromList(name string) bool { + for _, v := range notAnIndexer { + if name == v { + return true + } + } + return false +} + +func testMergeFileInfoRow(t *testing.T, wholeRef string) { + c := index.ExpNewCorpus() + value := "100|something%2egif|image%2Fgif" + want := camtypes.FileInfo{ + Size: 100, + MIMEType: "image/gif", + FileName: "something.gif", + } + if wholeRef != "" { + value += "|" + wholeRef + want.WholeRef = blob.MustParse(wholeRef) + } + c.Exp_mergeFileInfoRow("fileinfo|sha1-579f7f246bd420d486ddeb0dadbb256cfaf8bf6b", value) + fi := c.Exp_files(blob.MustParse("sha1-579f7f246bd420d486ddeb0dadbb256cfaf8bf6b")) + if !reflect.DeepEqual(want, fi) { + t.Errorf("Got %+v; want %+v", fi, want) + } +} + +// When requiredSchemaVersion was at 4, i.e. wholeRef hadn't been introduced into fileInfo +func TestMergeFileInfoRow4(t *testing.T) { + testMergeFileInfoRow(t, "") +} + +func TestMergeFileInfoRow(t *testing.T) { + testMergeFileInfoRow(t, "sha1-142b504945338158e0149d4ed25a41a522a28e88") +} + +var ( + chunk1 = &test.Blob{Contents: "foo"} + chunk2 = &test.Blob{Contents: "bar"} + chunk3 = &test.Blob{Contents: "baz"} + + chunk1ref = chunk1.BlobRef() + chunk2ref = chunk2.BlobRef() + chunk3ref = chunk3.BlobRef() + + fileBlob = &test.Blob{fmt.Sprintf(`{"camliVersion": 1, +"camliType": "file", +"fileName": "stuff.txt", +"parts": [ + {"blobRef": "%s", "size": 3}, + {"blobRef": "%s", "size": 3}, + {"blobRef": "%s", "size": 3} +]}`, chunk1ref, chunk2ref, chunk3ref)} + fileBlobRef = fileBlob.BlobRef() +) + +func TestInitNeededMaps(t *testing.T) { + s := sorted.NewMemoryKeyValue() + + // Start unknowning that the data chunks are all gone: + s.Set("schemaversion", fmt.Sprint(index.Exp_schemaVersion())) + s.Set(index.Exp_missingKey(fileBlobRef, chunk1ref), "1") + s.Set(index.Exp_missingKey(fileBlobRef, chunk2ref), "1") + s.Set(index.Exp_missingKey(fileBlobRef, chunk3ref), "1") + ix, err := index.New(s) + if err != nil { + t.Fatal(err) + } + { + needs, neededBy, _ := ix.NeededMapsForTest() + needsWant := map[blob.Ref][]blob.Ref{ + fileBlobRef: []blob.Ref{chunk1ref, chunk2ref, chunk3ref}, + } + neededByWant := map[blob.Ref][]blob.Ref{ + chunk1ref: []blob.Ref{fileBlobRef}, + chunk2ref: []blob.Ref{fileBlobRef}, + chunk3ref: []blob.Ref{fileBlobRef}, + } + if !reflect.DeepEqual(needs, needsWant) { + t.Errorf("needs = %v; want %v", needs, needsWant) + } + if !reflect.DeepEqual(neededBy, neededByWant) { + t.Errorf("neededBy = %v; want %v", neededBy, neededByWant) + } + } + + ix.Exp_noteBlobIndexed(chunk2ref) + + { + needs, neededBy, ready := ix.NeededMapsForTest() + needsWant := map[blob.Ref][]blob.Ref{ + fileBlobRef: []blob.Ref{chunk1ref, chunk3ref}, + } + neededByWant := map[blob.Ref][]blob.Ref{ + chunk1ref: []blob.Ref{fileBlobRef}, + chunk3ref: []blob.Ref{fileBlobRef}, + } + if !reflect.DeepEqual(needs, needsWant) { + t.Errorf("needs = %v; want %v", needs, needsWant) + } + if !reflect.DeepEqual(neededBy, neededByWant) { + t.Errorf("neededBy = %v; want %v", neededBy, neededByWant) + } + if len(ready) != 0 { + t.Errorf("ready = %v; want nothing", ready) + } + } + + ix.Exp_noteBlobIndexed(chunk1ref) + + { + needs, neededBy, ready := ix.NeededMapsForTest() + needsWant := map[blob.Ref][]blob.Ref{ + fileBlobRef: []blob.Ref{chunk3ref}, + } + neededByWant := map[blob.Ref][]blob.Ref{ + chunk3ref: []blob.Ref{fileBlobRef}, + } + if !reflect.DeepEqual(needs, needsWant) { + t.Errorf("needs = %v; want %v", needs, needsWant) + } + if !reflect.DeepEqual(neededBy, neededByWant) { + t.Errorf("neededBy = %v; want %v", neededBy, neededByWant) + } + if len(ready) != 0 { + t.Errorf("ready = %v; want nothing", ready) + } + } + + ix.Exp_noteBlobIndexed(chunk3ref) + + { + needs, neededBy, ready := ix.NeededMapsForTest() + needsWant := map[blob.Ref][]blob.Ref{} + neededByWant := map[blob.Ref][]blob.Ref{} + if !reflect.DeepEqual(needs, needsWant) { + t.Errorf("needs = %v; want %v", needs, needsWant) + } + if !reflect.DeepEqual(neededBy, neededByWant) { + t.Errorf("neededBy = %v; want %v", neededBy, neededByWant) + } + if !ready[fileBlobRef] { + t.Error("fileBlobRef not ready") + } + } + dumpSorted(t, s) +} + +func dumpSorted(t *testing.T, s sorted.KeyValue) { + foreachSorted(t, s, func(k, v string) { + t.Logf("index %q = %q", k, v) + }) +} + +func foreachSorted(t *testing.T, s sorted.KeyValue, fn func(string, string)) { + it := s.Find("", "") + for it.Next() { + fn(it.Key(), it.Value()) + } + if err := it.Close(); err != nil { + t.Fatal(err) + } +} + +func TestOutOfOrderIndexing(t *testing.T) { + tf := new(test.Fetcher) + s := sorted.NewMemoryKeyValue() + + ix, err := index.New(s) + if err != nil { + t.Fatal(err) + } + ix.InitBlobSource(tf) + + t.Logf("file ref = %v", fileBlobRef) + t.Logf("missing data chunks = %v, %v, %v", chunk1ref, chunk2ref, chunk3ref) + + add := func(b *test.Blob) { + tf.AddBlob(b) + if _, err := ix.ReceiveBlob(b.BlobRef(), b.Reader()); err != nil { + t.Fatalf("ReceiveBlob(%v): %v", b.BlobRef(), err) + } + } + + add(fileBlob) + + { + key := fmt.Sprintf("missing|%s|%s", fileBlobRef, chunk1ref) + if got, err := s.Get(key); got == "" || err != nil { + t.Errorf("key %q missing (err: %v); want 1", key, err) + } + } + + add(chunk1) + add(chunk2) + + ix.Exp_AwaitReindexing(t) + + { + key := fmt.Sprintf("missing|%s|%s", fileBlobRef, chunk3ref) + if got, err := s.Get(key); got == "" || err != nil { + t.Errorf("key %q missing (err: %v); want 1", key, err) + } + } + + add(chunk3) + + ix.Exp_AwaitReindexing(t) + + foreachSorted(t, s, func(k, v string) { + if strings.HasPrefix(k, "missing|") { + t.Errorf("Shouldn't have missing key: %q", k) + } + }) +} + +func TestIndexingClaimMissingPubkey(t *testing.T) { + s := sorted.NewMemoryKeyValue() + idx, err := index.New(s) + if err != nil { + t.Fatal(err) + } + + id := indextest.NewIndexDeps(idx) + id.Fataler = t + + goodKeyFetcher := id.Index.KeyFetcher + emptyFetcher := new(test.Fetcher) + + pn := id.NewPermanode() + + // Prevent the index from being able to find the public key: + idx.KeyFetcher = emptyFetcher + + // This previous failed to upload, since the signer's public key was + // unavailable. + claimRef := id.SetAttribute(pn, "tag", "foo") + + t.Logf(" Claim is %v", claimRef) + t.Logf("Signer is %v", id.SignerBlobRef) + + // Verify that populateClaim noted the missing public key blob: + { + key := fmt.Sprintf("missing|%s|%s", claimRef, id.SignerBlobRef) + if got, err := s.Get(key); got == "" || err != nil { + t.Errorf("key %q missing (err: %v); want 1", key, err) + } + } + + // Now make it available again: + idx.KeyFetcher = idx.Exp_BlobSource() + + if err := copyBlob(id.SignerBlobRef, idx.Exp_BlobSource().(*test.Fetcher), goodKeyFetcher); err != nil { + t.Errorf("Error copying public key to BlobSource: %v", err) + } + if err := copyBlob(id.SignerBlobRef, idx, goodKeyFetcher); err != nil { + t.Errorf("Error uploading public key to indexer: %v", err) + } + + idx.Exp_AwaitReindexing(t) + + // Verify that populateClaim noted the missing public key blob: + { + key := fmt.Sprintf("missing|%s|%s", claimRef, id.SignerBlobRef) + if got, err := s.Get(key); got != "" || err == nil { + t.Errorf("row %q still exists", key) + } + } +} + +func copyBlob(br blob.Ref, dst blobserver.BlobReceiver, src blob.Fetcher) error { + rc, _, err := src.Fetch(br) + if err != nil { + return err + } + defer rc.Close() + _, err = dst.ReceiveBlob(br, rc) + return err +} + +// tests that we add the missing wholeRef entries in FileInfo rows when going from +// a version 4 to a version 5 index. +func TestFixMissingWholeref(t *testing.T) { + tf := new(test.Fetcher) + s := sorted.NewMemoryKeyValue() + + ix, err := index.New(s) + if err != nil { + t.Fatal(err) + } + ix.InitBlobSource(tf) + + // populate with a file + add := func(b *test.Blob) { + tf.AddBlob(b) + if _, err := ix.ReceiveBlob(b.BlobRef(), b.Reader()); err != nil { + t.Fatalf("ReceiveBlob(%v): %v", b.BlobRef(), err) + } + } + add(chunk1) + add(chunk2) + add(chunk3) + add(fileBlob) + + // revert the row to the old form, by stripping the wholeRef suffix + key := "fileinfo|" + fileBlobRef.String() + val5, err := s.Get(key) + if err != nil { + t.Fatalf("could not get %v: %v", key, err) + } + parts := strings.SplitN(val5, "|", 4) + val4 := strings.Join(parts[:3], "|") + if err := s.Set(key, val4); err != nil { + t.Fatalf("could not set (%v, %v): %v", key, val4, err) + } + + // revert index version at 4 to trigger the fix + if err := s.Set("schemaversion", "4"); err != nil { + t.Fatal(err) + } + + // init broken index + ix, err = index.New(s) + if err != index.Exp_ErrMissingWholeRef { + t.Fatalf("wrong error upon index initialization: got %v, wanted %v", err, index.Exp_ErrMissingWholeRef) + } + // and fix it + if err := ix.Exp_FixMissingWholeRef(tf); err != nil { + t.Fatal(err) + } + + // init fixed index + ix, err = index.New(s) + if err != nil { + t.Fatal(err) + } + // and check that the value is now actually fixed + fi, err := ix.GetFileInfo(fileBlobRef) + if err != nil { + t.Fatal(err) + } + if fi.WholeRef.String() != parts[3] { + t.Fatalf("index fileInfo wholeref was not fixed: got %q, wanted %v", fi.WholeRef, parts[3]) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/indextest/testdata/0s.mp3 b/vendor/github.com/camlistore/camlistore/pkg/index/indextest/testdata/0s.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..7e4a59dedf0834e81e3353b436679749607233b8 GIT binary patch literal 1393 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 LL~r5?(F(~Qi|JWF literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/indextest/testdata/dude.jpg b/vendor/github.com/camlistore/camlistore/pkg/index/indextest/testdata/dude.jpg new file mode 100644 index 0000000000000000000000000000000000000000..447710ecc79f39cd08c0d10fe10261d622c84758 GIT binary patch literal 1932 zcmb7Cc{JPU8vaE@XbB;9u|{L3sf>=LmBbc1ZPiFnB{NJKgKAn-sj6iRN<&*pg{HQm z)>LY_L%7;%ucO*ZYEP)8Ox3ltRBn3jopbNM^L)>F|9Ic`dA{d6=M#;KW&j0u7orOQ z0)YTG@c~5pfD$2&5*AO1(7hNHAF1m~^zs$G0|)>FEGa1ohKLIU0+E8sN<+nvKX_0^ z7A~)-2$zS$l~lA;l@J=raJZVDng$Z7qobpws*lpwMrmp5Xn!*SiKS2}C`?)!rj39j zwEr?he*@ssKm>pRgY*FjI0y^}iMRj~00H1{1Oa~nDlG$%kdy*}#ba9q00fp02N%be z`S$`y0xSuL9|2dC*2DQJ9S(~{=o?&>QO2MQ(+XP9)&&2!LS|L7Sc~{S&;NRV>%#wc z)1O^FAS=!w0SCi@<3Q(yol3<*mI9zrn0Pw9nwO)_!ypNsEop*0vqi>zIU@Rz@;%LO#sPXyfWB%3{)X=XY7ObPl|^d)y+=ZQei$GF*d z>b=y-bt&m6lB?C|1Rf~@8a(QHcM>N*Vs9zV5s&7r(AXh&DH#ooUJ2$BJW~tS0Y7l8 zUbk!cwW%hrm$G(c952qSdadug^@C6fclnkAv$D|D7gu+!WKYc^d3`r8IFz=vm|q{$ zF=6kU;h?aLA9H1DJsBa9u=@jvq@mWS2DYIUy-VMnz-r}@h_C(#5q84ooara4--Trq z1_<(u42|42Q00yRBz#wRSLzFDZV!(h_Jq|w!sTLEjb_Y>fY+$>BJ}MIU~814map=Y zc1+K!(<>X(#a+EO=Y;HNHkvT$ELW2sdN{cgWwBqe=FB0$-SOZbBLFg`w2grau3K0i@y(N3NoBnJpo zN{f)gS2hl6FyGOdhZ<^9YKuqZBIk`TcB=ftm93EHF6LeXfe#47%-Pt>4WHPG`SOWl z#IGiy&$>b_w=>6M z%xP;;x7QXgj8y2>`v~ffxI>J;QFclYdoY*#b#mJuuNAFS>oBTzPHAtH>PG$k(&l4+ z^*R_uGK5a#M4z>vv@cCIjHr6+#GtO!ESGB)=ONSA45r0E#CB3%yM^UI08Qt zmww=Jx9mFK|48reQ}Y|}Y#(5hCO5R{6l^mbV+Bi1x`~Fio7OOu7P?FPS<=>_fhJ-w z3-r%!G(D#PJ60&|Fmh^6C%9^;WMRHXk4ygUCt+#ux`mo?ymMz;E`8`X@0tW~EpT0) zlUUO8+2-mQ%3yHNBZh?5j?3natrFF8zt_}b!cQLnq=~WM1ls$|ncuS?`q_IDDd%d9 zXts4HQ?NG6%ooJnyf&rPmUxdc4t7v7L+Hd94WYbwT^Dd&4hL

    i+e?UGRIET1wukSIu%k>HlDpGl=<+J1_T!!n739bApeKR#rR@R4#NQLxVpkh19-Pad} z`BWDWB-r6)rxOayFREX$^-RWHJ&ZE-O@ts%mUH3!hF@emYgMtBDeO}!-qI>9JXmc4 zyN-7q1Mk&u=){ipywwRKx!ClnYFQ>nJdsEW(M*l=chhAN9T3uz+H Target targ-123}", + claim1, claim1Time, pn) + if g := p.String(); g != want { + t.Errorf("claim wrong.\n got: %s\nwant: %s", g, want) + } + } + } + tests = []test{ + {"somedir", 1}, + {"with|pipe", 1}, + {"void", 0}, + } + for _, tt := range tests { + paths, err := id.Index.PathsLookup(id.SignerBlobRef, pn, tt.blobref) + if err != nil { + t.Fatalf("PathsLookup(%q): %v", tt.blobref, err) + } + if len(paths) != tt.want { + t.Fatalf("PathsLookup(%q) got %d results; want %d", + tt.blobref, len(paths), tt.want) + } + if tt.blobref == "with|pipe" { + p := paths[0] + want := fmt.Sprintf( + "Path{Claim: %s, %s; Base: %s + Suffix \"with|pipe\" => Target targ-124}", + claim2, claim2Time, pn) + if g := p.String(); g != want { + t.Errorf("claim wrong.\n got: %s\nwant: %s", g, want) + } + } + } + + // now test deletions + // Delete an existing value + claim3 := id.Delete(claim2) + t.Logf("claim %q deletes path claim %q", claim3, claim2) + tests = []test{ + {"targ-123", 1}, + {"targ-124", 0}, + {"targ-125", 0}, + } + for _, tt := range tests { + signer := id.SignerBlobRef + paths, err := id.Index.PathsOfSignerTarget(signer, blob.ParseOrZero(tt.blobref)) + if err != nil { + t.Fatalf("PathsOfSignerTarget(%q): %v", tt.blobref, err) + } + if len(paths) != tt.want { + t.Fatalf("PathsOfSignerTarget(%q) got %d results; want %d", + tt.blobref, len(paths), tt.want) + } + } + tests = []test{ + {"somedir", 1}, + {"with|pipe", 0}, + {"void", 0}, + } + for _, tt := range tests { + paths, err := id.Index.PathsLookup(id.SignerBlobRef, pn, tt.blobref) + if err != nil { + t.Fatalf("PathsLookup(%q): %v", tt.blobref, err) + } + if len(paths) != tt.want { + t.Fatalf("PathsLookup(%q) got %d results; want %d", + tt.blobref, len(paths), tt.want) + } + } + + // recreate second path, and test if the previous deletion of it + // is indeed ignored. + claim4 := id.Delete(claim3) + t.Logf("delete claim %q deletes claim %q, which should undelete %q", claim4, claim3, claim2) + tests = []test{ + {"targ-123", 1}, + {"targ-124", 1}, + {"targ-125", 0}, + } + for _, tt := range tests { + signer := id.SignerBlobRef + paths, err := id.Index.PathsOfSignerTarget(signer, blob.ParseOrZero(tt.blobref)) + if err != nil { + t.Fatalf("PathsOfSignerTarget(%q): %v", tt.blobref, err) + } + if len(paths) != tt.want { + t.Fatalf("PathsOfSignerTarget(%q) got %d results; want %d", + tt.blobref, len(paths), tt.want) + } + // and check the modtime too + if tt.blobref == "targ-124" { + p := paths[0] + want := fmt.Sprintf( + "Path{Claim: %s, %v; Base: %s + Suffix \"with|pipe\" => Target targ-124}", + claim2, claim2Time, pn) + if g := p.String(); g != want { + t.Errorf("claim wrong.\n got: %s\nwant: %s", g, want) + } + } + } + tests = []test{ + {"somedir", 1}, + {"with|pipe", 1}, + {"void", 0}, + } + for _, tt := range tests { + paths, err := id.Index.PathsLookup(id.SignerBlobRef, pn, tt.blobref) + if err != nil { + t.Fatalf("PathsLookup(%q): %v", tt.blobref, err) + } + if len(paths) != tt.want { + t.Fatalf("PathsLookup(%q) got %d results; want %d", + tt.blobref, len(paths), tt.want) + } + // and check that modtime is now claim4Time + if tt.blobref == "with|pipe" { + p := paths[0] + want := fmt.Sprintf( + "Path{Claim: %s, %s; Base: %s + Suffix \"with|pipe\" => Target targ-124}", + claim2, claim2Time, pn) + if g := p.String(); g != want { + t.Errorf("claim wrong.\n got: %s\nwant: %s", g, want) + } + } + } +} + +func Files(t *testing.T, initIdx func() *index.Index) { + id := NewIndexDeps(initIdx()) + id.Fataler = t + fileTime := time.Unix(1361250375, 0) + fileRef, wholeRef := id.UploadFile("foo.html", "I am an html file.", fileTime) + t.Logf("uploaded fileref %q, wholeRef %q", fileRef, wholeRef) + id.DumpIndex(t) + + // ExistingFileSchemas + { + key := fmt.Sprintf("wholetofile|%s|%s", wholeRef, fileRef) + if g, e := id.Get(key), "1"; g != e { + t.Fatalf("%q = %q, want %q", key, g, e) + } + + refs, err := id.Index.ExistingFileSchemas(wholeRef) + if err != nil { + t.Fatalf("ExistingFileSchemas = %v", err) + } + want := []blob.Ref{fileRef} + if !reflect.DeepEqual(refs, want) { + t.Errorf("ExistingFileSchemas got = %#v, want %#v", refs, want) + } + } + + // FileInfo + { + key := fmt.Sprintf("fileinfo|%s", fileRef) + if g, e := id.Get(key), "31|foo.html|text%2Fhtml|sha1-153cb1b63a8f120a0e3e14ff34c64f169df9430f"; g != e { + t.Fatalf("%q = %q, want %q", key, g, e) + } + + fi, err := id.Index.GetFileInfo(fileRef) + if err != nil { + t.Fatalf("GetFileInfo = %v", err) + } + if got, want := fi.Size, int64(31); got != want { + t.Errorf("Size = %d, want %d", got, want) + } + if got, want := fi.FileName, "foo.html"; got != want { + t.Errorf("FileName = %q, want %q", got, want) + } + if got, want := fi.MIMEType, "text/html"; got != want { + t.Errorf("MIMEType = %q, want %q", got, want) + } + if got, want := fi.Time, fileTime; !got.Time().Equal(want) { + t.Errorf("Time = %v; want %v", got, want) + } + if got, want := fi.WholeRef, blob.MustParse("sha1-153cb1b63a8f120a0e3e14ff34c64f169df9430f"); got != want { + t.Errorf("WholeRef = %v; want %v", got, want) + } + } +} + +func EdgesTo(t *testing.T, initIdx func() *index.Index) { + idx := initIdx() + id := NewIndexDeps(idx) + id.Fataler = t + defer id.DumpIndex(t) + + // pn1 ---member---> pn2 + pn1 := id.NewPermanode() + pn2 := id.NewPermanode() + claim1 := id.AddAttribute(pn1, "camliMember", pn2.String()) + + t.Logf("edge %s --> %s", pn1, pn2) + + // Look for pn1 + { + edges, err := idx.EdgesTo(pn2, nil) + if err != nil { + t.Fatal(err) + } + if len(edges) != 1 { + t.Fatalf("num edges = %d; want 1", len(edges)) + } + wantEdge := &camtypes.Edge{ + From: pn1, + To: pn2, + FromType: "permanode", + } + if got, want := edges[0].String(), wantEdge.String(); got != want { + t.Errorf("Wrong edge.\n GOT: %v\nWANT: %v", got, want) + } + } + + // Delete claim -> break edge relationship. + del1 := id.Delete(claim1) + t.Logf("del claim %q deletes claim %q, breaks link between p1 and p2", del1, claim1) + // test that we can't find anymore pn1 from pn2 + { + edges, err := idx.EdgesTo(pn2, nil) + if err != nil { + t.Fatal(err) + } + if len(edges) != 0 { + t.Fatalf("num edges = %d; want 0", len(edges)) + } + } + + // Undelete, should restore the link. + del2 := id.Delete(del1) + t.Logf("del claim %q deletes del claim %q, restores link between p1 and p2", del2, del1) + { + edges, err := idx.EdgesTo(pn2, nil) + if err != nil { + t.Fatal(err) + } + if len(edges) != 1 { + t.Fatalf("num edges = %d; want 1", len(edges)) + } + wantEdge := &camtypes.Edge{ + From: pn1, + To: pn2, + FromType: "permanode", + } + if got, want := edges[0].String(), wantEdge.String(); got != want { + t.Errorf("Wrong edge.\n GOT: %v\nWANT: %v", got, want) + } + } +} + +func Delete(t *testing.T, initIdx func() *index.Index) { + idx := initIdx() + id := NewIndexDeps(idx) + id.Fataler = t + defer id.DumpIndex(t) + pn1 := id.NewPermanode() + t.Logf("uploaded permanode %q", pn1) + cl1 := id.SetAttribute(pn1, "tag", "foo1") + cl1Time := id.LastTime() + t.Logf("set attribute %q", cl1) + + // delete pn1 + delpn1 := id.Delete(pn1) + t.Logf("del claim %q deletes %q", delpn1, pn1) + deleted := idx.IsDeleted(pn1) + if !deleted { + t.Fatal("pn1 should be deleted") + } + + // and try to find it with SearchPermanodesWithAttr (which should not work) + { + ch := make(chan blob.Ref, 10) + req := &camtypes.PermanodeByAttrRequest{ + Signer: id.SignerBlobRef, + Attribute: "tag", + Query: "foo1"} + err := id.Index.SearchPermanodesWithAttr(ch, req) + if err != nil { + t.Fatalf("SearchPermanodesWithAttr = %v", err) + } + var got []blob.Ref + for r := range ch { + got = append(got, r) + } + want := []blob.Ref{} + if len(got) != len(want) { + t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want) + } + } + + // delete pn1 again with another claim + delpn1bis := id.Delete(pn1) + t.Logf("del claim %q deletes %q a second time", delpn1bis, pn1) + deleted = idx.IsDeleted(pn1) + if !deleted { + t.Fatal("pn1 should be deleted") + } + + // verify that deleting delpn1 is not enough to make pn1 undeleted + del2 := id.Delete(delpn1) + t.Logf("delete claim %q deletes %q, which should not yet revive %q", del2, delpn1, pn1) + deleted = idx.IsDeleted(pn1) + if !deleted { + t.Fatal("pn1 should not yet be undeleted") + } + // we should not yet be able to find it again with SearchPermanodesWithAttr + { + ch := make(chan blob.Ref, 10) + req := &camtypes.PermanodeByAttrRequest{ + Signer: id.SignerBlobRef, + Attribute: "tag", + Query: "foo1"} + err := id.Index.SearchPermanodesWithAttr(ch, req) + if err != nil { + t.Fatalf("SearchPermanodesWithAttr = %v", err) + } + var got []blob.Ref + for r := range ch { + got = append(got, r) + } + want := []blob.Ref{} + if len(got) != len(want) { + t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want) + } + } + + // delete delpn1bis as well -> should undelete pn1 + del2bis := id.Delete(delpn1bis) + t.Logf("delete claim %q deletes %q, which should revive %q", del2bis, delpn1bis, pn1) + deleted = idx.IsDeleted(pn1) + if deleted { + t.Fatal("pn1 should be undeleted") + } + // we should now be able to find it again with SearchPermanodesWithAttr + { + ch := make(chan blob.Ref, 10) + req := &camtypes.PermanodeByAttrRequest{ + Signer: id.SignerBlobRef, + Attribute: "tag", + Query: "foo1"} + err := id.Index.SearchPermanodesWithAttr(ch, req) + if err != nil { + t.Fatalf("SearchPermanodesWithAttr = %v", err) + } + var got []blob.Ref + for r := range ch { + got = append(got, r) + } + want := []blob.Ref{pn1} + if len(got) < 1 || got[0].String() != want[0].String() { + t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want) + } + } + + // Delete cl1 + del3 := id.Delete(cl1) + t.Logf("del claim %q deletes claim %q", del3, cl1) + deleted = idx.IsDeleted(cl1) + if !deleted { + t.Fatal("cl1 should be deleted") + } + // we should not find anything with SearchPermanodesWithAttr + { + ch := make(chan blob.Ref, 10) + req := &camtypes.PermanodeByAttrRequest{ + Signer: id.SignerBlobRef, + Attribute: "tag", + Query: "foo1"} + err := id.Index.SearchPermanodesWithAttr(ch, req) + if err != nil { + t.Fatalf("SearchPermanodesWithAttr = %v", err) + } + var got []blob.Ref + for r := range ch { + got = append(got, r) + } + want := []blob.Ref{} + if len(got) != len(want) { + t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want) + } + } + // and now check that AppendClaims finds nothing for pn + { + claims, err := id.Index.AppendClaims(nil, pn1, id.SignerBlobRef, "") + if err != nil { + t.Errorf("AppendClaims = %v", err) + } else { + want := []camtypes.Claim{} + if len(claims) != len(want) { + t.Errorf("id.Index.AppendClaims gives %q, want %q", claims, want) + } + } + } + + // undelete cl1 + del4 := id.Delete(del3) + t.Logf("del claim %q deletes del claim %q, which should undelete %q", del4, del3, cl1) + // We should now be able to find it again with both methods + { + ch := make(chan blob.Ref, 10) + req := &camtypes.PermanodeByAttrRequest{ + Signer: id.SignerBlobRef, + Attribute: "tag", + Query: "foo1"} + err := id.Index.SearchPermanodesWithAttr(ch, req) + if err != nil { + t.Fatalf("SearchPermanodesWithAttr = %v", err) + } + var got []blob.Ref + for r := range ch { + got = append(got, r) + } + want := []blob.Ref{pn1} + if len(got) < 1 || got[0].String() != want[0].String() { + t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want) + } + } + // and check that AppendClaims finds cl1, with the right modtime too + { + claims, err := id.Index.AppendClaims(nil, pn1, id.SignerBlobRef, "") + if err != nil { + t.Errorf("AppendClaims = %v", err) + } else { + want := []camtypes.Claim{ + camtypes.Claim{ + BlobRef: cl1, + Permanode: pn1, + Signer: id.SignerBlobRef, + Date: cl1Time.UTC(), + Type: "set-attribute", + Attr: "tag", + Value: "foo1", + }, + } + if !reflect.DeepEqual(claims, want) { + t.Errorf("GetOwnerClaims results differ.\n got: %v\nwant: %v", + claims, want) + } + } + } +} + +type searchResults []camtypes.RecentPermanode + +func (s searchResults) String() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "[%d search results: ", len(s)) + for _, r := range s { + fmt.Fprintf(&buf, "{BlobRef: %s, Signer: %s, LastModTime: %d}", + r.Permanode, r.Signer, r.LastModTime.Unix()) + } + buf.WriteString("]") + return buf.String() +} + +func Reindex(t *testing.T, initIdx func() *index.Index) { + defaultReindexMaxProcs := index.ReindexMaxProcs() + // if not startOoo, the outOfOrderIndexerLoop will not be started, + // which should demonstrate that: + // since delpn1 will be enumerated before pn1, and indexing of delpn1 + // requires pn1, reindexing will fail. + reindex := func(t *testing.T, initIdx func() *index.Index, startOoo bool) { + if startOoo { + index.SetReindexMaxProcs(defaultReindexMaxProcs) + os.Setenv("CAMLI_TESTREINDEX_DISABLE_OOO", "false") + } else { + // We set the concurrency to 1, otherwise we could get "lucky" as the + // 2nd goroutine could index pn1 before the 1st goroutine notices it + // is missing as a dependency of delpn1 (which is the point of our test). + index.SetReindexMaxProcs(1) + os.Setenv("CAMLI_TESTREINDEX_DISABLE_OOO", "true") + } + idx := initIdx() + id := NewIndexDeps(idx) + id.Fataler = t + + pn1 := id.NewPlannedPermanode("foo1") // sha1-f06e30253644014922f955733a641cbc64d43d73 + t.Logf("uploaded permanode %q", pn1) + + // delete pn1 + delpn1 := id.Delete(pn1) // sha1-1d4c60cb3ce967edfb3194afd36124ce3f87ece0 + t.Logf("del claim %q deletes %q", delpn1, pn1) + deleted := idx.IsDeleted(pn1) + if !deleted { + t.Fatal("pn1 should be deleted") + } + + err := id.Index.Reindex() + if !startOoo && err == nil { + t.Fatal("Reindexing without outOfOrderIndexerLoop should have failed") + } + if startOoo && err != nil { + t.Fatal(err) + } + } + + reindex(t, initIdx, false) + reindex(t, initIdx, true) +} + +type enumArgs struct { + ctx *context.Context + dest chan blob.SizedRef + after string + limit int +} + +func checkEnumerate(idx *index.Index, want []blob.SizedRef, args *enumArgs) error { + if args == nil { + args = &enumArgs{} + } + if args.ctx == nil { + args.ctx = context.New() + } + if args.dest == nil { + args.dest = make(chan blob.SizedRef) + } + if args.limit == 0 { + args.limit = 5000 + } + errCh := make(chan error) + go func() { + errCh <- idx.EnumerateBlobs(args.ctx, args.dest, args.after, args.limit) + }() + for k, sbr := range want { + got, ok := <-args.dest + if !ok { + return fmt.Errorf("could not enumerate blob %d", k) + } + if got != sbr { + return fmt.Errorf("enumeration %d: got %v, wanted %v", k, got, sbr) + } + } + _, ok := <-args.dest + if ok { + return errors.New("chan was not closed after enumeration") + } + return <-errCh +} + +func checkStat(idx *index.Index, want []blob.SizedRef) error { + dest := make(chan blob.SizedRef) + defer close(dest) + errCh := make(chan error) + input := make([]blob.Ref, len(want)) + for _, sbr := range want { + input = append(input, sbr.Ref) + } + go func() { + errCh <- idx.StatBlobs(dest, input) + }() + for k, sbr := range want { + got, ok := <-dest + if !ok { + return fmt.Errorf("could not get stat number %d", k) + } + if got != sbr { + return fmt.Errorf("stat %d: got %v, wanted %v", k, got, sbr) + } + } + return <-errCh +} + +func EnumStat(t *testing.T, initIdx func() *index.Index) { + idx := initIdx() + id := NewIndexDeps(idx) + id.Fataler = t + + type step func() error + + // so we can refer to the added permanodes without using hardcoded blobRefs + added := make(map[string]blob.Ref) + + stepAdd := func(contents string) step { // add the blob + return func() error { + pn := id.NewPlannedPermanode(contents) + t.Logf("uploaded permanode %q", pn) + added[contents] = pn + return nil + } + } + + stepEnumCheck := func(want []blob.SizedRef, args *enumArgs) step { // check the blob + return func() error { + if err := checkEnumerate(idx, want, args); err != nil { + return err + } + return nil + } + } + + missingBlob := blob.MustParse("sha1-0000000000000000000000000000000000000000") + stepDelete := func(toDelete blob.Ref) step { + return func() error { + del := id.Delete(missingBlob) + t.Logf("added del claim %v to delete %v", del, toDelete) + return nil + } + } + + stepStatCheck := func(want []blob.SizedRef) step { + return func() error { + if err := checkStat(idx, want); err != nil { + return err + } + return nil + } + } + + for _, v := range []string{ + "foo", + "barr", + "bazzz", + } { + stepAdd(v)() + } + foo := blob.SizedRef{ // sha1-95d7290eb38520b257ef88d32f5b8d6be4fa9203 + Ref: blob.MustParse(added["foo"].String()), + Size: 534, + } + bar := blob.SizedRef{ // sha1-88c232875c2d6cfedfe91a2b06ea5c236e0389f4 + Ref: blob.MustParse(added["barr"].String()), + Size: 535, + } + baz := blob.SizedRef{ // sha1-718177762f7aba80a8b156bdd2b5a775b15a3132 + Ref: blob.MustParse(added["bazzz"].String()), + Size: 536, + } + delMissing := blob.SizedRef{ // sha1-a0b4db6c57851e5c63bfa81f5bdfd1eb9e32624e + Ref: blob.MustParse("sha1-a0b4db6c57851e5c63bfa81f5bdfd1eb9e32624e"), + Size: 649, + } + + if err := stepEnumCheck([]blob.SizedRef{baz, bar, foo}, nil)(); err != nil { + t.Fatalf("first enum, testing order: %v", err) + } + + // Now again, but skipping baz's blob + if err := stepEnumCheck([]blob.SizedRef{bar, foo}, + &enumArgs{ + after: added["bazzz"].String(), + }, + )(); err != nil { + t.Fatalf("second enum, testing skipping with after: %v", err) + } + + // Now add a delete claim with a missing dep, which should add an "have" row in the old format, + // i.e. without the "|indexed" suffix. So we can test if we're still compatible with old rows. + stepDelete(missingBlob)() + if err := stepEnumCheck([]blob.SizedRef{baz, bar, foo, delMissing}, nil)(); err != nil { + t.Fatalf("third enum, testing old \"have\" row compat: %v", err) + } + + if err := stepStatCheck([]blob.SizedRef{foo, bar, baz, delMissing})(); err != nil { + t.Fatalf("stat check: %v", err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/interface.go b/vendor/github.com/camlistore/camlistore/pkg/index/interface.go new file mode 100644 index 00000000..05a9faf0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/interface.go @@ -0,0 +1,143 @@ +package index + +import ( + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/context" + "camlistore.org/pkg/types/camtypes" +) + +type Interface interface { + // os.ErrNotExist should be returned if the blob isn't known + GetBlobMeta(blob.Ref) (camtypes.BlobMeta, error) + + // Should return os.ErrNotExist if not found. + GetFileInfo(fileRef blob.Ref) (camtypes.FileInfo, error) + + // Should return os.ErrNotExist if not found. + GetImageInfo(fileRef blob.Ref) (camtypes.ImageInfo, error) + + // Should return os.ErrNotExist if not found. + GetMediaTags(fileRef blob.Ref) (map[string]string, error) + + // KeyId returns the GPG keyid (e.g. "2931A67C26F5ABDA) + // given the blobref of its ASCII-armored blobref. + // The error is ErrNotFound if not found. + KeyId(blob.Ref) (string, error) + + // AppendClaims appends to dst claims on the given permanode. + // The signerFilter and attrFilter are both optional. If non-zero, + // they filter the return items to only claims made by the given signer + // or claims about the given attribute, respectively. + // Deleted claims are never returned. + // The items may be appended in any order. + // + // TODO: this should take a context and a callback func + // instead of a dst, then it can append to a channel instead, + // and the context lets it be interrupted. The callback should + // take the context too, so the channel send's select can read + // from the Done channel. + AppendClaims(dst []camtypes.Claim, permaNode blob.Ref, + signerFilter blob.Ref, + attrFilter string) ([]camtypes.Claim, error) + + // TODO(bradfitz): methods below this line are slated for a redesign + // to work efficiently for the new in-memory index. + + // dest must be closed, even when returning an error. + // limit <= 0 means unlimited. + GetRecentPermanodes(dest chan<- camtypes.RecentPermanode, + owner blob.Ref, + limit int, + before time.Time) error + + // SearchPermanodes finds permanodes matching the provided + // request and sends unique permanode blobrefs to dest. + // In particular, if request.FuzzyMatch is true, a fulltext + // search is performed (if supported by the attribute(s)) + // instead of an exact match search. + // If request.Query is blank, the permanodes which have + // request.Attribute as an attribute (regardless of its value) + // are searched. + // Additionally, if request.Attribute is blank, all attributes + // are searched (as fulltext), otherwise the search is + // restricted to the named attribute. + // + // dest is always closed, regardless of the error return value. + SearchPermanodesWithAttr(dest chan<- blob.Ref, + request *camtypes.PermanodeByAttrRequest) error + + // ExistingFileSchemas returns 0 or more blobrefs of "bytes" + // (TODO(bradfitz): or file?) schema blobs that represent the + // bytes of a file given in bytesRef. The file schema blobs + // returned are not guaranteed to reference chunks that still + // exist on the blobservers, though. It's purely a hint for + // clients to avoid uploads if possible. Before re-using any + // returned blobref they should be checked. + // + // Use case: a user drag & drops a large file onto their + // browser to upload. (imagine that "large" means anything + // larger than a blobserver's max blob size) JavaScript can + // first SHA-1 the large file locally, then send the + // wholeFileRef to this call and see if they'd previously + // uploaded the same file in the past. If so, the upload + // can be avoided if at least one of the returned schemaRefs + // can be validated (with a validating HEAD request) to still + // all exist on the blob server. + ExistingFileSchemas(wholeFileRef blob.Ref) (schemaRefs []blob.Ref, err error) + + // GetDirMembers sends on dest the children of the static + // directory dirRef. It returns os.ErrNotExist if dirRef + // is nil. + // dest must be closed, even when returning an error. + // limit <= 0 means unlimited. + GetDirMembers(dirRef blob.Ref, dest chan<- blob.Ref, limit int) error + + // Given an owner key, a camliType 'claim', 'attribute' name, + // and specific 'value', find the most recent permanode that has + // a corresponding 'set-attribute' claim attached. + // Returns os.ErrNotExist if none is found. + // Only attributes white-listed by IsIndexedAttribute are valid. + // TODO(bradfitz): ErrNotExist here is a weird error message ("file" not found). change. + // TODO(bradfitz): use keyId instead of signer? + PermanodeOfSignerAttrValue(signer blob.Ref, attr, val string) (blob.Ref, error) + + // PathsOfSignerTarget queries the index about "camliPath:" + // URL-dispatch attributes. + // + // It returns a list of all the path claims that have been signed + // by the provided signer and point at the given target. + // + // This is used when editing a permanode, to figure work up + // the name resolution tree backwards ultimately to a + // camliRoot permanode (which should know its base URL), and + // then the complete URL(s) of a target can be found. + PathsOfSignerTarget(signer, target blob.Ref) ([]*camtypes.Path, error) + + // All Path claims for (signer, base, suffix) + PathsLookup(signer, base blob.Ref, suffix string) ([]*camtypes.Path, error) + + // Most recent Path claim for (signer, base, suffix) as of + // provided time 'at', or most recent if 'at' is nil. + PathLookup(signer, base blob.Ref, suffix string, at time.Time) (*camtypes.Path, error) + + // EdgesTo finds references to the provided ref. + // + // For instance, if ref is a permanode, it might find the parent permanodes + // that have ref as a member. + // Or, if ref is a static file, it might find static directories which contain + // that file. + // This is a way to go "up" or "back" in a hierarchy. + // + // opts may be nil to accept the defaults. + EdgesTo(ref blob.Ref, opts *camtypes.EdgesToOpts) ([]*camtypes.Edge, error) + + // EnumerateBlobMeta sends ch information about all blobs + // known to the indexer (which may be a subset of all total + // blobs, since the indexer is typically configured to not see + // non-metadata blobs) and then closes ch. When it returns an + // error, it also closes ch. The blobs may be sent in any order. + // If the context finishes, the return error is context.ErrCanceled. + EnumerateBlobMeta(*context.Context, chan<- camtypes.BlobMeta) error +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/keys.go b/vendor/github.com/camlistore/camlistore/pkg/index/keys.go new file mode 100644 index 00000000..6476586e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/keys.go @@ -0,0 +1,396 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "bytes" + "fmt" + "strings" + + "camlistore.org/pkg/blob" +) + +// requiredSchemaVersion is incremented every time +// an index key type is added, changed, or removed. +// Version 4: EXIF tags + GPS +// Version 5: wholeRef added to keyFileInfo +const requiredSchemaVersion = 5 + +// type of key returns the identifier in k before the first ":" or "|". +// (Originally we packed keys by hand and there are a mix of styles) +func typeOfKey(k string) string { + c := strings.Index(k, ":") + p := strings.Index(k, "|") + if c < 0 && p < 0 { + return "" + } + if c < 0 { + return k[:p] + } + if p < 0 { + return k[:c] + } + min := c + if p < min { + min = p + } + return k[:min] +} + +type keyType struct { + name string + keyParts []part + valParts []part +} + +func (k *keyType) Prefix(args ...interface{}) string { + return k.build(true, true, k.keyParts, args...) +} + +func (k *keyType) Key(args ...interface{}) string { + return k.build(false, true, k.keyParts, args...) +} + +func (k *keyType) Val(args ...interface{}) string { + return k.build(false, false, k.valParts, args...) +} + +func (k *keyType) build(isPrefix, isKey bool, parts []part, args ...interface{}) string { + var buf bytes.Buffer + if isKey { + buf.WriteString(k.name) + } + if !isPrefix && len(args) != len(parts) { + panic("wrong number of arguments") + } + if len(args) > len(parts) { + panic("too many arguments") + } + for i, arg := range args { + if isKey || i > 0 { + buf.WriteString("|") + } + asStr := func() string { + s, ok := arg.(string) + if !ok { + s = arg.(fmt.Stringer).String() + } + return s + } + switch parts[i].typ { + case typeIntStr: + switch arg.(type) { + case int, int64, uint64: + buf.WriteString(fmt.Sprintf("%d", arg)) + default: + panic("bogus int type") + } + case typeStr: + buf.WriteString(urle(asStr())) + case typeRawStr: + buf.WriteString(asStr()) + case typeReverseTime: + s := asStr() + const example = "2011-01-23T05:23:12" + if len(s) < len(example) || s[4] != '-' && s[10] != 'T' { + panic("doesn't look like a time: " + s) + } + buf.WriteString(reverseTimeString(s)) + case typeBlobRef: + if br, ok := arg.(blob.Ref); ok { + if br.Valid() { + buf.WriteString(br.String()) + } + break + } + fallthrough + default: + if s, ok := arg.(string); ok { + buf.WriteString(s) + } else { + buf.WriteString(arg.(fmt.Stringer).String()) + } + } + } + if isPrefix { + buf.WriteString("|") + } + return buf.String() +} + +type part struct { + name string + typ partType +} + +type partType int + +const ( + typeKeyId partType = iota // PGP key id + typeTime + typeReverseTime // time prepended with "rt" + each numeric digit reversed from '9' + typeBlobRef + typeStr // URL-escaped + typeIntStr // integer as string + typeRawStr // not URL-escaped +) + +var ( + // keySchemaVersion indexes the index schema version. + keySchemaVersion = &keyType{ + "schemaversion", + nil, + []part{ + {"version", typeIntStr}, + }, + } + + keyMissing = &keyType{ + "missing", + []part{ + {"have", typeBlobRef}, + {"needed", typeBlobRef}, + }, + []part{ + {"1", typeStr}, + }, + } + + // keyPermanodeClaim indexes when a permanode is modified (or deleted) by a claim. + // It ties the affected permanode to the date of the modification, the responsible + // claim, and the nature of the modification. + keyPermanodeClaim = &keyType{ + "claim", + []part{ + {"permanode", typeBlobRef}, // modified permanode + {"signer", typeKeyId}, + {"claimDate", typeTime}, + {"claim", typeBlobRef}, + }, + []part{ + {"claimType", typeStr}, + {"attr", typeStr}, + {"value", typeStr}, + // And the signerRef, which seems redundant + // with the signer keyId in the jey, but the + // Claim struct needs this, and there's 1:m + // for keyId:blobRef, so: + {"signerRef", typeBlobRef}, + }, + } + + keyRecentPermanode = &keyType{ + "recpn", + []part{ + {"owner", typeKeyId}, + {"modtime", typeReverseTime}, + {"claim", typeBlobRef}, + }, + nil, + } + + keyPathBackward = &keyType{ + "signertargetpath", + []part{ + {"signer", typeKeyId}, + {"target", typeBlobRef}, + {"claim", typeBlobRef}, // for key uniqueness + }, + []part{ + {"claimDate", typeTime}, + {"base", typeBlobRef}, + {"active", typeStr}, // 'Y', or 'N' for deleted + {"suffix", typeStr}, + }, + } + + keyPathForward = &keyType{ + "path", + []part{ + {"signer", typeKeyId}, + {"base", typeBlobRef}, + {"suffix", typeStr}, + {"claimDate", typeReverseTime}, + {"claim", typeBlobRef}, // for key uniqueness + }, + []part{ + {"active", typeStr}, // 'Y', or 'N' for deleted + {"target", typeBlobRef}, + }, + } + + keyWholeToFileRef = &keyType{ + "wholetofile", + []part{ + {"whole", typeBlobRef}, + {"schema", typeBlobRef}, // for key uniqueness + }, + []part{ + {"1", typeStr}, + }, + } + + keyFileInfo = &keyType{ + "fileinfo", + []part{ + {"file", typeBlobRef}, + }, + []part{ + {"size", typeIntStr}, + {"filename", typeStr}, + {"mimetype", typeStr}, + {"whole", typeBlobRef}, + }, + } + + keyFileTimes = &keyType{ + "filetimes", + []part{ + {"file", typeBlobRef}, + }, + []part{ + // 0, 1, or 2 comma-separated types.Time3339 + // strings for creation/mod times. Oldest, + // then newest. See FileInfo docs. + {"time3339s", typeStr}, + }, + } + + keySignerAttrValue = &keyType{ + "signerattrvalue", + []part{ + {"signer", typeKeyId}, + {"attr", typeStr}, + {"value", typeStr}, + {"claimdate", typeReverseTime}, + {"claimref", typeBlobRef}, + }, + []part{ + {"permanode", typeBlobRef}, + }, + } + + // keyDeleted indexes a claim that deletes an entity. It ties the deleted + // entity to the date it was deleted, and to the deleter claim. + keyDeleted = &keyType{ + "deleted", + []part{ + {"deleted", typeBlobRef}, // the deleted entity (a permanode or another claim) + {"claimdate", typeReverseTime}, + {"deleter", typeBlobRef}, // the deleter claim blobref + }, + nil, + } + + // Given a blobref (permanode or static file or directory), provide a mapping + // to potential parents (they may no longer be parents, in the case of permanodes). + // In the case of permanodes, camliMember or camliContent constitutes a forward + // edge. In the case of static directories, the forward path is dir->static set->file, + // and that's what's indexed here, inverted. + keyEdgeBackward = &keyType{ + "edgeback", + []part{ + {"child", typeBlobRef}, // the edge target; thing we want to find parent(s) of + {"parent", typeBlobRef}, // the parent / edge source (e.g. permanode blobref) + // the blobref is the blob establishing the relationship + // (for a permanode: the claim; for static: often same as parent) + {"blobref", typeBlobRef}, + }, + []part{ + {"parenttype", typeStr}, // either "permanode" or the camliType ("file", "static-set", etc) + {"name", typeStr}, // the name, if static. + }, + } + + // Width and height after any EXIF rotation. + keyImageSize = &keyType{ + "imagesize", + []part{ + {"fileref", typeBlobRef}, // blobref of "file" schema blob + }, + []part{ + {"width", typeStr}, + {"height", typeStr}, + }, + } + + // child of a directory + keyStaticDirChild = &keyType{ + "dirchild", + []part{ + {"dirref", typeBlobRef}, // blobref of "directory" schema blob + {"child", typeStr}, // blobref of the child + }, + []part{ + {"1", typeStr}, + }, + } + + // Media attributes (e.g. ID3 tags). Uses generic terms like + // "artist", "title", "album", etc. + keyMediaTag = &keyType{ + "mediatag", + []part{ + {"wholeRef", typeBlobRef}, // wholeRef for song + {"tag", typeStr}, + }, + []part{ + {"value", typeStr}, + }, + } + + // EXIF tags + keyEXIFTag = &keyType{ + "exiftag", + []part{ + {"wholeRef", typeBlobRef}, // of entire file, not fileref + {"tag", typeStr}, // uint16 tag number as hex: xxxx + }, + []part{ + {"type", typeStr}, // "int", "rat", "float", "string" + {"n", typeIntStr}, // n components of type + {"vals", typeRawStr}, // pipe-separated; rats are n/d. strings are URL-escaped. + }, + } + + // Redundant version of keyEXIFTag. TODO: maybe get rid of this. + // Easier to process as one row instead of 4, though. + keyEXIFGPS = &keyType{ + "exifgps", + []part{ + {"wholeRef", typeBlobRef}, // of entire file, not fileref + }, + []part{ + {"lat", typeStr}, + {"long", typeStr}, + }, + } +) + +func containsUnsafeRawStrByte(s string) bool { + for _, r := range s { + if r >= 'z' || r < ' ' { + // pipe ('|) and non-ASCII are above 'z'. + return true + } + if r == '%' || r == '+' { + // Could be interpretted as URL-encoded + return true + } + } + return false +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/keys_test.go b/vendor/github.com/camlistore/camlistore/pkg/index/keys_test.go new file mode 100644 index 00000000..93440ba6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/keys_test.go @@ -0,0 +1,44 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "testing" +) + +func TestKeyPrefix(t *testing.T) { + if g, e := keyRecentPermanode.Prefix("ABC"), "recpn|ABC|"; g != e { + t.Errorf("recpn = %q; want %q", g, e) + } +} + +func TestTypeOfKey(t *testing.T) { + tests := []struct { + in, want string + }{ + {"foo:bar", "foo"}, + {"foo|bar", "foo"}, + {"foo|bar:blah", "foo"}, + {"foo:bar|blah", "foo"}, + {"fooo", ""}, + } + for _, tt := range tests { + if got := typeOfKey(tt.in); got != tt.want { + t.Errorf("typeOfKey(%q) = %q; want %q", tt.in, got, tt.want) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/kvfile_test.go b/vendor/github.com/camlistore/camlistore/pkg/index/kvfile_test.go new file mode 100644 index 00000000..e1a37884 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/kvfile_test.go @@ -0,0 +1,104 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "sync" + "testing" + + "camlistore.org/pkg/index" + "camlistore.org/pkg/index/indextest" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/kvfile" + "camlistore.org/pkg/sorted/kvtest" + "camlistore.org/pkg/test" +) + +func newKvfileSorted(t *testing.T) (kv sorted.KeyValue, cleanup func()) { + td, err := ioutil.TempDir("", "kvfile-test") + if err != nil { + t.Fatal(err) + } + kv, err = kvfile.NewStorage(filepath.Join(td, "kvfile")) + if err != nil { + os.RemoveAll(td) + t.Fatal(err) + } + return kv, func() { + kv.Close() + os.RemoveAll(td) + } +} + +func TestSorted_Kvfile(t *testing.T) { + kv, cleanup := newKvfileSorted(t) + defer cleanup() + kvtest.TestSorted(t, kv) +} + +func indexTest(t *testing.T, + sortedGenfn func(t *testing.T) (sorted.KeyValue, func()), + tfn func(*testing.T, func() *index.Index)) { + defer test.TLog(t)() + var mu sync.Mutex // guards cleanups + var cleanups []func() + defer func() { + mu.Lock() // never unlocked + for _, fn := range cleanups { + fn() + } + }() + makeIndex := func() *index.Index { + s, cleanup := sortedGenfn(t) + mu.Lock() + cleanups = append(cleanups, cleanup) + mu.Unlock() + return index.MustNew(t, s) + } + tfn(t, makeIndex) +} + +func TestIndex_Kvfile(t *testing.T) { + indexTest(t, newKvfileSorted, indextest.Index) +} + +func TestPathsOfSignerTarget_Kvfile(t *testing.T) { + indexTest(t, newKvfileSorted, indextest.PathsOfSignerTarget) +} + +func TestFiles_Kvfile(t *testing.T) { + indexTest(t, newKvfileSorted, indextest.Files) +} + +func TestEdgesTo_Kvfile(t *testing.T) { + indexTest(t, newKvfileSorted, indextest.EdgesTo) +} + +func TestDelete_Kvfile(t *testing.T) { + indexTest(t, newKvfileSorted, indextest.Delete) +} + +func TestReindex_Kvfile(t *testing.T) { + indexTest(t, newKvfileSorted, indextest.Reindex) +} + +func TestEnumStat_Kvfile(t *testing.T) { + indexTest(t, newKvfileSorted, indextest.EnumStat) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/memindex.go b/vendor/github.com/camlistore/camlistore/pkg/index/memindex.go new file mode 100644 index 00000000..3f1010bf --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/memindex.go @@ -0,0 +1,55 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" +) + +func init() { + blobserver.RegisterStorageConstructor("memory-only-dev-indexer", + blobserver.StorageConstructor(newMemoryIndexFromConfig)) +} + +// NewMemoryIndex returns an Index backed only by memory, for use in tests. +func NewMemoryIndex() *Index { + ix, err := New(sorted.NewMemoryKeyValue()) + if err != nil { + // Nothing to fail in memory, so worth panicing about + // if we ever see something. + panic(err) + } + return ix +} + +func newMemoryIndexFromConfig(ld blobserver.Loader, config jsonconfig.Obj) (blobserver.Storage, error) { + blobPrefix := config.RequiredString("blobSource") + if err := config.Validate(); err != nil { + return nil, err + } + sto, err := ld.GetStorage(blobPrefix) + if err != nil { + return nil, err + } + + ix := NewMemoryIndex() + ix.InitBlobSource(sto) + + return ix, err +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/mongo_test.go b/vendor/github.com/camlistore/camlistore/pkg/index/mongo_test.go new file mode 100644 index 00000000..5ee85a12 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/mongo_test.go @@ -0,0 +1,74 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index_test + +import ( + "testing" + + "camlistore.org/pkg/index/indextest" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/kvtest" + _ "camlistore.org/pkg/sorted/mongo" + "camlistore.org/pkg/test/dockertest" +) + +func newMongoSorted(t *testing.T) (kv sorted.KeyValue, cleanup func()) { + dbname := "camlitest_" + osutil.Username() + containerID, ip := dockertest.SetupMongoContainer(t) + + kv, err := sorted.NewKeyValue(jsonconfig.Obj{ + "type": "mongo", + "host": ip, + "database": dbname, + }) + if err != nil { + containerID.KillRemove(t) + t.Fatal(err) + } + return kv, func() { + kv.Close() + containerID.KillRemove(t) + } +} + +func TestSorted_Mongo(t *testing.T) { + kv, cleanup := newMongoSorted(t) + defer cleanup() + kvtest.TestSorted(t, kv) +} + +func TestIndex_Mongo(t *testing.T) { + indexTest(t, newMongoSorted, indextest.Index) +} + +func TestPathsOfSignerTarget_Mongo(t *testing.T) { + indexTest(t, newMongoSorted, indextest.PathsOfSignerTarget) +} + +func TestFiles_Mongo(t *testing.T) { + indexTest(t, newMongoSorted, indextest.Files) +} + +func TestEdgesTo_Mongo(t *testing.T) { + indexTest(t, newMongoSorted, indextest.EdgesTo) +} + +func TestDelete_Mongo(t *testing.T) { + indexTest(t, newMongoSorted, indextest.Delete) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/mysql_test.go b/vendor/github.com/camlistore/camlistore/pkg/index/mysql_test.go new file mode 100644 index 00000000..b6c5b06b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/mysql_test.go @@ -0,0 +1,76 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index_test + +import ( + "testing" + + "camlistore.org/pkg/index/indextest" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/kvtest" + _ "camlistore.org/pkg/sorted/mysql" + "camlistore.org/pkg/test/dockertest" +) + +func newMySQLSorted(t *testing.T) (kv sorted.KeyValue, clean func()) { + dbname := "camlitest_" + osutil.Username() + containerID, ip := dockertest.SetupMySQLContainer(t, dbname) + + kv, err := sorted.NewKeyValue(jsonconfig.Obj{ + "type": "mysql", + "host": ip + ":3306", + "database": dbname, + "user": dockertest.MySQLUsername, + "password": dockertest.MySQLPassword, + }) + if err != nil { + containerID.KillRemove(t) + t.Fatal(err) + } + return kv, func() { + kv.Close() + containerID.KillRemove(t) + } +} + +func TestSorted_MySQL(t *testing.T) { + kv, clean := newMySQLSorted(t) + defer clean() + kvtest.TestSorted(t, kv) +} + +func TestIndex_MySQL(t *testing.T) { + indexTest(t, newMySQLSorted, indextest.Index) +} + +func TestPathsOfSignerTarget_MySQL(t *testing.T) { + indexTest(t, newMySQLSorted, indextest.PathsOfSignerTarget) +} + +func TestFiles_MySQL(t *testing.T) { + indexTest(t, newMySQLSorted, indextest.Files) +} + +func TestEdgesTo_MySQL(t *testing.T) { + indexTest(t, newMySQLSorted, indextest.EdgesTo) +} + +func TestDelete_MySQL(t *testing.T) { + indexTest(t, newMySQLSorted, indextest.Delete) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/postgres_test.go b/vendor/github.com/camlistore/camlistore/pkg/index/postgres_test.go new file mode 100644 index 00000000..5e130f7b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/postgres_test.go @@ -0,0 +1,77 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index_test + +import ( + "testing" + + "camlistore.org/pkg/index/indextest" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/kvtest" + _ "camlistore.org/pkg/sorted/postgres" + "camlistore.org/pkg/test/dockertest" +) + +func newPostgresSorted(t *testing.T) (kv sorted.KeyValue, clean func()) { + dbname := "camlitest_" + osutil.Username() + containerID, ip := dockertest.SetupPostgreSQLContainer(t, dbname) + + kv, err := sorted.NewKeyValue(jsonconfig.Obj{ + "type": "postgres", + "host": ip, + "database": dbname, + "user": dockertest.PostgresUsername, + "password": dockertest.PostgresPassword, + "sslmode": "disable", + }) + if err != nil { + containerID.KillRemove(t) + t.Fatal(err) + } + return kv, func() { + kv.Close() + containerID.KillRemove(t) + } +} + +func TestSorted_Postgres(t *testing.T) { + kv, clean := newPostgresSorted(t) + defer clean() + kvtest.TestSorted(t, kv) +} + +func TestIndex_Postgres(t *testing.T) { + indexTest(t, newPostgresSorted, indextest.Index) +} + +func TestPathsOfSignerTarget_Postgres(t *testing.T) { + indexTest(t, newPostgresSorted, indextest.PathsOfSignerTarget) +} + +func TestFiles_Postgres(t *testing.T) { + indexTest(t, newPostgresSorted, indextest.Files) +} + +func TestEdgesTo_Postgres(t *testing.T) { + indexTest(t, newPostgresSorted, indextest.EdgesTo) +} + +func TestDelete_Postgres(t *testing.T) { + indexTest(t, newPostgresSorted, indextest.Delete) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/receive.go b/vendor/github.com/camlistore/camlistore/pkg/index/receive.go new file mode 100644 index 00000000..e07b17a9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/receive.go @@ -0,0 +1,826 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "bytes" + "crypto/sha1" + "errors" + "fmt" + _ "image/gif" + _ "image/png" + "io" + "log" + "os" + "sort" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/images" + "camlistore.org/pkg/jsonsign" + "camlistore.org/pkg/magic" + "camlistore.org/pkg/media" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/types" + + "camlistore.org/third_party/github.com/hjfreyer/taglib-go/taglib" + "camlistore.org/third_party/github.com/rwcarlsen/goexif/exif" + "camlistore.org/third_party/github.com/rwcarlsen/goexif/tiff" + _ "camlistore.org/third_party/go/pkg/image/jpeg" +) + +// outOfOrderIndexerLoop asynchronously reindexes blobs received +// out of order. It panics if started more than once or if the +// index has no blobSource. +func (ix *Index) outOfOrderIndexerLoop() { + ix.mu.RLock() + if ix.oooRunning == true { + panic("outOfOrderIndexerLoop is already running") + } + if ix.blobSource == nil { + panic("index has no blobSource") + } + ix.oooRunning = true + ix.mu.RUnlock() +WaitTickle: + for _ = range ix.tickleOoo { + for { + ix.mu.Lock() + if len(ix.readyReindex) == 0 { + ix.mu.Unlock() + continue WaitTickle + } + var br blob.Ref + for br = range ix.readyReindex { + break + } + delete(ix.readyReindex, br) + ix.mu.Unlock() + + err := ix.indexBlob(br) + if err != nil { + log.Printf("out-of-order indexBlob(%v) = %v", br, err) + ix.mu.Lock() + if len(ix.needs[br]) == 0 { + ix.readyReindex[br] = true + } + ix.mu.Unlock() + } + } + } +} + +func (ix *Index) indexBlob(br blob.Ref) error { + ix.mu.RLock() + bs := ix.blobSource + ix.mu.RUnlock() + if bs == nil { + panic(fmt.Sprintf("index: can't re-index %v: no blobSource", br)) + } + rc, _, err := bs.Fetch(br) + if err != nil { + return fmt.Errorf("index: failed to fetch %v for reindexing: %v", br, err) + } + defer rc.Close() + if _, err := blobserver.Receive(ix, br, rc); err != nil { + return err + } + return nil +} + +type mutationMap struct { + kv map[string]string // the keys and values we populate + + // We record if we get a delete claim, so we can update + // the deletes cache right after committing the mutation. + // + // TODO(mpl): we only need to keep track of one claim so far, + // but I chose a slice for when we need to do multi-claims? + deletes []schema.Claim +} + +func (mm *mutationMap) Set(k, v string) { + if mm.kv == nil { + mm.kv = make(map[string]string) + } + mm.kv[k] = v +} + +func (mm *mutationMap) noteDelete(deleteClaim schema.Claim) { + mm.deletes = append(mm.deletes, deleteClaim) +} + +func blobsFilteringOut(v []blob.Ref, x blob.Ref) []blob.Ref { + switch len(v) { + case 0: + return nil + case 1: + if v[0] == x { + return nil + } + return v + } + nl := v[:0] + for _, vb := range v { + if vb != x { + nl = append(nl, vb) + } + } + return nl +} + +func (ix *Index) noteBlobIndexed(br blob.Ref) { + ix.mu.Lock() + defer ix.mu.Unlock() + for _, needer := range ix.neededBy[br] { + newNeeds := blobsFilteringOut(ix.needs[needer], br) + if len(newNeeds) == 0 { + ix.readyReindex[needer] = true + delete(ix.needs, needer) + select { + case ix.tickleOoo <- true: + default: + } + } else { + ix.needs[needer] = newNeeds + } + } + delete(ix.neededBy, br) +} + +func (ix *Index) removeAllMissingEdges(br blob.Ref) { + var toDelete []string + it := ix.queryPrefix(keyMissing, br) + for it.Next() { + toDelete = append(toDelete, it.Key()) + } + if err := it.Close(); err != nil { + // TODO: Care? Can lazily clean up later. + log.Printf("Iterator close error: %v", err) + } + for _, k := range toDelete { + if err := ix.s.Delete(k); err != nil { + log.Printf("Error deleting key %s: %v", k, err) + } + } +} + +func (ix *Index) ReceiveBlob(blobRef blob.Ref, source io.Reader) (retsb blob.SizedRef, err error) { + missingDeps := false + defer func() { + if err == nil { + ix.noteBlobIndexed(blobRef) + if !missingDeps { + ix.removeAllMissingEdges(blobRef) + } + } + }() + sniffer := NewBlobSniffer(blobRef) + written, err := io.Copy(sniffer, source) + if err != nil { + return + } + if haveVal, haveErr := ix.s.Get("have:" + blobRef.String()); haveErr == nil { + if strings.HasSuffix(haveVal, "|indexed") { + return blob.SizedRef{blobRef, uint32(written)}, nil + } + } + + sniffer.Parse() + + fetcher := &missTrackFetcher{ + fetcher: ix.blobSource, + } + + mm, err := ix.populateMutationMap(fetcher, blobRef, sniffer) + if err != nil { + if err != errMissingDep { + return + } + fetcher.mu.Lock() + defer fetcher.mu.Unlock() + if len(fetcher.missing) == 0 { + panic("errMissingDep happened, but no fetcher.missing recorded") + } + missingDeps = true + allRecorded := true + for _, missing := range fetcher.missing { + if err := ix.noteNeeded(blobRef, missing); err != nil { + allRecorded = false + } + } + if allRecorded { + // Lie and say things are good. We've + // successfully recorded that the blob isn't + // indexed, but we'll reindex it later once + // the dependent blobs arrive. + return blob.SizedRef{blobRef, uint32(written)}, nil + } + return + } + + if err := ix.commit(mm); err != nil { + return retsb, err + } + + if c := ix.corpus; c != nil { + if err = c.addBlob(blobRef, mm); err != nil { + return + } + } + + // TODO(bradfitz): log levels? These are generally noisy + // (especially in tests, like search/handler_test), but I + // could see it being useful in production. For now, disabled: + // + // mimeType := sniffer.MIMEType() + // log.Printf("indexer: received %s; type=%v; truncated=%v", blobRef, mimeType, sniffer.IsTruncated()) + + return blob.SizedRef{blobRef, uint32(written)}, nil +} + +// commit writes the contents of the mutationMap on a batch +// mutation and commits that batch. It also updates the deletes +// cache. +func (ix *Index) commit(mm *mutationMap) error { + // We want the update of the deletes cache to be atomic + // with the transaction commit, so we lock here instead + // of within updateDeletesCache. + ix.deletes.Lock() + defer ix.deletes.Unlock() + bm := ix.s.BeginBatch() + for k, v := range mm.kv { + bm.Set(k, v) + } + err := ix.s.CommitBatch(bm) + if err != nil { + return err + } + for _, cl := range mm.deletes { + if err := ix.updateDeletesCache(cl); err != nil { + return fmt.Errorf("Could not update the deletes cache after deletion from %v: %v", cl, err) + } + } + return nil +} + +// populateMutationMap populates keys & values that will be committed +// into the returned map. +// +// the blobref can be trusted at this point (it's been fully consumed +// and verified to match), and the sniffer has been populated. +func (ix *Index) populateMutationMap(fetcher *missTrackFetcher, br blob.Ref, sniffer *BlobSniffer) (*mutationMap, error) { + mm := &mutationMap{ + kv: map[string]string{ + "meta:" + br.String(): fmt.Sprintf("%d|%s", sniffer.Size(), sniffer.MIMEType()), + }, + } + var err error + if blob, ok := sniffer.SchemaBlob(); ok { + switch blob.Type() { + case "claim": + err = ix.populateClaim(fetcher, blob, mm) + case "file": + err = ix.populateFile(fetcher, blob, mm) + case "directory": + err = ix.populateDir(fetcher, blob, mm) + } + } + if err != nil && err != errMissingDep { + return nil, err + } + var haveVal string + if err == errMissingDep { + haveVal = fmt.Sprintf("%d", sniffer.Size()) + } else { + haveVal = fmt.Sprintf("%d|indexed", sniffer.Size()) + } + mm.kv["have:"+br.String()] = haveVal + ix.mu.Lock() + defer ix.mu.Unlock() + if len(fetcher.missing) == 0 { + // If err == nil, we're good. Else (err == errMissingDep), we + // know the error did not come from a fetching miss (because + // len(fetcher.missing) == 0) , but from an index miss. Therefore + // we know the miss has already been noted and will be dealt with + // later, so we can also pretend everything's fine. + return mm, nil + } + return mm, err +} + +// keepFirstN keeps the first N bytes written to it in Bytes. +type keepFirstN struct { + N int + Bytes []byte +} + +func (w *keepFirstN) Write(p []byte) (n int, err error) { + if n := w.N - len(w.Bytes); n > 0 { + if n > len(p) { + n = len(p) + } + w.Bytes = append(w.Bytes, p[:n]...) + } + return len(p), nil +} + +// missTrackFetcher is a blob.Fetcher that records which blob(s) it +// failed to load from src. +type missTrackFetcher struct { + fetcher blob.Fetcher + + mu sync.Mutex // guards missing + missing []blob.Ref +} + +func (f *missTrackFetcher) Fetch(br blob.Ref) (blob io.ReadCloser, size uint32, err error) { + blob, size, err = f.fetcher.Fetch(br) + if err == os.ErrNotExist { + f.mu.Lock() + defer f.mu.Unlock() + f.missing = append(f.missing, br) + err = errMissingDep + } + return +} + +// filePrefixReader is both a *bytes.Reader and a *schema.FileReader for use in readPrefixOrFile +type filePrefixReader interface { + io.Reader + io.ReaderAt +} + +// readPrefixOrFile executes a given func with a reader on the passed prefix and +// falls back to passing a reader on the whole file if the func returns an error. +func readPrefixOrFile(prefix []byte, fetcher blob.Fetcher, b *schema.Blob, fn func(filePrefixReader) error) (err error) { + pr := bytes.NewReader(prefix) + err = fn(pr) + if err == io.EOF || err == io.ErrUnexpectedEOF { + var fr *schema.FileReader + fr, err = b.NewFileReader(fetcher) + if err == nil { + err = fn(fr) + fr.Close() + } + } + return err +} + +// b: the parsed file schema blob +// mm: keys to populate +func (ix *Index) populateFile(fetcher blob.Fetcher, b *schema.Blob, mm *mutationMap) (err error) { + var times []time.Time // all creation or mod times seen; may be zero + times = append(times, b.ModTime()) + + blobRef := b.BlobRef() + fr, err := b.NewFileReader(fetcher) + if err != nil { + return err + } + defer fr.Close() + mime, mr := magic.MIMETypeFromReader(fr) + + sha1 := sha1.New() + var copyDest io.Writer = sha1 + var imageBuf *keepFirstN // or nil + if strings.HasPrefix(mime, "image/") { + imageBuf = &keepFirstN{N: 512 << 10} + copyDest = io.MultiWriter(copyDest, imageBuf) + } + size, err := io.Copy(copyDest, mr) + if err != nil { + return err + } + wholeRef := blob.RefFromHash(sha1) + + if imageBuf != nil { + var conf images.Config + decodeConfig := func(r filePrefixReader) error { + conf, err = images.DecodeConfig(r) + return err + } + if err := readPrefixOrFile(imageBuf.Bytes, fetcher, b, decodeConfig); err == nil { + mm.Set(keyImageSize.Key(blobRef), keyImageSize.Val(fmt.Sprint(conf.Width), fmt.Sprint(conf.Height))) + } + + var ft time.Time + fileTime := func(r filePrefixReader) error { + ft, err = schema.FileTime(r) + return err + } + if err = readPrefixOrFile(imageBuf.Bytes, fetcher, b, fileTime); err == nil { + times = append(times, ft) + } + log.Printf("filename %q exif = %v, %v", b.FileName(), ft, err) + + // TODO(mpl): find (generate?) more broken EXIF images to experiment with. + indexEXIFData := func(r filePrefixReader) error { + return indexEXIF(wholeRef, r, mm) + } + if err = readPrefixOrFile(imageBuf.Bytes, fetcher, b, indexEXIFData); err != nil { + log.Printf("error parsing EXIF: %v", err) + } + } + + var sortTimes []time.Time + for _, t := range times { + if !t.IsZero() { + sortTimes = append(sortTimes, t) + } + } + sort.Sort(types.ByTime(sortTimes)) + var time3339s string + switch { + case len(sortTimes) == 1: + time3339s = types.Time3339(sortTimes[0]).String() + case len(sortTimes) >= 2: + oldest, newest := sortTimes[0], sortTimes[len(sortTimes)-1] + time3339s = types.Time3339(oldest).String() + "," + types.Time3339(newest).String() + } + + mm.Set(keyWholeToFileRef.Key(wholeRef, blobRef), "1") + mm.Set(keyFileInfo.Key(blobRef), keyFileInfo.Val(size, b.FileName(), mime, wholeRef)) + mm.Set(keyFileTimes.Key(blobRef), keyFileTimes.Val(time3339s)) + + if strings.HasPrefix(mime, "audio/") { + indexMusic(io.NewSectionReader(fr, 0, fr.Size()), wholeRef, mm) + } + + return nil +} + +func tagFormatString(tag *tiff.Tag) string { + switch tag.Format() { + case tiff.IntVal: + return "int" + case tiff.RatVal: + return "rat" + case tiff.FloatVal: + return "float" + case tiff.StringVal: + return "string" + } + return "" +} + +type exifWalkFunc func(name exif.FieldName, tag *tiff.Tag) error + +func (f exifWalkFunc) Walk(name exif.FieldName, tag *tiff.Tag) error { return f(name, tag) } + +var errEXIFPanic = errors.New("EXIF library panicked while walking fields") + +func indexEXIF(wholeRef blob.Ref, r io.Reader, mm *mutationMap) (err error) { + var tiffErr error + ex, err := exif.Decode(r) + if err != nil { + tiffErr = err + if exif.IsCriticalError(err) { + if exif.IsShortReadTagValueError(err) { + return io.ErrUnexpectedEOF // trigger a retry with whole file + } + return + } + log.Printf("Non critical TIFF decoding error: %v", err) + } + defer func() { + // The EXIF library panics if you access a field past + // what the file contains. Be paranoid and just + // recover here, instead of crashing on an invalid + // EXIF file. + if e := recover(); e != nil { + err = errEXIFPanic + } + }() + + err = ex.Walk(exifWalkFunc(func(name exif.FieldName, tag *tiff.Tag) error { + tagFmt := tagFormatString(tag) + if tagFmt == "" { + return nil + } + key := keyEXIFTag.Key(wholeRef, fmt.Sprintf("%04x", tag.Id)) + numComp := int(tag.Count) + if tag.Format() == tiff.StringVal { + numComp = 1 + } + var val bytes.Buffer + val.WriteString(keyEXIFTag.Val(tagFmt, numComp, "")) + if tag.Format() == tiff.StringVal { + str, err := tag.StringVal() + if err != nil { + log.Printf("Invalid EXIF string data: %v", err) + return nil + } + if containsUnsafeRawStrByte(str) { + val.WriteString(urle(str)) + } else { + val.WriteString(str) + } + } else { + for i := 0; i < int(tag.Count); i++ { + if i > 0 { + val.WriteByte('|') + } + switch tagFmt { + case "int": + v, err := tag.Int(i) + if err != nil { + log.Printf("Invalid EXIF int data: %v", err) + return nil + } + fmt.Fprintf(&val, "%d", v) + case "rat": + n, d, err := tag.Rat2(i) + if err != nil { + log.Printf("Invalid EXIF rat data: %v", err) + return nil + } + fmt.Fprintf(&val, "%d/%d", n, d) + case "float": + v, err := tag.Float(i) + if err != nil { + log.Printf("Invalid EXIF float data: %v", err) + return nil + } + fmt.Fprintf(&val, "%v", v) + default: + panic("shouldn't get here") + } + } + } + valStr := val.String() + mm.Set(key, valStr) + return nil + })) + if err != nil { + return + } + + if exif.IsGPSError(tiffErr) { + log.Printf("Invalid EXIF GPS data: %v", tiffErr) + return nil + } + if lat, long, err := ex.LatLong(); err == nil { + mm.Set(keyEXIFGPS.Key(wholeRef), keyEXIFGPS.Val(fmt.Sprint(lat), fmt.Sprint(long))) + } else if !exif.IsTagNotPresentError(err) { + log.Printf("Invalid EXIF GPS data: %v", err) + } + return nil +} + +// indexMusic adds mutations to index the wholeRef by attached metadata and other properties. +func indexMusic(r types.SizeReaderAt, wholeRef blob.Ref, mm *mutationMap) { + tag, err := taglib.Decode(r, r.Size()) + if err != nil { + log.Print("index: error parsing tag: ", err) + return + } + + var footerLength int64 = 0 + if hasTag, err := media.HasID3v1Tag(r); err != nil { + log.Print("index: unable to check for ID3v1 tag: ", err) + return + } else if hasTag { + footerLength = media.ID3v1TagLength + } + + // Generate a hash of the audio portion of the file (i.e. excluding ID3v1 and v2 tags). + audioStart := int64(tag.TagSize()) + audioSize := r.Size() - audioStart - footerLength + hash := sha1.New() + if _, err := io.Copy(hash, io.NewSectionReader(r, audioStart, audioSize)); err != nil { + log.Print("index: error generating SHA1 from audio data: ", err) + return + } + mediaRef := blob.RefFromHash(hash) + + duration, err := media.GetMPEGAudioDuration(io.NewSectionReader(r, audioStart, audioSize)) + if err != nil { + log.Print("index: unable to calculate audio duration: ", err) + duration = 0 + } + + var yearStr, trackStr, discStr, durationStr string + if !tag.Year().IsZero() { + const justYearLayout = "2006" + yearStr = tag.Year().Format(justYearLayout) + } + if tag.Track() != 0 { + trackStr = fmt.Sprintf("%d", tag.Track()) + } + if tag.Disc() != 0 { + discStr = fmt.Sprintf("%d", tag.Disc()) + } + if duration != 0 { + durationStr = fmt.Sprintf("%d", duration/time.Millisecond) + } + + // Note: if you add to this map, please update + // pkg/search/query.go's MediaTagConstraint Tag docs. + tags := map[string]string{ + "title": tag.Title(), + "artist": tag.Artist(), + "album": tag.Album(), + "genre": tag.Genre(), + "musicbrainzalbumid": tag.CustomFrames()["MusicBrainz Album Id"], + "year": yearStr, + "track": trackStr, + "disc": discStr, + "mediaref": mediaRef.String(), + "durationms": durationStr, + } + + for tag, value := range tags { + if value != "" { + mm.Set(keyMediaTag.Key(wholeRef, tag), keyMediaTag.Val(value)) + } + } +} + +// b: the parsed file schema blob +// mm: keys to populate +func (ix *Index) populateDir(fetcher blob.Fetcher, b *schema.Blob, mm *mutationMap) error { + blobRef := b.BlobRef() + // TODO(bradfitz): move the NewDirReader and FileName method off *schema.Blob and onto + // StaticFile/StaticDirectory or something. + + dr, err := b.NewDirReader(fetcher) + if err != nil { + // TODO(bradfitz): propagate up a transient failure + // error type, so we can retry indexing files in the + // future if blobs are only temporarily unavailable. + log.Printf("index: error indexing directory, creating NewDirReader %s: %v", blobRef, err) + return nil + } + sts, err := dr.StaticSet() + if err != nil { + log.Printf("index: error indexing directory: can't get StaticSet: %v\n", err) + return nil + } + + mm.Set(keyFileInfo.Key(blobRef), keyFileInfo.Val(len(sts), b.FileName(), "", blob.Ref{})) + for _, br := range sts { + mm.Set(keyStaticDirChild.Key(blobRef, br.String()), "1") + } + return nil +} + +var errMissingDep = errors.New("blob was not fully indexed because of a missing dependency") + +// populateDeleteClaim adds to mm the entries resulting from the delete claim cl. +// It is assumed cl is a valid claim, and vr has already been verified. +func (ix *Index) populateDeleteClaim(cl schema.Claim, vr *jsonsign.VerifyRequest, mm *mutationMap) error { + br := cl.Blob().BlobRef() + target := cl.Target() + if !target.Valid() { + log.Print(fmt.Errorf("no valid target for delete claim %v", br)) + return nil + } + meta, err := ix.GetBlobMeta(target) + if err != nil { + if err == os.ErrNotExist { + if err := ix.noteNeeded(br, target); err != nil { + return fmt.Errorf("could not note that delete claim %v depends on %v: %v", br, target, err) + } + return errMissingDep + } + log.Print(fmt.Errorf("Could not get mime type of target blob %v: %v", target, err)) + return nil + } + + // TODO(mpl): create consts somewhere for "claim" and "permanode" as camliTypes, and use them, + // instead of hardcoding. Unless they already exist ? (didn't find them). + if meta.CamliType != "permanode" && meta.CamliType != "claim" { + log.Print(fmt.Errorf("delete claim target in %v is neither a permanode nor a claim: %v", br, meta.CamliType)) + return nil + } + mm.Set(keyDeleted.Key(target, cl.ClaimDateString(), br), "") + if meta.CamliType == "claim" { + return nil + } + recentKey := keyRecentPermanode.Key(vr.SignerKeyId, cl.ClaimDateString(), br) + mm.Set(recentKey, target.String()) + attr, value := cl.Attribute(), cl.Value() + claimKey := keyPermanodeClaim.Key(target, vr.SignerKeyId, cl.ClaimDateString(), br) + mm.Set(claimKey, keyPermanodeClaim.Val(cl.ClaimType(), attr, value, vr.CamliSigner)) + return nil +} + +func (ix *Index) populateClaim(fetcher *missTrackFetcher, b *schema.Blob, mm *mutationMap) error { + br := b.BlobRef() + + claim, ok := b.AsClaim() + if !ok { + // Skip bogus claim with malformed permanode. + return nil + } + + vr := jsonsign.NewVerificationRequest(b.JSON(), blob.NewSerialFetcher(ix.KeyFetcher, fetcher)) + if !vr.Verify() { + // TODO(bradfitz): ask if the vr.Err.(jsonsign.Error).IsPermanent() and retry + // later if it's not permanent? or maybe do this up a level? + if vr.Err != nil { + return vr.Err + } + return errors.New("index: populateClaim verification failure") + } + verifiedKeyId := vr.SignerKeyId + mm.Set("signerkeyid:"+vr.CamliSigner.String(), verifiedKeyId) + + if claim.ClaimType() == string(schema.DeleteClaim) { + if err := ix.populateDeleteClaim(claim, vr, mm); err != nil { + return err + } + mm.noteDelete(claim) + return nil + } + + pnbr := claim.ModifiedPermanode() + if !pnbr.Valid() { + // A different type of claim; not modifying a permanode. + return nil + } + + attr, value := claim.Attribute(), claim.Value() + recentKey := keyRecentPermanode.Key(verifiedKeyId, claim.ClaimDateString(), br) + mm.Set(recentKey, pnbr.String()) + claimKey := keyPermanodeClaim.Key(pnbr, verifiedKeyId, claim.ClaimDateString(), br) + mm.Set(claimKey, keyPermanodeClaim.Val(claim.ClaimType(), attr, value, vr.CamliSigner)) + + if strings.HasPrefix(attr, "camliPath:") { + targetRef, ok := blob.Parse(value) + if ok { + // TODO: deal with set-attribute vs. del-attribute + // properly? I think we get it for free when + // del-attribute has no Value, but we need to deal + // with the case where they explicitly delete the + // current value. + suffix := attr[len("camliPath:"):] + active := "Y" + if claim.ClaimType() == "del-attribute" { + active = "N" + } + baseRef := pnbr + claimRef := br + + key := keyPathBackward.Key(verifiedKeyId, targetRef, claimRef) + val := keyPathBackward.Val(claim.ClaimDateString(), baseRef, active, suffix) + mm.Set(key, val) + + key = keyPathForward.Key(verifiedKeyId, baseRef, suffix, claim.ClaimDateString(), claimRef) + val = keyPathForward.Val(active, targetRef) + mm.Set(key, val) + } + } + + if claim.ClaimType() != string(schema.DelAttributeClaim) && IsIndexedAttribute(attr) { + key := keySignerAttrValue.Key(verifiedKeyId, attr, value, claim.ClaimDateString(), br) + mm.Set(key, keySignerAttrValue.Val(pnbr)) + } + + if IsBlobReferenceAttribute(attr) { + targetRef, ok := blob.Parse(value) + if ok { + key := keyEdgeBackward.Key(targetRef, pnbr, br) + mm.Set(key, keyEdgeBackward.Val("permanode", "")) + } + } + + return nil +} + +// updateDeletesCache updates the index deletes cache with the cl delete claim. +// deleteClaim is trusted to be a valid delete Claim. +func (x *Index) updateDeletesCache(deleteClaim schema.Claim) error { + target := deleteClaim.Target() + deleter := deleteClaim.Blob() + when, err := deleter.ClaimDate() + if err != nil { + return fmt.Errorf("Could not get date of delete claim %v: %v", deleteClaim, err) + } + targetDeletions := append(x.deletes.m[target], + deletion{ + deleter: deleter.BlobRef(), + when: when, + }) + sort.Sort(sort.Reverse(byDeletionDate(targetDeletions))) + x.deletes.m[target] = targetDeletions + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/reversetime.go b/vendor/github.com/camlistore/camlistore/pkg/index/reversetime.go new file mode 100644 index 00000000..818ba135 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/reversetime.go @@ -0,0 +1,51 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "fmt" + "strings" +) + +func unreverseTimeString(s string) string { + if !strings.HasPrefix(s, "rt") { + panic(fmt.Sprintf("can't unreverse time string: %q", s)) + } + b := make([]byte, 0, len(s)-2) + b = appendReverseString(b, s[2:]) + return string(b) +} + +func reverseTimeString(s string) string { + b := make([]byte, 0, len(s)+2) + b = append(b, 'r') + b = append(b, 't') + b = appendReverseString(b, s) + return string(b) +} + +func appendReverseString(b []byte, s string) []byte { + for i := 0; i < len(s); i++ { + c := s[i] + if c >= '0' && c <= '9' { + b = append(b, '0'+('9'-c)) + } else { + b = append(b, c) + } + } + return b +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/sniff.go b/vendor/github.com/camlistore/camlistore/pkg/index/sniff.go new file mode 100644 index 00000000..d82081c0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/sniff.go @@ -0,0 +1,106 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "bytes" + "errors" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/magic" + "camlistore.org/pkg/schema" +) + +type BlobSniffer struct { + br blob.Ref + + header []byte + written int64 + meta *schema.Blob // or nil + mimeType string + camliType string +} + +func NewBlobSniffer(ref blob.Ref) *BlobSniffer { + if !ref.Valid() { + panic("invalid ref") + } + return &BlobSniffer{br: ref} +} + +func (sn *BlobSniffer) SchemaBlob() (meta *schema.Blob, ok bool) { + return sn.meta, sn.meta != nil +} + +func (sn *BlobSniffer) Write(d []byte) (int, error) { + if !sn.br.Valid() { + panic("write on sniffer with invalid blobref") + } + sn.written += int64(len(d)) + if len(sn.header) < schema.MaxSchemaBlobSize { + n := schema.MaxSchemaBlobSize - len(sn.header) + if len(d) < n { + n = len(d) + } + sn.header = append(sn.header, d[:n]...) + } + return len(d), nil +} + +func (sn *BlobSniffer) Size() int64 { + return sn.written +} + +func (sn *BlobSniffer) IsTruncated() bool { + return sn.written > schema.MaxSchemaBlobSize +} + +func (sn *BlobSniffer) Body() ([]byte, error) { + if sn.IsTruncated() { + return nil, errors.New("index.Body: was truncated") + } + return sn.header, nil +} + +// MIMEType returns the sniffed blob's content-type or the empty string if unknown. +// If the blob is a Camlistore schema metadata blob, the MIME type will be of +// the form "application/json; camliType=foo". +func (sn *BlobSniffer) MIMEType() string { return sn.mimeType } + +func (sn *BlobSniffer) CamliType() string { return sn.camliType } + +func (sn *BlobSniffer) Parse() { + if sn.bufferIsCamliJSON() { + sn.camliType = sn.meta.Type() + sn.mimeType = "application/json; camliType=" + sn.camliType + } else { + sn.mimeType = magic.MIMEType(sn.header) + } +} + +func (sn *BlobSniffer) bufferIsCamliJSON() bool { + buf := sn.header + if !schema.LikelySchemaBlob(buf) { + return false + } + blob, err := schema.BlobFromReader(sn.br, bytes.NewReader(buf)) + if err != nil { + return false + } + sn.meta = blob + return true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/sqlindex/sqlindex.go b/vendor/github.com/camlistore/camlistore/pkg/index/sqlindex/sqlindex.go new file mode 100644 index 00000000..5d8b4b78 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/sqlindex/sqlindex.go @@ -0,0 +1,250 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package sqlindex implements the sorted.KeyValue interface using an *sql.DB. +package sqlindex + +import ( + "database/sql" + "errors" + "fmt" + "log" + "regexp" + "sync" + + "camlistore.org/pkg/leak" + "camlistore.org/pkg/sorted" +) + +// Storage implements the sorted.KeyValue interface using an *sql.DB. +type Storage struct { + DB *sql.DB + + // SetFunc is an optional func to use when REPLACE INTO does not exist + SetFunc func(*sql.DB, string, string) error + BatchSetFunc func(*sql.Tx, string, string) error + + // PlaceHolderFunc optionally replaces ? placeholders with the right ones for the rdbms + // in use + PlaceHolderFunc func(string) string + + // Serial determines whether a Go-level mutex protects DB from + // concurrent access. This isn't perfect and exists just for + // SQLite, whose driver likes to return "the database is + // locked" (camlistore.org/issue/114), so this keeps some + // pressure off. But we still trust SQLite to deal with + // concurrency in most cases. + Serial bool + + mu sync.Mutex // the mutex used, if Serial is set +} + +func (s *Storage) sql(v string) string { + if f := s.PlaceHolderFunc; f != nil { + return f(v) + } + return v +} + +type batchTx struct { + tx *sql.Tx + err error // sticky + + // SetFunc is an optional func to use when REPLACE INTO does not exist + SetFunc func(*sql.Tx, string, string) error + + // PlaceHolderFunc optionally replaces ? placeholders with the right ones for the rdbms + // in use + PlaceHolderFunc func(string) string +} + +func (b *batchTx) sql(v string) string { + if f := b.PlaceHolderFunc; f != nil { + return f(v) + } + return v +} + +func (b *batchTx) Set(key, value string) { + if b.err != nil { + return + } + if b.SetFunc != nil { + b.err = b.SetFunc(b.tx, key, value) + return + } + _, b.err = b.tx.Exec(b.sql("REPLACE INTO rows (k, v) VALUES (?, ?)"), key, value) +} + +func (b *batchTx) Delete(key string) { + if b.err != nil { + return + } + _, b.err = b.tx.Exec(b.sql("DELETE FROM rows WHERE k=?"), key) +} + +func (s *Storage) BeginBatch() sorted.BatchMutation { + if s.Serial { + s.mu.Lock() + } + tx, err := s.DB.Begin() + return &batchTx{ + tx: tx, + err: err, + SetFunc: s.BatchSetFunc, + PlaceHolderFunc: s.PlaceHolderFunc, + } +} + +func (s *Storage) CommitBatch(b sorted.BatchMutation) error { + if s.Serial { + defer s.mu.Unlock() + } + bt, ok := b.(*batchTx) + if !ok { + return fmt.Errorf("wrong BatchMutation type %T", b) + } + if bt.err != nil { + return bt.err + } + return bt.tx.Commit() +} + +func (s *Storage) Get(key string) (value string, err error) { + if s.Serial { + s.mu.Lock() + defer s.mu.Unlock() + } + err = s.DB.QueryRow(s.sql("SELECT v FROM rows WHERE k=?"), key).Scan(&value) + if err == sql.ErrNoRows { + err = sorted.ErrNotFound + } + return +} + +func (s *Storage) Set(key, value string) error { + if s.Serial { + s.mu.Lock() + defer s.mu.Unlock() + } + if s.SetFunc != nil { + return s.SetFunc(s.DB, key, value) + } + _, err := s.DB.Exec(s.sql("REPLACE INTO rows (k, v) VALUES (?, ?)"), key, value) + return err +} + +func (s *Storage) Delete(key string) error { + if s.Serial { + s.mu.Lock() + defer s.mu.Unlock() + } + _, err := s.DB.Exec(s.sql("DELETE FROM rows WHERE k=?"), key) + return err +} + +func (s *Storage) Close() error { return s.DB.Close() } + +func (s *Storage) Find(start, end string) sorted.Iterator { + if s.Serial { + s.mu.Lock() + defer s.mu.Unlock() + } + var rows *sql.Rows + var err error + if end == "" { + rows, err = s.DB.Query(s.sql("SELECT k, v FROM rows WHERE k >= ? ORDER BY k "), start) + } else { + rows, err = s.DB.Query(s.sql("SELECT k, v FROM rows WHERE k >= ? AND k < ? ORDER BY k "), start, end) + } + if err != nil { + log.Printf("unexpected query error: %v", err) + return &iter{err: err} + } + + it := &iter{ + s: s, + rows: rows, + closeCheck: leak.NewChecker(), + } + return it +} + +var wordThenPunct = regexp.MustCompile(`^\w+\W$`) + +// iter is a iterator over sorted key/value pairs in rows. +type iter struct { + s *Storage + end string // optional end bound + err error // accumulated error, returned at Close + + closeCheck *leak.Checker + + rows *sql.Rows // if non-nil, the rows we're reading from + + key sql.RawBytes + val sql.RawBytes + skey, sval *string // if non-nil, it's been stringified +} + +var errClosed = errors.New("sqlkv: Iterator already closed") + +func (t *iter) KeyBytes() []byte { return t.key } +func (t *iter) Key() string { + if t.skey != nil { + return *t.skey + } + str := string(t.key) + t.skey = &str + return str +} + +func (t *iter) ValueBytes() []byte { return t.val } +func (t *iter) Value() string { + if t.sval != nil { + return *t.sval + } + str := string(t.val) + t.sval = &str + return str +} + +func (t *iter) Close() error { + t.closeCheck.Close() + if t.rows != nil { + t.rows.Close() + t.rows = nil + } + err := t.err + t.err = errClosed + return err +} + +func (t *iter) Next() bool { + if t.err != nil { + return false + } + t.skey, t.sval = nil, nil + if !t.rows.Next() { + return false + } + t.err = t.rows.Scan(&t.key, &t.val) + if t.err != nil { + log.Printf("unexpected Scan error: %v", t.err) + return false + } + return true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/sqlite/sqlite.go b/vendor/github.com/camlistore/camlistore/pkg/index/sqlite/sqlite.go new file mode 100644 index 00000000..50a625f3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/sqlite/sqlite.go @@ -0,0 +1,3 @@ +// Empty file to make the go tool happy with a test-only directory. + +package sqlite diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/sqlite/sqlite_test.go b/vendor/github.com/camlistore/camlistore/pkg/index/sqlite/sqlite_test.go new file mode 100644 index 00000000..71ee4b6f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/sqlite/sqlite_test.go @@ -0,0 +1,185 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sqlite_test + +import ( + "bytes" + "database/sql" + "fmt" + "io/ioutil" + "os" + "os/exec" + "sync" + "testing" + + "camlistore.org/pkg/index" + "camlistore.org/pkg/index/indextest" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/kvtest" + _ "camlistore.org/pkg/sorted/sqlite" + + _ "camlistore.org/third_party/github.com/mattn/go-sqlite3" +) + +var ( + once sync.Once + dbAvailable bool +) + +func do(db *sql.DB, sql string) { + _, err := db.Exec(sql) + if err == nil { + return + } + panic(fmt.Sprintf("Error %v running SQL: %s", err, sql)) +} + +func newSorted(t *testing.T) (kv sorted.KeyValue, clean func()) { + f, err := ioutil.TempFile("", "sqlite-test") + if err != nil { + t.Fatal(err) + } + + kv, err = sorted.NewKeyValue(jsonconfig.Obj{ + "type": "sqlite", + "file": f.Name(), + }) + if err != nil { + t.Fatal(err) + } + return kv, func() { + kv.Close() + os.Remove(f.Name()) + } +} + +func TestSorted_SQLite(t *testing.T) { + kv, clean := newSorted(t) + defer clean() + kvtest.TestSorted(t, kv) +} + +type tester struct{} + +func (tester) test(t *testing.T, tfn func(*testing.T, func() *index.Index)) { + var mu sync.Mutex // guards cleanups + var cleanups []func() + defer func() { + mu.Lock() // never unlocked + for _, fn := range cleanups { + fn() + } + }() + makeIndex := func() *index.Index { + s, cleanup := newSorted(t) + mu.Lock() + cleanups = append(cleanups, cleanup) + mu.Unlock() + return index.MustNew(t, s) + } + tfn(t, makeIndex) +} + +func TestIndex_SQLite(t *testing.T) { + tester{}.test(t, indextest.Index) +} + +func TestPathsOfSignerTarget_SQLite(t *testing.T) { + tester{}.test(t, indextest.PathsOfSignerTarget) +} + +func TestFiles_SQLite(t *testing.T) { + tester{}.test(t, indextest.Files) +} + +func TestEdgesTo_SQLite(t *testing.T) { + tester{}.test(t, indextest.EdgesTo) +} + +func TestDelete_SQLite(t *testing.T) { + tester{}.test(t, indextest.Delete) +} + +func TestConcurrency(t *testing.T) { + if testing.Short() { + t.Logf("skipping for short mode") + return + } + s, clean := newSorted(t) + defer clean() + const n = 100 + ch := make(chan error) + for i := 0; i < n; i++ { + i := i + go func() { + bm := s.BeginBatch() + bm.Set("keyA-"+fmt.Sprint(i), fmt.Sprintf("valA=%d", i)) + bm.Set("keyB-"+fmt.Sprint(i), fmt.Sprintf("valB=%d", i)) + ch <- s.CommitBatch(bm) + }() + } + for i := 0; i < n; i++ { + if err := <-ch; err != nil { + t.Errorf("%d: %v", i, err) + } + } +} + +func numFDs(t *testing.T) int { + lsofPath, err := exec.LookPath("lsof") + if err != nil { + t.Skipf("No lsof available; skipping test") + } + out, err := exec.Command(lsofPath, "-n", "-p", fmt.Sprint(os.Getpid())).Output() + if err != nil { + t.Skipf("Error running lsof; skipping test: %s", err) + } + return bytes.Count(out, []byte("\n")) - 1 // hacky +} + +func TestFDLeak(t *testing.T) { + if testing.Short() { + t.Skip("Skipping in short mode.") + } + fd0 := numFDs(t) + t.Logf("fd0 = %d", fd0) + + s, clean := newSorted(t) + defer clean() + + bm := s.BeginBatch() + const numRows = 150 // 3x the batchSize of 50 in sqlindex.go; to gaurantee we do multiple batches + for i := 0; i < numRows; i++ { + bm.Set(fmt.Sprintf("key:%05d", i), fmt.Sprint(i)) + } + if err := s.CommitBatch(bm); err != nil { + t.Fatal(err) + } + for i := 0; i < 5; i++ { + it := s.Find("key:", "key~") + n := 0 + for it.Next() { + n++ + } + if n != numRows { + t.Errorf("iterated over %d rows; want %d", n, numRows) + } + it.Close() + t.Logf("fd after iteration %d = %d", i, numFDs(t)) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/index/util.go b/vendor/github.com/camlistore/camlistore/pkg/index/util.go new file mode 100644 index 00000000..6ac352e1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/index/util.go @@ -0,0 +1,44 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "net/url" +) + +var urle = url.QueryEscape + +func urld(s string) string { + d, _ := url.QueryUnescape(s) + return d +} + +type dupSkipper struct { + m map[string]bool +} + +// not thread safe. +func (s *dupSkipper) Dup(v string) bool { + if s.m == nil { + s.m = make(map[string]bool) + } + if s.m[v] { + return true + } + s.m[v] = true + return false +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/eval.go b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/eval.go new file mode 100644 index 00000000..0020821a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/eval.go @@ -0,0 +1,292 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package jsonconfig + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "log" + "os" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + + "camlistore.org/pkg/errorutil" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/wkfs" +) + +type stringVector struct { + v []string +} + +func (v *stringVector) Push(s string) { + v.v = append(v.v, s) +} + +func (v *stringVector) Pop() { + v.v = v.v[:len(v.v)-1] +} + +func (v *stringVector) Last() string { + return v.v[len(v.v)-1] +} + +// A File is the type returned by ConfigParser.Open. +type File interface { + io.ReadSeeker + io.Closer + Name() string +} + +// ConfigParser specifies the environment for parsing a config file +// and evaluating expressions. +type ConfigParser struct { + rootJSON Obj + + touchedFiles map[string]bool + includeStack stringVector + + // Open optionally specifies an opener function. + Open func(filename string) (File, error) +} + +func (c *ConfigParser) open(filename string) (File, error) { + if c.Open == nil { + return wkfs.Open(filename) + } + return c.Open(filename) +} + +// Validates variable names for config _env expresssions +var envPattern = regexp.MustCompile(`\$\{[A-Za-z0-9_]+\}`) + +// ReadFile parses the provided path and returns the config file. +// If path is empty, the c.Open function must be defined. +func (c *ConfigParser) ReadFile(path string) (m map[string]interface{}, err error) { + if path == "" && c.Open == nil { + return nil, errors.New("ReadFile of empty string but Open hook not defined") + } + c.touchedFiles = make(map[string]bool) + c.rootJSON, err = c.recursiveReadJSON(path) + return c.rootJSON, err +} + +// Decodes and evaluates a json config file, watching for include cycles. +func (c *ConfigParser) recursiveReadJSON(configPath string) (decodedObject map[string]interface{}, err error) { + if configPath != "" { + absConfigPath, err := filepath.Abs(configPath) + if err != nil { + return nil, fmt.Errorf("Failed to expand absolute path for %s", configPath) + } + if c.touchedFiles[absConfigPath] { + return nil, fmt.Errorf("ConfigParser include cycle detected reading config: %v", + absConfigPath) + } + c.touchedFiles[absConfigPath] = true + + c.includeStack.Push(absConfigPath) + defer c.includeStack.Pop() + } + + var f File + if f, err = c.open(configPath); err != nil { + return nil, fmt.Errorf("Failed to open config: %v", err) + } + defer f.Close() + + decodedObject = make(map[string]interface{}) + dj := json.NewDecoder(f) + if err = dj.Decode(&decodedObject); err != nil { + extra := "" + if serr, ok := err.(*json.SyntaxError); ok { + if _, serr := f.Seek(0, os.SEEK_SET); serr != nil { + log.Fatalf("seek error: %v", serr) + } + line, col, highlight := errorutil.HighlightBytePosition(f, serr.Offset) + extra = fmt.Sprintf(":\nError at line %d, column %d (file offset %d):\n%s", + line, col, serr.Offset, highlight) + } + return nil, fmt.Errorf("error parsing JSON object in config file %s%s\n%v", + f.Name(), extra, err) + } + + if err = c.evaluateExpressions(decodedObject, nil, false); err != nil { + return nil, fmt.Errorf("error expanding JSON config expressions in %s:\n%v", + f.Name(), err) + } + + return decodedObject, nil +} + +var regFunc = map[string]expanderFunc{} + +// RegisterFunc registers a new function that may be called from JSON +// configs using an array of the form ["_name", arg0, argN...]. +// The provided name must begin with an underscore. +func RegisterFunc(name string, fn func(c *ConfigParser, v []interface{}) (interface{}, error)) { + if len(name) < 2 || !strings.HasPrefix(name, "_") { + panic("illegal name") + } + if _, dup := regFunc[name]; dup { + panic("duplicate registration of " + name) + } + regFunc[name] = fn +} + +type expanderFunc func(c *ConfigParser, v []interface{}) (interface{}, error) + +func namedExpander(name string) (fn expanderFunc, ok bool) { + switch name { + case "_env": + return (*ConfigParser).expandEnv, true + case "_fileobj": + return (*ConfigParser).expandFile, true + } + fn, ok = regFunc[name] + return +} + +func (c *ConfigParser) evalValue(v interface{}) (interface{}, error) { + sl, ok := v.([]interface{}) + if !ok { + return v, nil + } + if name, ok := sl[0].(string); ok { + if expander, ok := namedExpander(name); ok { + newval, err := expander(c, sl[1:]) + if err != nil { + return nil, err + } + return newval, nil + } + } + for i, oldval := range sl { + newval, err := c.evalValue(oldval) + if err != nil { + return nil, err + } + sl[i] = newval + } + return v, nil +} + +// CheckTypes parses m and returns an error if it encounters a type or value +// that is not supported by this package. +func (c *ConfigParser) CheckTypes(m map[string]interface{}) error { + return c.evaluateExpressions(m, nil, true) +} + +// evaluateExpressions parses recursively m, populating it with the values +// that are found, unless testOnly is true. +func (c *ConfigParser) evaluateExpressions(m map[string]interface{}, seenKeys []string, testOnly bool) error { + for k, ei := range m { + thisPath := append(seenKeys, k) + switch subval := ei.(type) { + case string, bool, float64, nil: + continue + case []interface{}: + if len(subval) == 0 { + continue + } + evaled, err := c.evalValue(subval) + if err != nil { + return fmt.Errorf("%s: value error %v", strings.Join(thisPath, "."), err) + } + if !testOnly { + m[k] = evaled + } + case map[string]interface{}: + if err := c.evaluateExpressions(subval, thisPath, testOnly); err != nil { + return err + } + default: + return fmt.Errorf("%s: unhandled type %T", strings.Join(thisPath, "."), ei) + } + } + return nil +} + +// Permit either: +// ["_env", "VARIABLE"] (required to be set) +// or ["_env", "VARIABLE", "default_value"] +func (c *ConfigParser) expandEnv(v []interface{}) (interface{}, error) { + hasDefault := false + def := "" + if len(v) < 1 || len(v) > 2 { + return "", fmt.Errorf("_env expansion expected 1 or 2 args, got %d", len(v)) + } + s, ok := v[0].(string) + if !ok { + return "", fmt.Errorf("Expected a string after _env expansion; got %#v", v[0]) + } + boolDefault, wantsBool := false, false + if len(v) == 2 { + hasDefault = true + switch vdef := v[1].(type) { + case string: + def = vdef + case bool: + wantsBool = true + boolDefault = vdef + default: + return "", fmt.Errorf("Expected default value in %q _env expansion; got %#v", s, v[1]) + } + } + var err error + expanded := envPattern.ReplaceAllStringFunc(s, func(match string) string { + envVar := match[2 : len(match)-1] + val := os.Getenv(envVar) + // Special case: + if val == "" && envVar == "USER" && runtime.GOOS == "windows" { + val = os.Getenv("USERNAME") + } + if val == "" { + if hasDefault { + return def + } + err = fmt.Errorf("couldn't expand environment variable %q", envVar) + } + return val + }) + if wantsBool { + if expanded == "" { + return boolDefault, nil + } + return strconv.ParseBool(expanded) + } + return expanded, err +} + +func (c *ConfigParser) expandFile(v []interface{}) (exp interface{}, err error) { + if len(v) != 1 { + return "", fmt.Errorf("_file expansion expected 1 arg, got %d", len(v)) + } + var incPath string + if incPath, err = osutil.FindCamliInclude(v[0].(string)); err != nil { + return "", fmt.Errorf("Included config does not exist: %v", v[0]) + } + if exp, err = c.recursiveReadJSON(incPath); err != nil { + return "", fmt.Errorf("In file included from %s:\n%v", + c.includeStack.Last(), err) + } + return exp, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/jsonconfig.go b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/jsonconfig.go new file mode 100644 index 00000000..5f7bd609 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/jsonconfig.go @@ -0,0 +1,296 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package jsonconfig defines a helper type for JSON objects to be +// used for configuration. +package jsonconfig + +import ( + "fmt" + "sort" + "strconv" + "strings" +) + +// Obj is a JSON configuration map. +type Obj map[string]interface{} + +// Reads json config data from the specified open file, expanding +// all expressions +func ReadFile(configPath string) (Obj, error) { + var c ConfigParser + return c.ReadFile(configPath) +} + +func (jc Obj) RequiredObject(key string) Obj { + return jc.obj(key, false) +} + +func (jc Obj) OptionalObject(key string) Obj { + return jc.obj(key, true) +} + +func (jc Obj) obj(key string, optional bool) Obj { + jc.noteKnownKey(key) + ei, ok := jc[key] + if !ok { + if optional { + return make(Obj) + } + jc.appendError(fmt.Errorf("Missing required config key %q (object)", key)) + return make(Obj) + } + m, ok := ei.(map[string]interface{}) + if !ok { + jc.appendError(fmt.Errorf("Expected config key %q to be an object, not %T", key, ei)) + return make(Obj) + } + return m +} + +func (jc Obj) RequiredString(key string) string { + return jc.string(key, nil) +} + +func (jc Obj) OptionalString(key, def string) string { + return jc.string(key, &def) +} + +func (jc Obj) string(key string, def *string) string { + jc.noteKnownKey(key) + ei, ok := jc[key] + if !ok { + if def != nil { + return *def + } + jc.appendError(fmt.Errorf("Missing required config key %q (string)", key)) + return "" + } + s, ok := ei.(string) + if !ok { + jc.appendError(fmt.Errorf("Expected config key %q to be a string", key)) + return "" + } + return s +} + +func (jc Obj) RequiredStringOrObject(key string) interface{} { + return jc.stringOrObject(key, true) +} + +func (jc Obj) OptionalStringOrObject(key string) interface{} { + return jc.stringOrObject(key, false) +} + +func (jc Obj) stringOrObject(key string, required bool) interface{} { + jc.noteKnownKey(key) + ei, ok := jc[key] + if !ok { + if !required { + return nil + } + jc.appendError(fmt.Errorf("Missing required config key %q (string or object)", key)) + return "" + } + if _, ok := ei.(map[string]interface{}); ok { + return ei + } + if _, ok := ei.(string); ok { + return ei + } + jc.appendError(fmt.Errorf("Expected config key %q to be a string or object", key)) + return "" +} + +func (jc Obj) RequiredBool(key string) bool { + return jc.bool(key, nil) +} + +func (jc Obj) OptionalBool(key string, def bool) bool { + return jc.bool(key, &def) +} + +func (jc Obj) bool(key string, def *bool) bool { + jc.noteKnownKey(key) + ei, ok := jc[key] + if !ok { + if def != nil { + return *def + } + jc.appendError(fmt.Errorf("Missing required config key %q (boolean)", key)) + return false + } + switch v := ei.(type) { + case bool: + return v + case string: + b, err := strconv.ParseBool(v) + if err != nil { + jc.appendError(fmt.Errorf("Config key %q has bad boolean format %q", key, v)) + } + return b + default: + jc.appendError(fmt.Errorf("Expected config key %q to be a boolean", key)) + return false + } +} + +func (jc Obj) RequiredInt(key string) int { + return jc.int(key, nil) +} + +func (jc Obj) OptionalInt(key string, def int) int { + return jc.int(key, &def) +} + +func (jc Obj) int(key string, def *int) int { + jc.noteKnownKey(key) + ei, ok := jc[key] + if !ok { + if def != nil { + return *def + } + jc.appendError(fmt.Errorf("Missing required config key %q (integer)", key)) + return 0 + } + b, ok := ei.(float64) + if !ok { + jc.appendError(fmt.Errorf("Expected config key %q to be a number", key)) + return 0 + } + return int(b) +} + +func (jc Obj) RequiredInt64(key string) int64 { + return jc.int64(key, nil) +} + +func (jc Obj) OptionalInt64(key string, def int64) int64 { + return jc.int64(key, &def) +} + +func (jc Obj) int64(key string, def *int64) int64 { + jc.noteKnownKey(key) + ei, ok := jc[key] + if !ok { + if def != nil { + return *def + } + jc.appendError(fmt.Errorf("Missing required config key %q (integer)", key)) + return 0 + } + b, ok := ei.(float64) + if !ok { + jc.appendError(fmt.Errorf("Expected config key %q to be a number", key)) + return 0 + } + return int64(b) +} + +func (jc Obj) RequiredList(key string) []string { + return jc.requiredList(key, true) +} + +func (jc Obj) OptionalList(key string) []string { + return jc.requiredList(key, false) +} + +func (jc Obj) requiredList(key string, required bool) []string { + jc.noteKnownKey(key) + ei, ok := jc[key] + if !ok { + if required { + jc.appendError(fmt.Errorf("Missing required config key %q (list of strings)", key)) + } + return nil + } + eil, ok := ei.([]interface{}) + if !ok { + jc.appendError(fmt.Errorf("Expected config key %q to be a list, not %T", key, ei)) + return nil + } + sl := make([]string, len(eil)) + for i, ei := range eil { + s, ok := ei.(string) + if !ok { + jc.appendError(fmt.Errorf("Expected config key %q index %d to be a string, not %T", key, i, ei)) + return nil + } + sl[i] = s + } + return sl +} + +func (jc Obj) noteKnownKey(key string) { + _, ok := jc["_knownkeys"] + if !ok { + jc["_knownkeys"] = make(map[string]bool) + } + jc["_knownkeys"].(map[string]bool)[key] = true +} + +func (jc Obj) appendError(err error) { + ei, ok := jc["_errors"] + if ok { + jc["_errors"] = append(ei.([]error), err) + } else { + jc["_errors"] = []error{err} + } +} + +// UnknownKeys returns the keys from the config that have not yet been discovered by one of the RequiredT or OptionalT calls. +func (jc Obj) UnknownKeys() []string { + ei, ok := jc["_knownkeys"] + var known map[string]bool + if ok { + known = ei.(map[string]bool) + } + var unknown []string + for k, _ := range jc { + if ok && known[k] { + continue + } + if strings.HasPrefix(k, "_") { + // Permit keys with a leading underscore as a + // form of comments. + continue + } + unknown = append(unknown, k) + } + sort.Strings(unknown) + return unknown +} + +func (jc Obj) Validate() error { + unknown := jc.UnknownKeys() + for _, k := range unknown { + jc.appendError(fmt.Errorf("Unknown key %q", k)) + } + + ei, ok := jc["_errors"] + if !ok { + return nil + } + errList := ei.([]error) + if len(errList) == 1 { + return errList[0] + } + strs := make([]string, 0) + for _, v := range errList { + strs = append(strs, v.Error()) + } + return fmt.Errorf("Multiple errors: " + strings.Join(strs, ", ")) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/jsonconfig_test.go b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/jsonconfig_test.go new file mode 100644 index 00000000..70d796c2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/jsonconfig_test.go @@ -0,0 +1,104 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package jsonconfig + +import ( + "os" + "reflect" + "strings" + "testing" +) + +func TestIncludes(t *testing.T) { + obj, err := ReadFile("testdata/include1.json") + if err != nil { + t.Fatal(err) + } + two := obj.RequiredObject("two") + if err := obj.Validate(); err != nil { + t.Error(err) + } + if g, e := two.RequiredString("key"), "value"; g != e { + t.Errorf("sub object key = %q; want %q", g, e) + } +} + +func TestIncludeLoop(t *testing.T) { + _, err := ReadFile("testdata/loop1.json") + if err == nil { + t.Fatal("expected an error about import cycles.") + } + if !strings.Contains(err.Error(), "include cycle detected") { + t.Fatalf("expected an error about import cycles; got: %v", err) + } +} + +func TestBoolEnvs(t *testing.T) { + os.Setenv("TEST_EMPTY", "") + os.Setenv("TEST_TRUE", "true") + os.Setenv("TEST_ONE", "1") + os.Setenv("TEST_ZERO", "0") + os.Setenv("TEST_FALSE", "false") + obj, err := ReadFile("testdata/boolenv.json") + if err != nil { + t.Fatal(err) + } + if str := obj.RequiredString("emptystr"); str != "" { + t.Errorf("str = %q, want empty", str) + } + tests := []struct { + key string + want bool + }{ + {"def_false", false}, + {"def_true", true}, + {"set_true_def_false", true}, + {"set_false_def_true", false}, + {"lit_true", true}, + {"lit_false", false}, + {"one", true}, + {"zero", false}, + } + for _, tt := range tests { + if v := obj.RequiredBool(tt.key); v != tt.want { + t.Errorf("key %q = %v; want %v", tt.key, v, tt.want) + } + } + if err := obj.Validate(); err != nil { + t.Error(err) + } +} + +func TestListExpansion(t *testing.T) { + os.Setenv("TEST_BAR", "bar") + obj, err := ReadFile("testdata/listexpand.json") + if err != nil { + t.Fatal(err) + } + s := obj.RequiredString("str") + l := obj.RequiredList("list") + if err := obj.Validate(); err != nil { + t.Error(err) + } + want := []string{"foo", "bar"} + if !reflect.DeepEqual(l, want) { + t.Errorf("got = %#v\nwant = %#v", l, want) + } + if s != "bar" { + t.Errorf("str = %q, want %q", s, "bar") + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/boolenv.json b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/boolenv.json new file mode 100644 index 00000000..fe9431eb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/boolenv.json @@ -0,0 +1,11 @@ +{ + "emptystr": ["_env", "${TEST_EMPTY}", ""], + "def_false": ["_env", "${TEST_EMPTY}", false], + "def_true": ["_env", "${TEST_EMPTY}", true], + "set_true_def_false": ["_env", "${TEST_TRUE}", false], + "set_false_def_true": ["_env", "${TEST_FALSE}", true], + "one": ["_env", "${TEST_ONE}"], + "zero": ["_env", "${TEST_ZERO}"], + "lit_true": true, + "lit_false": false +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/include1.json b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/include1.json new file mode 100644 index 00000000..6d8b38e9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/include1.json @@ -0,0 +1,3 @@ +{ + "two": ["_fileobj", "testdata/include2.json"] +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/include2.json b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/include2.json new file mode 100644 index 00000000..7a9e8644 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/include2.json @@ -0,0 +1,3 @@ +{ + "key": "value" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/listexpand.json b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/listexpand.json new file mode 100644 index 00000000..ccabceff --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/listexpand.json @@ -0,0 +1,4 @@ +{ + "list": ["foo", ["_env", "${TEST_BAR}"]], + "str": ["_env", "${TEST_BAR}"] +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/loop1.json b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/loop1.json new file mode 100644 index 00000000..215146fd --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/loop1.json @@ -0,0 +1,3 @@ +{ + "obj": ["_fileobj", "testdata/loop2.json"] +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/loop2.json b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/loop2.json new file mode 100644 index 00000000..1d270eb4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonconfig/testdata/loop2.json @@ -0,0 +1,3 @@ +{ + "obj": ["_fileobj", "testdata/loop1.json"] +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/doc.go b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/doc.go new file mode 100644 index 00000000..ff30f857 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package jsonsign implements Camlistore's cryptographic signing and +// verification of JSON blobs. +package jsonsign diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/jsonsign_test.go b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/jsonsign_test.go new file mode 100644 index 00000000..b78eb986 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/jsonsign_test.go @@ -0,0 +1,222 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package jsonsign_test + +import ( + "bytes" + "fmt" + "sort" + "strings" + "testing" + + . "camlistore.org/pkg/jsonsign" + "camlistore.org/pkg/test" + . "camlistore.org/pkg/test/asserts" + "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp" +) + +var unsigned = `{"camliVersion": 1, +"camliType": "foo" +}` + +var pubKey1 = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +mQENBEzgoVsBCAC/56aEJ9BNIGV9FVP+WzenTAkg12k86YqlwJVAB/VwdMlyXxvi +bCT1RVRfnYxscs14LLfcMWF3zMucw16mLlJCBSLvbZ0jn4h+/8vK5WuAdjw2YzLs +WtBcjWn3lV6tb4RJz5gtD/o1w8VWxwAnAVIWZntKAWmkcChCRgdUeWso76+plxE5 +aRYBJqdT1mctGqNEISd/WYPMgwnWXQsVi3x4z1dYu2tD9uO1dkAff12z1kyZQIBQ +rexKYRRRh9IKAayD4kgS0wdlULjBU98aeEaMz1ckuB46DX3lAYqmmTEL/Rl9cOI0 +Enpn/oOOfYFa5h0AFndZd1blMvruXfdAobjVABEBAAG0JUNhbWxpIFRlc3RlciA8 +Y2FtbGktdGVzdEBleGFtcGxlLmNvbT6JATgEEwECACIFAkzgoVsCGwMGCwkIBwMC +BhUIAgkKCwQWAgMBAh4BAheAAAoJECkxpnwm9avaHE0IAJ/pMZgiURl3kefrFMAV +7ei0XDfTekZOwDRcZWTVQ/A97phpzO8t78qLYbFeHuq3myNhrlVO9Gyp+2V904rN +dudoHLhpegf5TNeHGmAGHBxcooMPMp0JyIDnUBxtCNGxgWfbKpEDRsQAjkCc7sR0 +H+OegzlEf6JZGzEhV5ohOioTsC1DmJNoQsRz5Kes7sLoAzpQCbCv4yv+1o+mnzgW +9qPJXKxcScc0t2YTvcvpJ7LV8no1OP6vpYqB1A9Pzze6XFBlcXOUKbRKk0fEIV/u +pU3ph1fF7wlyRgA4A3iPwDC4BgVmHYkz9nYPn+7IcT/dDig5SWU+n7WZgGeyv75y +0Ue5AQ0ETOChWwEIALuHxKI+oSH+eeMSXhxcSUXnhp4cUeyvOV7oNPYcmsDclF0Y +7y8NrSPiEZod9vSTEDMq7hd3BG+feCBqjgR4qtmoXguJhWcnJqDBk5iAMuuAph9O +CC8QLACMJPhoxQ0UtDPKlpG4X8kLK1woHd716ulPl2KLjTgd6K4kCGj+CV5Ekn6u +IJj+3IPbYDOwk1l06ksimwQAY4dA1CXOTviH1bVqR6CzuzVPg4hcryWDva1rEO5c +LcOR8Wk/thANFLSNjqX8UgtGXhFZRWxKetFDQiX5f2BKoqTVYvD3pqt+zzyLNFAz +xhMc3cyFfqM8yQdzdEey/DIWtMoDqZCSVMJ63N8AEQEAAYkBHwQYAQIACQUCTOCh +WwIbDAAKCRApMaZ8JvWr2mHACACkco+fAfRK+gmprF2m8E0Bp1frwFH0g4RJVHXQ +BUDbg7OZbWumzD4Br28si6XDVMP6fLOeyD0EHYb6LhAHDkBLqx6e3kKG1mQ8fMIV +O4YMQfskYH2FJqlCtgMnM8N3oslPBTpZedNPSUq7HJh2pKr9GIDi1V+Hgc/qEigE +dj9f2zSSaKZdC4eL73GvlQOh+4XqgaMnMiKfI+/2WlRaJs1KOgKmIp5yHt0qY0ef +y+40BY/z9pMjyUvr/Wwp8KXArw0NAwzp8NUl5fNxRg9XWQWLn6hW8ydR20X3t2ym +iNSWzNQiTT6k7fumOABCoSZsow/AJxQSxqKOJBjgpKjIKCgY +=ru0J +-----END PGP PUBLIC KEY BLOCK-----` + +var pubKey2 = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +mQENBEz61lcBCADRQhcb9LIQdV3LhU5f7cCjOctmLsL+y4k4VKmznssWORiNPEHQ +13CxFLjRDN2OQYXi4NSqoUqHNMsRTUJTVW0CnznUUb11ibXLUYW/zbPN9dWs8PlI +UZSScS1dxtGKKk+VfXrvc1LB6pqrjWmAgEwQxsBWToW2IFR/eMo1LiVU83dzpKU1 +n/yb8Jy9wizchspd9xecK2X0JnKLRIJklLTAKQ+XKP+cSwXmShcs+3pxu5f4piqF +7oBfh9noFA0vdGYNBGVch3DfJwFcTmLkkGFZKdiehWncvVYT1jxUkJvc0K44ohDH +smkG2VZm3rJCwi2GIWA/clLiDAhYM6vTI3oZABEBAAG0K0NhbWxpIFRlc3R1c2Vy +IFR3byA8Y2FtbGkudGVzdEBleGFtcGxlLmNvbT6JATgEEwECACIFAkz61lcCGwMG +CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEIUeCLJL7Fq1c44IAKOJjymoinXd +9NOW7GfpHCmynzSflJJoRcRzsNz83lJbwITYCd1ExQxkO84sMKRPJiefc9epP/Hg +8V4b1SwkGi+A8WaoH/OZtEM8HA7iEKmV+wjfZE6kt+y0trbxdu42W5hLz/uerrNl +G+r90mBNjmJXsZxmwaZEFrLtFlqezCzdQSur35QLZMFvW6aoYFTAgOk1rk9lBtkC +DePaadZQGHNWr+Rw2M5xXv9BZ4Rrjl6VLjE2DuqMSBVkelckBcsmRppaszF3J8y3 +9gd10xC+5/LVfhU8niDZjY3pIcjQwsYJ+Jdyce2OEYo1i6pQDiq2WewXdCJ28DVK +1SX38WFB3Zm5AQ0ETPrWVwEIAMQ/dRCrkhy2D0SzJV5o/Z3uVf1nFLlEFfavV45F +8wtG/Bi5EuZXoYqU+O79O7sPy9Dw3Qhxtvt159l6/sSLXYTBBs3HJ2zTVhI5tbAZ +DMz4/wfkRP/h74KuXnWfin1ynswzqdPVXgrRvTsfHbkwbTaRwbx186VYqM17Wqy2 +hFAUCdQIIW0+X9upjGek+kESldSzeUV87fr3IN/pq6fRc90h8xAKfz6mMc7AAUUL +NLNxb9y18u4Bw+fKgc6W7YxB+gQN1IajmgGPcqUTxNxydWF974iqsKnkZpzHg0Ce +zGGLWzCAGzI8drltgJPBoGGo56U1s2hW6JzLUi03phV10H8AEQEAAYkBHwQYAQIA +CQUCTPrWVwIbDAAKCRCFHgiyS+xatUPIB/9VPOeIxH5UcNYuZT+LW2tdcWPNhyQ+ +u5UC9DC2A3F9AYNYRwDcSVOMmqS8hPJxg/biFxFoGFgm14Vp0nd1blOHcmNXcDzk +XTv2CKcUbgYpvDVmfCcEf6seSf+/RDbyj/VzebE6yvXuwsPus7ntbMw+Dum42z55 +XYiYsfEFu25RtxritG3eYklCKymdRg615pj8zoRpL5Z1NAy5QBb5sv5hPbdGSyqL +Kw6aLcq2IU7kev6CYJVyXzJ1XtsYv/o7hzKKmZ5WcwuPc9Yqh6onJt1RC8jzz8Ry +jyVNPb8AaaWVW1uZLg6Em61aKnbOG10B30m3CQ8dwBjF9hgmtcY0IZ/Y +=OWHA +-----END PGP PUBLIC KEY BLOCK----- +` + +var pubKeyBlob1 = &test.Blob{pubKey1} // user 1 +var pubKeyBlob2 = &test.Blob{pubKey2} // user 2 + +var testFetcher = &test.Fetcher{} + +func init() { + testFetcher.AddBlob(pubKeyBlob1) + testFetcher.AddBlob(pubKeyBlob2) +} + +func TestSigningBadInput(t *testing.T) { + sr := newRequest(1) + + sr.UnsignedJSON = "" + _, err := sr.Sign() + ExpectErrorContains(t, err, "json parse error", "empty input") + + sr.UnsignedJSON = "{}" + _, err = sr.Sign() + ExpectErrorContains(t, err, "json lacks \"camliSigner\" key", "just braces") + + sr.UnsignedJSON = `{"camliSigner": 123}` + _, err = sr.Sign() + ExpectErrorContains(t, err, "\"camliSigner\" key is malformed or unsupported", "camliSigner 123") + + sr.UnsignedJSON = `{"camliSigner": ""}` + _, err = sr.Sign() + ExpectErrorContains(t, err, "\"camliSigner\" key is malformed or unsupported", "empty camliSigner") +} + +func newRequest(userN int) *SignRequest { + if userN < 1 || userN > 2 { + panic("invalid userid") + } + suffix := ".gpg" + if userN == 2 { + suffix = "2.gpg" + } + return &SignRequest{ + UnsignedJSON: "", + Fetcher: testFetcher, + ServerMode: true, + SecretKeyringPath: "./testdata/test-secring" + suffix, + } +} + +func TestSigning(t *testing.T) { + sr := newRequest(1) + sr.UnsignedJSON = fmt.Sprintf(`{"camliVersion": 1, "foo": "fooVal", "camliSigner": %q }`, pubKeyBlob1.BlobRef().String()) + signed, err := sr.Sign() + AssertNil(t, err, "no error signing") + Assert(t, strings.Contains(signed, `"camliSig":`), "got a camliSig") + + vr := NewVerificationRequest(signed, testFetcher) + if !vr.Verify() { + t.Fatalf("verification failed on signed json [%s]: %v", signed, vr.Err) + } + ExpectString(t, "fooVal", vr.PayloadMap["foo"].(string), "PayloadMap") + ExpectString(t, "2931A67C26F5ABDA", vr.SignerKeyId, "SignerKeyId") + + // Test a non-matching signature. + fakeSigned := strings.Replace(signed, pubKeyBlob1.BlobRef().String(), pubKeyBlob2.BlobRef().String(), 1) + vr = NewVerificationRequest(fakeSigned, testFetcher) + if vr.Verify() { + t.Fatalf("unexpected verification of faked signature") + } + AssertErrorContains(t, vr.Err, "openpgp: invalid signature: hash tag doesn't match", + "expected signature verification error") + + t.Logf("TODO: verify GPG-vs-Go sign & verify interop both ways, once implemented.") +} + +func TestEntityFromSecring(t *testing.T) { + ent, err := EntityFromSecring("26F5ABDA", "testdata/test-secring.gpg") + if err != nil { + t.Fatalf("EntityFromSecring: %v", err) + } + if ent == nil { + t.Fatalf("nil entity") + } + if _, ok := ent.Identities["Camli Tester "]; !ok { + t.Errorf("missing expected identity") + } +} + +func TestWriteKeyRing(t *testing.T) { + ent, err := EntityFromSecring("26F5ABDA", "testdata/test-secring.gpg") + if err != nil { + t.Fatalf("NewEntity: %v", err) + } + var buf bytes.Buffer + err = WriteKeyRing(&buf, openpgp.EntityList([]*openpgp.Entity{ent})) + if err != nil { + t.Fatalf("WriteKeyRing: %v", err) + } + + el, err := openpgp.ReadKeyRing(&buf) + if err != nil { + t.Fatalf("ReadKeyRing: %v", err) + } + if len(el) != 1 { + t.Fatalf("ReadKeyRing read %d entities; want 1", len(el)) + } + orig := entityString(ent) + got := entityString(el[0]) + if orig != got { + t.Fatalf("original vs. wrote-then-read entities differ:\norig: %s\n got: %s", orig, got) + } +} + +// stupid entity stringier for testing. +func entityString(ent *openpgp.Entity) string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "PublicKey=%s", ent.PrimaryKey.KeyIdShortString()) + var ids []string + for k := range ent.Identities { + ids = append(ids, k) + } + sort.Strings(ids) + for _, k := range ids { + fmt.Fprintf(&buf, " id[%q]", k) + } + return buf.String() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/keys.go b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/keys.go new file mode 100644 index 00000000..14a2ae42 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/keys.go @@ -0,0 +1,220 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package jsonsign + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/wkfs" + "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp" + "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp/armor" + "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp/packet" +) + +const publicKeyMaxSize = 256 * 1024 + +// ParseArmoredPublicKey tries to parse an armored public key from r, +// taking care to bound the amount it reads. +// The returned shortKeyId is 8 capital hex digits. +// The returned armoredKey is a copy of the contents read. +func ParseArmoredPublicKey(r io.Reader) (shortKeyId, armoredKey string, err error) { + var buf bytes.Buffer + pk, err := openArmoredPublicKeyFile(ioutil.NopCloser(io.TeeReader(r, &buf))) + if err != nil { + return + } + return publicKeyId(pk), buf.String(), nil +} + +func VerifyPublicKeyFile(file, keyid string) (bool, error) { + f, err := wkfs.Open(file) + if err != nil { + return false, err + } + + key, err := openArmoredPublicKeyFile(f) + if err != nil { + return false, err + } + keyId := publicKeyId(key) + if keyId != strings.ToUpper(keyid) { + return false, fmt.Errorf("Key in file %q has id %q; expected %q", + file, keyId, keyid) + } + return true, nil +} + +// publicKeyId returns the short (8 character) capital hex GPG key ID +// of the provided public key. +func publicKeyId(pubKey *packet.PublicKey) string { + return fmt.Sprintf("%X", pubKey.Fingerprint[len(pubKey.Fingerprint)-4:]) +} + +func openArmoredPublicKeyFile(reader io.ReadCloser) (*packet.PublicKey, error) { + defer reader.Close() + + var lr = io.LimitReader(reader, publicKeyMaxSize) + block, _ := armor.Decode(lr) + if block == nil { + return nil, errors.New("Couldn't find PGP block in public key file") + } + if block.Type != "PGP PUBLIC KEY BLOCK" { + return nil, errors.New("Invalid public key blob.") + } + p, err := packet.Read(block.Body) + if err != nil { + return nil, fmt.Errorf("Invalid public key blob: %v", err) + } + + pk, ok := p.(*packet.PublicKey) + if !ok { + return nil, fmt.Errorf("Invalid public key blob; not a public key packet") + } + return pk, nil +} + +// EntityFromSecring returns the openpgp Entity from keyFile that matches keyId. +// If empty, keyFile defaults to osutil.SecretRingFile(). +func EntityFromSecring(keyId, keyFile string) (*openpgp.Entity, error) { + if keyId == "" { + return nil, errors.New("empty keyId passed to EntityFromSecring") + } + keyId = strings.ToUpper(keyId) + if keyFile == "" { + keyFile = osutil.SecretRingFile() + } + secring, err := wkfs.Open(keyFile) + if err != nil { + return nil, fmt.Errorf("jsonsign: failed to open keyring: %v", err) + } + defer secring.Close() + + el, err := openpgp.ReadKeyRing(secring) + if err != nil { + return nil, fmt.Errorf("openpgp.ReadKeyRing of %q: %v", keyFile, err) + } + var entity *openpgp.Entity + for _, e := range el { + pk := e.PrivateKey + if pk == nil || (pk.KeyIdString() != keyId && pk.KeyIdShortString() != keyId) { + continue + } + entity = e + } + if entity == nil { + found := []string{} + for _, e := range el { + pk := e.PrivateKey + if pk == nil { + continue + } + found = append(found, pk.KeyIdShortString()) + } + return nil, fmt.Errorf("didn't find a key in %q for keyId %q; other keyIds in file = %v", keyFile, keyId, found) + } + return entity, nil +} + +var newlineBytes = []byte("\n") + +func ArmoredPublicKey(entity *openpgp.Entity) (string, error) { + var buf bytes.Buffer + wc, err := armor.Encode(&buf, openpgp.PublicKeyType, nil) + if err != nil { + return "", err + } + err = entity.PrivateKey.PublicKey.Serialize(wc) + if err != nil { + return "", err + } + wc.Close() + if !bytes.HasSuffix(buf.Bytes(), newlineBytes) { + buf.WriteString("\n") + } + return buf.String(), nil +} + +// NewEntity returns a new OpenPGP entity. +func NewEntity() (*openpgp.Entity, error) { + name := "" // intentionally empty + comment := "camlistore" + email := "" // intentionally empty + return openpgp.NewEntity(name, comment, email, nil) +} + +func WriteKeyRing(w io.Writer, el openpgp.EntityList) error { + for _, ent := range el { + if err := ent.SerializePrivate(w, nil); err != nil { + return err + } + } + return nil +} + +// KeyIdFromRing returns the public keyId contained in the secret +// ring file secRing. It expects only one keyId in this secret ring +// and returns an error otherwise. +func KeyIdFromRing(secRing string) (keyId string, err error) { + f, err := wkfs.Open(secRing) + if err != nil { + return "", fmt.Errorf("Could not open secret ring file %v: %v", secRing, err) + } + defer f.Close() + el, err := openpgp.ReadKeyRing(f) + if err != nil { + return "", fmt.Errorf("Could not read secret ring file %s: %v", secRing, err) + } + if len(el) != 1 { + return "", fmt.Errorf("Secret ring file %v contained %d identities; expected 1", secRing, len(el)) + } + ent := el[0] + return ent.PrimaryKey.KeyIdShortString(), nil +} + +// GenerateNewSecRing creates a new secret ring file secRing, with +// a new GPG identity. It returns the public keyId of that identity. +// It returns an error if the file already exists. +func GenerateNewSecRing(secRing string) (keyId string, err error) { + ent, err := NewEntity() + if err != nil { + return "", fmt.Errorf("generating new identity: %v", err) + } + if err := os.MkdirAll(filepath.Dir(secRing), 0700); err != nil { + return "", err + } + f, err := wkfs.OpenFile(secRing, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return "", err + } + err = WriteKeyRing(f, openpgp.EntityList([]*openpgp.Entity{ent})) + if err != nil { + f.Close() + return "", fmt.Errorf("Could not write new key ring to %s: %v", secRing, err) + } + if err := f.Close(); err != nil { + return "", fmt.Errorf("Could not close %v: %v", secRing, err) + } + return ent.PrimaryKey.KeyIdShortString(), nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/sign.go b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/sign.go new file mode 100644 index 00000000..79a0908b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/sign.go @@ -0,0 +1,219 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package jsonsign + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "strings" + "sync" + "time" + "unicode" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/wkfs" + "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp" + "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp/packet" +) + +type EntityFetcher interface { + FetchEntity(keyId string) (*openpgp.Entity, error) +} + +type FileEntityFetcher struct { + File string +} + +func FlagEntityFetcher() *FileEntityFetcher { + return &FileEntityFetcher{File: osutil.SecretRingFile()} +} + +type CachingEntityFetcher struct { + Fetcher EntityFetcher + + lk sync.Mutex + m map[string]*openpgp.Entity +} + +func (ce *CachingEntityFetcher) FetchEntity(keyId string) (*openpgp.Entity, error) { + ce.lk.Lock() + if ce.m != nil { + e := ce.m[keyId] + if e != nil { + ce.lk.Unlock() + return e, nil + } + } + ce.lk.Unlock() + + e, err := ce.Fetcher.FetchEntity(keyId) + if err == nil { + ce.lk.Lock() + defer ce.lk.Unlock() + if ce.m == nil { + ce.m = make(map[string]*openpgp.Entity) + } + ce.m[keyId] = e + } + + return e, err +} + +func (fe *FileEntityFetcher) FetchEntity(keyId string) (*openpgp.Entity, error) { + f, err := wkfs.Open(fe.File) + if err != nil { + return nil, fmt.Errorf("jsonsign: FetchEntity: %v", err) + } + defer f.Close() + el, err := openpgp.ReadKeyRing(f) + if err != nil { + return nil, fmt.Errorf("jsonsign: openpgp.ReadKeyRing of %q: %v", fe.File, err) + } + for _, e := range el { + pubk := &e.PrivateKey.PublicKey + if pubk.KeyIdString() != keyId { + continue + } + if e.PrivateKey.Encrypted { + if err := fe.decryptEntity(e); err == nil { + return e, nil + } else { + return nil, err + } + } + return e, nil + } + return nil, fmt.Errorf("jsonsign: entity for keyid %q not found in %q", keyId, fe.File) +} + +type SignRequest struct { + UnsignedJSON string + Fetcher blob.Fetcher + ServerMode bool // if true, can't use pinentry or gpg-agent, etc. + + // Optional signature time. If zero, time.Now() is used. + SignatureTime time.Time + + // Optional function to return an entity (including decrypting + // the PrivateKey, if necessary) + EntityFetcher EntityFetcher + + // SecretKeyringPath is only used if EntityFetcher is nil, + // in which case SecretKeyringPath is used if non-empty. + // As a final resort, we default to osutil.SecretRingFile(). + SecretKeyringPath string +} + +func (sr *SignRequest) secretRingPath() string { + if sr.SecretKeyringPath != "" { + return sr.SecretKeyringPath + } + return osutil.SecretRingFile() +} + +func (sr *SignRequest) Sign() (signedJSON string, err error) { + trimmedJSON := strings.TrimRightFunc(sr.UnsignedJSON, unicode.IsSpace) + + // TODO: make sure these return different things + inputfail := func(msg string) (string, error) { + return "", errors.New(msg) + } + execfail := func(msg string) (string, error) { + return "", errors.New(msg) + } + + jmap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(trimmedJSON), &jmap); err != nil { + return inputfail("json parse error") + } + + camliSigner, hasSigner := jmap["camliSigner"] + if !hasSigner { + return inputfail("json lacks \"camliSigner\" key with public key blobref") + } + + camliSignerStr, _ := camliSigner.(string) + signerBlob, ok := blob.Parse(camliSignerStr) + if !ok { + return inputfail("json \"camliSigner\" key is malformed or unsupported") + } + + pubkeyReader, _, err := sr.Fetcher.Fetch(signerBlob) + if err != nil { + // TODO: not really either an inputfail or an execfail.. but going + // with exec for now. + return execfail(fmt.Sprintf("failed to find public key %s: %v", signerBlob.String(), err)) + } + + pubk, err := openArmoredPublicKeyFile(pubkeyReader) + pubkeyReader.Close() + if err != nil { + return execfail(fmt.Sprintf("failed to parse public key from blobref %s: %v", signerBlob.String(), err)) + } + + // This check should be redundant if the above JSON parse succeeded, but + // for explicitness... + if len(trimmedJSON) == 0 || trimmedJSON[len(trimmedJSON)-1] != '}' { + return inputfail("json parameter lacks trailing '}'") + } + trimmedJSON = trimmedJSON[0 : len(trimmedJSON)-1] + + // sign it + entityFetcher := sr.EntityFetcher + if entityFetcher == nil { + file := sr.secretRingPath() + if file == "" { + return "", errors.New("jsonsign: no EntityFetcher, and no secret ring file defined.") + } + secring, err := wkfs.Open(sr.secretRingPath()) + if err != nil { + return "", fmt.Errorf("jsonsign: failed to open secret ring file %q: %v", sr.secretRingPath(), err) + } + secring.Close() // just opened to see if it's readable + entityFetcher = &FileEntityFetcher{File: file} + } + signer, err := entityFetcher.FetchEntity(pubk.KeyIdString()) + if err != nil { + return "", err + } + + var buf bytes.Buffer + err = openpgp.ArmoredDetachSign( + &buf, + signer, + strings.NewReader(trimmedJSON), + &packet.Config{Time: func() time.Time { return sr.SignatureTime }}, + ) + if err != nil { + return "", err + } + + output := buf.String() + + index1 := strings.Index(output, "\n\n") + index2 := strings.Index(output, "\n-----") + if index1 == -1 || index2 == -1 { + return execfail("Failed to parse signature from gpg.") + } + inner := output[index1+2 : index2] + signature := strings.Replace(inner, "\n", "", -1) + + return fmt.Sprintf("%s,\"camliSig\":\"%s\"}\n", trimmedJSON, signature), nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/sign_appengine.go b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/sign_appengine.go new file mode 100644 index 00000000..0f185af3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/sign_appengine.go @@ -0,0 +1,29 @@ +// +build appengine + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package jsonsign + +import ( + "errors" + + "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp" +) + +func (fe *FileEntityFetcher) decryptEntity(e *openpgp.Entity) error { + return errors.New("No gpg-agent or on-demand password entry on AppEngine.") +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/sign_normal.go b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/sign_normal.go new file mode 100644 index 00000000..9aaf5504 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/sign_normal.go @@ -0,0 +1,86 @@ +// +build !appengine + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package jsonsign + +import ( + "errors" + "fmt" + "log" + "os" + + "camlistore.org/pkg/misc/gpgagent" + "camlistore.org/pkg/misc/pinentry" + "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp" +) + +func (fe *FileEntityFetcher) decryptEntity(e *openpgp.Entity) error { + // TODO: syscall.Mlock a region and keep pass phrase in it. + pubk := &e.PrivateKey.PublicKey + desc := fmt.Sprintf("Need to unlock GPG key %s to use it for signing.", + pubk.KeyIdShortString()) + + conn, err := gpgagent.NewConn() + switch err { + case gpgagent.ErrNoAgent: + fmt.Fprintf(os.Stderr, "Note: gpg-agent not found; resorting to on-demand password entry.\n") + case nil: + defer conn.Close() + req := &gpgagent.PassphraseRequest{ + CacheKey: "camli:jsonsign:" + pubk.KeyIdShortString(), + Prompt: "Passphrase", + Desc: desc, + } + for tries := 0; tries < 2; tries++ { + pass, err := conn.GetPassphrase(req) + if err == nil { + err = e.PrivateKey.Decrypt([]byte(pass)) + if err == nil { + return nil + } + req.Error = "Passphrase failed to decrypt: " + err.Error() + conn.RemoveFromCache(req.CacheKey) + continue + } + if err == gpgagent.ErrCancel { + return errors.New("jsonsign: failed to decrypt key; action canceled") + } + log.Printf("jsonsign: gpgagent: %v", err) + } + default: + log.Printf("jsonsign: gpgagent: %v", err) + } + + pinReq := &pinentry.Request{Desc: desc, Prompt: "Passphrase"} + for tries := 0; tries < 2; tries++ { + pass, err := pinReq.GetPIN() + if err == nil { + err = e.PrivateKey.Decrypt([]byte(pass)) + if err == nil { + return nil + } + pinReq.Error = "Passphrase failed to decrypt: " + err.Error() + continue + } + if err == pinentry.ErrCancel { + return errors.New("jsonsign: failed to decrypt key; action canceled") + } + log.Printf("jsonsign: pinentry: %v", err) + } + return fmt.Errorf("jsonsign: failed to decrypt key %q", pubk.KeyIdShortString()) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/signhandler/sig.go b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/signhandler/sig.go new file mode 100644 index 00000000..231965c1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/signhandler/sig.go @@ -0,0 +1,289 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package signhandler implements the HTTP interface to signing and verifying +// Camlistore JSON blobs. +package signhandler + +import ( + "fmt" + "log" + "net/http" + "strings" + "sync" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/blobserver/gethandler" + "camlistore.org/pkg/blobserver/memory" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/jsonsign" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/types/camtypes" + + "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp" +) + +const kMaxJSONLength = 1024 * 1024 + +type Handler struct { + // Optional path to non-standard secret gpg keyring file + secretRing string + + pubKey string // armored + pubKeyBlobRef blob.Ref + pubKeyFetcher blob.Fetcher + + pubKeyBlobRefServeSuffix string // "camli/sha1-xxxx" + pubKeyHandler http.Handler + + pubKeyDest blobserver.Storage // Where our public key is published + + pubKeyUploadMu sync.RWMutex + pubKeyUploaded bool + + entity *openpgp.Entity + signer *schema.Signer +} + +func (h *Handler) Signer() *schema.Signer { return h.signer } + +func (h *Handler) secretRingPath() string { + if h.secretRing != "" { + return h.secretRing + } + return osutil.SecretRingFile() +} + +func init() { + blobserver.RegisterHandlerConstructor("jsonsign", newJSONSignFromConfig) +} + +func newJSONSignFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Handler, error) { + var ( + // either a short form ("26F5ABDA") or one the longer forms. + keyId = conf.RequiredString("keyId") + + pubKeyDestPrefix = conf.OptionalString("publicKeyDest", "") + secretRing = conf.OptionalString("secretRing", "") + ) + if err := conf.Validate(); err != nil { + return nil, err + } + + h := &Handler{ + secretRing: secretRing, + } + + var err error + h.entity, err = jsonsign.EntityFromSecring(keyId, h.secretRingPath()) + if err != nil { + return nil, err + } + + h.pubKey, err = jsonsign.ArmoredPublicKey(h.entity) + + ms := &memory.Storage{} + h.pubKeyBlobRef = blob.SHA1FromString(h.pubKey) + if _, err := ms.ReceiveBlob(h.pubKeyBlobRef, strings.NewReader(h.pubKey)); err != nil { + return nil, fmt.Errorf("could not store pub key blob: %v", err) + } + h.pubKeyFetcher = ms + + if pubKeyDestPrefix != "" { + sto, err := ld.GetStorage(pubKeyDestPrefix) + if err != nil { + return nil, err + } + h.pubKeyDest = sto + } + h.pubKeyBlobRefServeSuffix = "camli/" + h.pubKeyBlobRef.String() + h.pubKeyHandler = &gethandler.Handler{ + Fetcher: ms, + } + + h.signer, err = schema.NewSigner(h.pubKeyBlobRef, strings.NewReader(h.pubKey), h.entity) + if err != nil { + return nil, err + } + + return h, nil +} + +func (h *Handler) uploadPublicKey() error { + h.pubKeyUploadMu.RLock() + if h.pubKeyUploaded { + h.pubKeyUploadMu.RUnlock() + return nil + } + h.pubKeyUploadMu.RUnlock() + + sto := h.pubKeyDest + + h.pubKeyUploadMu.Lock() + defer h.pubKeyUploadMu.Unlock() + if h.pubKeyUploaded { + return nil + } + _, err := blobserver.StatBlob(sto, h.pubKeyBlobRef) + if err == nil { + h.pubKeyUploaded = true + return nil + } + _, err = blobserver.Receive(sto, h.pubKeyBlobRef, strings.NewReader(h.pubKey)) + h.pubKeyUploaded = (err == nil) + return err +} + +// Discovery returns the Discovery response for the signing handler. +func (h *Handler) Discovery(base string) *camtypes.SignDiscovery { + sd := &camtypes.SignDiscovery{ + PublicKeyID: h.entity.PrimaryKey.KeyIdString(), + SignHandler: base + "camli/sig/sign", + VerifyHandler: base + "camli/sig/verify", + } + if h.pubKeyBlobRef.Valid() { + sd.PublicKeyBlobRef = h.pubKeyBlobRef + sd.PublicKey = base + h.pubKeyBlobRefServeSuffix + } + return sd +} + +func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + base := httputil.PathBase(req) + subPath := httputil.PathSuffix(req) + switch req.Method { + case "GET", "HEAD": + switch subPath { + case "": + http.Redirect(rw, req, base+"camli/sig/discovery", http.StatusFound) + return + case h.pubKeyBlobRefServeSuffix: + h.pubKeyHandler.ServeHTTP(rw, req) + return + case "camli/sig/sign": + fallthrough + case "camli/sig/verify": + http.Error(rw, "POST required", 400) + return + case "camli/sig/discovery": + httputil.ReturnJSON(rw, h.Discovery(base)) + return + } + case "POST": + switch subPath { + case "camli/sig/sign": + h.handleSign(rw, req) + return + case "camli/sig/verify": + h.handleVerify(rw, req) + return + } + } + http.Error(rw, "Unsupported path or method.", http.StatusBadRequest) +} + +func (h *Handler) handleVerify(rw http.ResponseWriter, req *http.Request) { + req.ParseForm() + sjson := req.FormValue("sjson") + if sjson == "" { + http.Error(rw, "missing \"sjson\" parameter", http.StatusBadRequest) + return + } + + // TODO: use a different fetcher here that checks memory, disk, + // the internet, etc. + fetcher := h.pubKeyFetcher + + var res camtypes.VerifyResponse + vreq := jsonsign.NewVerificationRequest(sjson, fetcher) + if vreq.Verify() { + res.SignatureValid = true + res.SignerKeyId = vreq.SignerKeyId + res.VerifiedData = vreq.PayloadMap + } else { + res.SignatureValid = false + res.ErrorMessage = vreq.Err.Error() + } + + rw.WriteHeader(http.StatusOK) // no HTTP response code fun, error info in JSON + httputil.ReturnJSON(rw, &res) +} + +func (h *Handler) handleSign(rw http.ResponseWriter, req *http.Request) { + req.ParseForm() + + badReq := func(s string) { + http.Error(rw, s, http.StatusBadRequest) + log.Printf("bad request: %s", s) + return + } + + jsonStr := req.FormValue("json") + if jsonStr == "" { + badReq("missing \"json\" parameter") + return + } + if len(jsonStr) > kMaxJSONLength { + badReq("parameter \"json\" too large") + return + } + + sreq := &jsonsign.SignRequest{ + UnsignedJSON: jsonStr, + Fetcher: h.pubKeyFetcher, + ServerMode: true, + SecretKeyringPath: h.secretRing, + } + signedJSON, err := sreq.Sign() + if err != nil { + // TODO: some aren't really a "bad request" + badReq(fmt.Sprintf("%v", err)) + return + } + if err := h.uploadPublicKey(); err != nil { + log.Printf("signing handler failed to upload public key: %v", err) + } + rw.Write([]byte(signedJSON)) +} + +func (h *Handler) Sign(bb *schema.Builder) (string, error) { + bb.SetSigner(h.pubKeyBlobRef) + unsigned, err := bb.JSON() + if err != nil { + return "", err + } + sreq := &jsonsign.SignRequest{ + UnsignedJSON: unsigned, + Fetcher: h.pubKeyFetcher, + ServerMode: true, + SecretKeyringPath: h.secretRing, + } + claimTime, err := bb.Blob().ClaimDate() + if err != nil { + if !schema.IsMissingField(err) { + return "", err + } + } else { + sreq.SignatureTime = claimTime + } + if err := h.uploadPublicKey(); err != nil { + log.Printf("signing handler failed to upload public key: %v", err) + } + return sreq.Sign() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/password-foo-keyring.gpg b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/password-foo-keyring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..e2a30ae5862f5a21d784a36aadd201ef600f1240 GIT binary patch literal 1187 zcmV;U1YG->0SyFAtH$X82msdORR6KrGG~GbKlMzVm1sEcjtJFZEe(kj*V7TK=K7?P})>c1JmgI ztn90y<`C;=EA!(>RxdtjXTu(UGE>{_Jw5RNICs`bk z?AD$FxK69r)h(G6owh$&zoz0O4e*7Wlqbm$4%8KFHH*eZFGm-;T%|N~e(W1;gO_<} z&(Ppip^{~QX9=jkU_hIh#KisBd7pM1(ui&sHQR@H6@WWi80H7o?&7J=8x`DtsnPLK z_hiq2_(1-dw99|ItHS^h0RREC98_gP~0viJc3ke7Z0|EvW2m%QT3j`Jd0|5da0Rk6*0162Z2>!w<$HU=v zZl(wTu*X0_1K6yt7UL|TZy~x)>GfVd4jP6;_{WV(M_WKM_k&-Xx>S8)TX|KFg(gG zjo=O}L0JBGb&=ZTXAXbB@00OFP`4j{l*~kFAc%`Cs@Zjq-FHj1&yXk3-qCsEFKvro zD{#eJ1qm4n&$_&V@_Ruej-&->=uQ!6=>E$spI7PkO{(E13;@BF%9!>w%bO#1D#s3V zumS)Bxd9CXO{>P~0SExW0)7kbRLzPZ3r7}IKu2Hc5e=8@%p*gQNkrD}S?R2Cgn9vG znv5Jw98PWZXG4}bB+b|xXO0~}yne>aO{5O|r8_5|q?+HRway)qVQ}jry?d zqgrZabfA0-pt#Qf5di=Ji2)x37y$wR2?YX8tH$X98w>yn2@nYW!YaqZ;dcIR2ms7P zk*K{-OB*^^uokU^%aLX`O6rbP6%6a3qz>_FDnf2AzX>^Tfs$7zi}sW|8U&7GKO=^d zzwLxup2~G)m0pnBB2=uJCIt({nZ98yuN6Hdfc^wUuPtT;PZ);bdabq*OPaiG{8bRp z%TRWB@G6A{;RDQx7rRH@dc~R#Df<~e(Wh#$>YW?i%KVO&G;x(s=0sP3=J1M=`b8kd z6Th*%iiYE}S4gHwDNtZ#IfVMUrBrv|tX+EU*J`?7cOm?`Gt`UgeY_m$9TW27`|NU@x!&00Rbo B9zXy9 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/password-foo-secring.gpg b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/password-foo-secring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..233cbba45e1964a0398d3ffc9fa82670e055b63b GIT binary patch literal 2565 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 KC@2`P0ssRNTnzyL literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/test-keyring2.gpg b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/test-keyring2.gpg new file mode 100644 index 0000000000000000000000000000000000000000..ded7d5151dfbfb7a3c78487e9b4a5173fac49fed GIT binary patch literal 1202 zcmV;j1Wo&y0SyF9`qozg2msMS7aR1l5OrP4g-&1Xz@s_KW-h}1%ZWHtsk5HT7C9J= zJVDUcaIqA)(G1;=L51Sr)T*IMhcwF(O+r&uZ33S;)KR^4iM7j7g}=?S&Gpr+@cBql zl#+2RUB=OhDo>Stdhc^m!Rnf;jcI^@Oc2JvR!)VsAXI;N$~7(}RP%Rpq@^{V{G0Hc zy}~TqhRR*{7o00)^d@qPM1o|Lw7@A3mni?7O9kdi7cBdFal4oJrYeQ*fM18%=oAev zbY=|%Wn71F-zNcFPGaPcVOc5Io`q@Ly;c*}JXDaI+|aH#q7cWjX$IL=X5O+w!Yzg& zU_WwF;tU8_Gpo}hdKmx_0RRECD??#zY-u1=Wpi|Ob7gWMRCjM6JY!*PY-uiZWpi{u zWq4t2aBO8RV{dIfi2*nS69EDMA_W3W`qozh8v_Ol2?z%R0tOWb0tpHW1Qr4V0RkQY z0vCV)3JDN}9tg5a>{_*RjtBswiH|9$ign%e)0XUK=^QDtpERG8l4wQ5bFkd}-cnn@ zgxCq)M8ynbJI*XHq)#R%pL5r#Kk?x4UK`acBpNS(@n)zW^O>|mJRA<<5UG{>2;XE* zq_^y}wzlzh?lxPPOV9hBuCrwu>iyDSO^#w$v7BbXrbHI9?G{>|%q-nOE34m>3uM7> zTc)UBRKS4gHLg!(2H64)go#Dz;hd z7jz2b0SB^#V3r74Hxf14Ap^B9F?)^Kv56jT--3W2E`*r8pdj78q#FbKN2H5DI@jrZLXI0YwWmvvF_S zwes!(!{^F@&X(MSJ(0>3C0RRDs0Urby0RjLC1p-X^)>i@>3;+rV z5QQEHvP+oIwgYi}0fSga0NhDa zjGCmpgz|BN_Tm>2Xc$-~*M(`)cXe)4hjL?Aa6IH)JN5{t6mAA7yftQgCj@`19!dYd zL^kq|^>cZ#I?DC#!o%*fx$SJsJ`U-)+dg?+h?ueQ1-ouhw;JNKZQf!@LMthqMh>;+ znEcL!X)l&_Gz_^w7WuONVLi7-ODc;i4w@~>wjoaBdj5i7m2zJ)bza*Tzxq3eGK!g= zR&xuFbJi+{swXDhQ47fP&%|<%B~3lQ0BNO_TU(hf4uqSnS}JzV8(jh4Nw*0P9l#jH Q_82C$#xxPx# literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/test-secring.gpg b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/test-secring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..bca3ad0392b59249597bc1ec8a93a5d9f6141758 GIT binary patch literal 2498 zcmV;z2|f0e1DFI%;GtUq2mrt5ri3TZO(11`6;u9OH>XSqAlGR;>58Sml|TpeaCFIX zUmN0VB=tp9U!9C>a?N-wx7;ydcg)M2!(OH?QbGkH?`@qUpNM|{%gW_zfOb4KV>0Yo z&|Hma_my6)Z-hzDm@N76B%w zQ`ToK8lyxZCx2Ok%!3KmT?-Y9e0a}SSi5UO_T#m7Kp%fyv(`+RK!8xK>`Gx2QHRnB z0jz`KND|WrWl*@mQ{NhRMvTu_B)A?r4SnSSil&({3;h{=aN;x)dT0KFj(vez<{bbQ zcUgB<HCtsTo?UIY?o8P)-l;skFtF!kjTIu zn*|i?O6N>5MhWts?3DK0aq;yP4xx(}bknMyLN}9E{y6VrE$S>x$U`(IlR`Q!Zh_8d zdqWb*g1x_~!v*!hqfApW)dK_o$D|i;6pxSHPaTCh_1>e$&42Ft;x3fGz^UJ+)FqgF zVvaE3{-+kg3V5Wc?oHE9$jFWXJ_5>)1Vb61)g&E<3>I z!Yrv66{d;_vH*Oy?|DyzC*@D%tzN0!w*1QQDM;7wI%Z5=O+6&}i&}$A1OWCFdJG6# z$j+G^Lm|%&+PNI~Ps@w$?M+ICg2DI12q+rJ1yWNqZBO(@?1wN;%E%(~TSOE?q8;fb zOXpMl(&T#{tC$$@U=YuRJXQ}`yX_m(el-g#b~*I1@Polf@xN5~s}Wg5X~{DwyW6Z1 z$kduP>Np`U`TcP~9`77M0ABv}m##ixgY!5u^@JZP@)o`pL}_6(^=N8jQlz@dL;t0q?d*NLSeKeLt$-f zX&_W(b97~LAUtDXZER^RbY*jNKxKGgZE$R5E@N+PK8XQ11QP)Q03rnfOyHqg0viJc z3ke7Z0|EvW2m%QT3j`Jd0|5da0Rk6*0162ZDKVyeCiSb@98CxSpXo7}B2gK4k>~3a zz!mN2v|KmSdPYvbG+bq5)kE+-?wD!J?=A1ji(#=|9_qK7BVn#pPV{W4`(=I8ip_TC zXdJj{dI$MT*M}Nl1{@q*qJs}Ioe9W*=TIDN2+^^DXWJ@~14hIEjzFC5#B?9yo`X3= zf1+6%F(Fr)Av!7(uq{KFlW0Q3bL6M2?!xE;I#3C)uj4EJ){my2I2QJ!$y}^lNyjv| zW)r>3=_j()@_IEm{;#Eqfz%IA&o{bUP-Ss*lqs}IlSjlMU+$$%>4#Uv?+J2705}79 zkH9dv1_fpvi8JG5zH-q=umS)8odcKzOyHqg0SExQ zhs2^jp&|Zx;}TvRTuDXehMpWz?5{aq=rr~mn!wzYT^R2#4Xq>M5t<$L^pg-XD()9| z1aF^sAZm^Tc&gc`UJHqZXD240!IPMPGV6e*A5I7_5G(+UB=~5>4HUF9%9fG1U&#wA zTqqsh_3G(Qmtu>JI34J&BnW8!30_2!ey$*x{@jDxU^B3jS#;`4BAWyNV~0T0CC*Ox zht;)eN1(I2HBWzw2h9X{89@>UJ+SEY)X33Lqa9_ ze_%?Yq}5{Z_ol0U&peAXP&3989No-?exp3e2Xk~svivd@w8{gikdjowdfeXt5di=J z00;grg6dMB!+JGZ^eD$CFiLPIgmUe;3?;Ym(F6eaQaZ+%N9tQpQy^7=SZ?Q@p?@sQIyp*n zY6qYHzEg#$s_x6AVUUoMu!M0<@+t^|Sr>&|Wz>AJX^OhQzKYKpoU*a+*@+q&nlX<) z4m8XS>`|82cy6ROTzDRr8u3mt2aTGhS%}MaIbUM7AF=?9(X=b!Ykd8>ChL0#0AH^I z|7Pkc^0R^%;X`5*?ONdj;Q`1Cs72zRr**w)b95Y{}}by{B#sDD0b1#{~H}F zp9BAc$87!>4ho|wioapc!ld;DPe#PZze+TT0Urby0RjLC1p-Xqp<4nQ3;+rV5GgUH zd?xj)+F`&50Hkt{p8@nr`U$D5U8e9&0jF2%z)|#rgh^C&&;>x-gR_}!Yo^RT0k3Z? zi>1R}fWITMr6+4CuLHi_NeT62eLbd}ZGsAbH z$xj73S$We>NlLpMn0BP9{TP7a)nA8!&*~B=1a?1P+cc7Brd?#sh3bK$ zCo&?RBk%TFR9Ys@N;(3jBA#*{-6~^8pUdtv1&{OglOxGX>-}sg@TI`74GjYf>G0Jh z<@0ey4_8?Qi=U`g^CwZ;MfbOCrij#*%+w-HKBVpYrZ@mXp(bpj55OlB62_vABpBeN MsK_WN7_b5W05^l1$p8QV literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/test-secring2.gpg b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/testdata/test-secring2.gpg new file mode 100644 index 0000000000000000000000000000000000000000..f4b7ed22286696c1f8ee02530e882fc2252bbdaf GIT binary patch literal 2504 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 S7{&G&Cbh;iA)nZ=0ssKko~)Aq literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/jsonsign/verify.go b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/verify.go new file mode 100644 index 00000000..460f33bb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/jsonsign/verify.go @@ -0,0 +1,239 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package jsonsign + +import ( + "bytes" + "crypto" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "strings" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/camerrors" + "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp/armor" + "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp/packet" +) + +const sigSeparator = `,"camliSig":"` + +// reArmor takes a camliSig (single line armor) and turns it back into an PGP-style +// multi-line armored string +func reArmor(line string) string { + lastEq := strings.LastIndex(line, "=") + if lastEq == -1 { + return "" + } + buf := new(bytes.Buffer) + fmt.Fprintf(buf, "-----BEGIN PGP SIGNATURE-----\n\n") + payload := line[0:lastEq] + crc := line[lastEq:] + for len(payload) > 0 { + chunkLen := len(payload) + if chunkLen > 60 { + chunkLen = 60 + } + fmt.Fprintf(buf, "%s\n", payload[0:chunkLen]) + payload = payload[chunkLen:] + } + fmt.Fprintf(buf, "%s\n-----BEGIN PGP SIGNATURE-----\n", crc) + return buf.String() +} + +// See doc/json-signing/* for background and details +// on these variable names. +type VerifyRequest struct { + fetcher blob.Fetcher // fetcher used to find public key blob + + ba []byte // "bytes all" + bp []byte // "bytes payload" (the part that is signed) + bpj []byte // "bytes payload, JSON" (BP + "}") + bs []byte // "bytes signature", "{" + separator + camliSig, valid JSON + + CamliSigner blob.Ref + CamliSig string + PublicKeyPacket *packet.PublicKey + + // set if Verify() returns true: + PayloadMap map[string]interface{} // The JSON values from BPJ + SignerKeyId string // e.g. "2931A67C26F5ABDA" + + Err error // last error encountered +} + +func (vr *VerifyRequest) fail(msg string) bool { + vr.Err = errors.New("jsonsign: " + msg) + return false +} + +func (vr *VerifyRequest) ParseSigMap() bool { + sigMap := make(map[string]interface{}) + if err := json.Unmarshal(vr.bs, &sigMap); err != nil { + return vr.fail("invalid JSON in signature") + } + + if len(sigMap) != 1 { + return vr.fail("signature JSON didn't have exactly 1 key") + } + + sigVal, hasCamliSig := sigMap["camliSig"] + if !hasCamliSig { + return vr.fail("no 'camliSig' key in signature") + } + + var ok bool + vr.CamliSig, ok = sigVal.(string) + if !ok { + return vr.fail("camliSig not a string") + } + + return true +} + +func (vr *VerifyRequest) ParsePayloadMap() bool { + vr.PayloadMap = make(map[string]interface{}) + pm := vr.PayloadMap + + if err := json.Unmarshal(vr.bpj, &pm); err != nil { + return vr.fail("parse error; payload JSON is invalid") + } + + if _, hasVersion := pm["camliVersion"]; !hasVersion { + return vr.fail("missing 'camliVersion' in the JSON payload") + } + + signer, hasSigner := pm["camliSigner"] + if !hasSigner { + return vr.fail("missing 'camliSigner' in the JSON payload") + } + + if _, ok := signer.(string); !ok { + return vr.fail("invalid 'camliSigner' in the JSON payload") + } + + var ok bool + vr.CamliSigner, ok = blob.Parse(signer.(string)) + if !ok { + return vr.fail("malformed 'camliSigner' blobref in the JSON payload") + } + return true +} + +func (vr *VerifyRequest) FindAndParsePublicKeyBlob() bool { + reader, _, err := vr.fetcher.Fetch(vr.CamliSigner) + if err == os.ErrNotExist { + vr.Err = camerrors.ErrMissingKeyBlob + return false + } + if err != nil { + log.Printf("error fetching public key blob %v: %v", vr.CamliSigner, err) + vr.Err = err + return false + } + defer reader.Close() + pk, err := openArmoredPublicKeyFile(reader) + if err != nil { + return vr.fail(fmt.Sprintf("error opening public key file: %v", err)) + } + vr.PublicKeyPacket = pk + return true +} + +func (vr *VerifyRequest) VerifySignature() bool { + armorData := reArmor(vr.CamliSig) + block, _ := armor.Decode(bytes.NewBufferString(armorData)) + if block == nil { + return vr.fail("can't parse camliSig armor") + } + var p packet.Packet + var err error + p, err = packet.Read(block.Body) + if err != nil { + return vr.fail("error reading PGP packet from camliSig: " + err.Error()) + } + sig, ok := p.(*packet.Signature) + if !ok { + return vr.fail("PGP packet isn't a signature packet") + } + if sig.Hash != crypto.SHA1 && sig.Hash != crypto.SHA256 { + return vr.fail("I can only verify SHA1 or SHA256 signatures") + } + if sig.SigType != packet.SigTypeBinary { + return vr.fail("I can only verify binary signatures") + } + hash := sig.Hash.New() + hash.Write(vr.bp) // payload bytes + err = vr.PublicKeyPacket.VerifySignature(hash, sig) + if err != nil { + return vr.fail(fmt.Sprintf("bad signature: %s", err)) + } + vr.SignerKeyId = vr.PublicKeyPacket.KeyIdString() + return true +} + +func NewVerificationRequest(sjson string, fetcher blob.Fetcher) (vr *VerifyRequest) { + if fetcher == nil { + panic("NewVerificationRequest fetcher is nil") + } + vr = new(VerifyRequest) + vr.ba = []byte(sjson) + vr.fetcher = fetcher + + sigIndex := bytes.LastIndex(vr.ba, []byte(sigSeparator)) + if sigIndex == -1 { + vr.Err = errors.New("jsonsign: no 13-byte camliSig separator found in sjson") + return + } + + // "Bytes Payload" + vr.bp = vr.ba[:sigIndex] + + // "Bytes Payload JSON". Note we re-use the memory (the ",") + // from BA in BPJ, so we can't re-use that "," byte for + // the opening "{" in "BS". + vr.bpj = vr.ba[:sigIndex+1] + vr.bpj[sigIndex] = '}' + vr.bs = []byte("{" + sjson[sigIndex+1:]) + return +} + +// TODO: turn this into (bool, os.Error) return, probably, or *Details, os.Error. +func (vr *VerifyRequest) Verify() bool { + if vr.Err != nil { + return false + } + + if vr.ParseSigMap() && + vr.ParsePayloadMap() && + vr.FindAndParsePublicKeyBlob() && + vr.VerifySignature() { + return true + } + + // Don't allow dumbs callers to accidentally check this + // if it's not valid. + vr.PayloadMap = nil + if vr.Err == nil { + // The other functions should have filled this in + // already, but just in case: + vr.Err = errors.New("jsonsign: verification failed") + } + return false +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/kvutil/kvutil.go b/vendor/github.com/camlistore/camlistore/pkg/kvutil/kvutil.go new file mode 100644 index 00000000..0fd6c781 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/kvutil/kvutil.go @@ -0,0 +1,64 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package kvutil contains helpers related to +// github.com/cznic/kv. +package kvutil + +import ( + "fmt" + "io" + "os" + "strconv" + + "camlistore.org/third_party/github.com/camlistore/lock" + "camlistore.org/third_party/github.com/cznic/kv" +) + +// Open opens the named kv DB file for reading/writing. It +// creates the file if it does not exist yet. +func Open(dbFile string, opts *kv.Options) (*kv.DB, error) { + createOpen := kv.Open + verb := "opening" + if _, err := os.Stat(dbFile); os.IsNotExist(err) { + createOpen = kv.Create + verb = "creating" + } + if opts == nil { + opts = &kv.Options{} + } + if opts.Locker == nil { + opts.Locker = func(dbFile string) (io.Closer, error) { + lkfile := dbFile + ".lock" + cl, err := lock.Lock(lkfile) + if err != nil { + return nil, fmt.Errorf("failed to acquire lock on %s: %v", lkfile, err) + } + return cl, nil + } + } + if v, _ := strconv.ParseBool(os.Getenv("CAMLI_KV_VERIFY")); v { + opts.VerifyDbBeforeOpen = true + opts.VerifyDbAfterOpen = true + opts.VerifyDbBeforeClose = true + opts.VerifyDbAfterClose = true + } + db, err := createOpen(dbFile, opts) + if err != nil { + return nil, fmt.Errorf("error %s %s: %v", verb, dbFile, err) + } + return db, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/leak/leak.go b/vendor/github.com/camlistore/camlistore/pkg/leak/leak.go new file mode 100644 index 00000000..5bc2696e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/leak/leak.go @@ -0,0 +1,74 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package leak + +import ( + "bytes" + "fmt" + "log" + "runtime" +) + +// A Checker checks for leaks. +type Checker struct { + pc []uintptr // nil once closed +} + +// NewChecker returns a Checker, remembering the stack trace. +func NewChecker() *Checker { + pc := make([]uintptr, 50) + ch := &Checker{pc[:runtime.Callers(0, pc)]} + runtime.SetFinalizer(ch, (*Checker).finalize) + return ch +} + +func (c *Checker) Close() { + if c != nil { + c.pc = nil + } +} + +func (c *Checker) finalize() { + if testHookFinalize != nil { + defer testHookFinalize() + } + if c == nil || c.pc == nil { + return + } + var buf bytes.Buffer + buf.WriteString("Leak at:\n") + for _, pc := range c.pc { + f := runtime.FuncForPC(pc) + if f == nil { + break + } + file, line := f.FileLine(f.Entry()) + fmt.Fprintf(&buf, " %s:%d\n", file, line) + } + onLeak(c, buf.String()) +} + +// testHookFinalize optionally specifies a function to run after +// finalization. For tests. +var testHookFinalize func() + +// onLeak is changed by tests. +var onLeak = logLeak + +func logLeak(c *Checker, stack string) { + log.Println(stack) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/leak/leak_test.go b/vendor/github.com/camlistore/camlistore/pkg/leak/leak_test.go new file mode 100644 index 00000000..84cb8609 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/leak/leak_test.go @@ -0,0 +1,74 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package leak + +import ( + "runtime" + "strings" + "sync" + "testing" + "time" +) + +func TestLeak(t *testing.T) { + testLeak(t, true, 1) +} + +func TestNoLeak(t *testing.T) { + testLeak(t, false, 0) +} + +func testLeak(t *testing.T, leak bool, want int) { + defer func() { + testHookFinalize = nil + onLeak = logLeak + }() + var mu sync.Mutex // guards leaks + var leaks []string + onLeak = func(_ *Checker, stack string) { + mu.Lock() + defer mu.Unlock() + leaks = append(leaks, stack) + } + finalizec := make(chan bool) + testHookFinalize = func() { + finalizec <- true + } + + c := make(chan bool) + go func() { + ch := NewChecker() + if !leak { + ch.Close() + } + c <- true + }() + <-c + go runtime.GC() + select { + case <-time.After(5 * time.Second): + t.Error("timeout waiting for finalization") + case <-finalizec: + } + mu.Lock() // no need to unlock + if len(leaks) != want { + t.Errorf("got %d leaks; want %d", len(leaks), want) + } + if len(leaks) == 1 && !strings.Contains(leaks[0], "leak_test.go") { + t.Errorf("Leak stack doesn't contain leak_test.go: %s", leaks[0]) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/legal/legal.go b/vendor/github.com/camlistore/camlistore/pkg/legal/legal.go new file mode 100644 index 00000000..a75283fd --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/legal/legal.go @@ -0,0 +1,50 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package legal provides project-wide storage for compiled-in licenses. +package legal + +var licenses []string + +func init() { + RegisterLicense(` +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +`) +} + +// RegisterLicense stores the license text. +// It doesn't check whether the text was already present. +func RegisterLicense(text string) { + licenses = append(licenses, text) + return +} + +// Licenses returns a slice of the licenses. +func Licenses() []string { + return licenses +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/legal/legal_test.go b/vendor/github.com/camlistore/camlistore/pkg/legal/legal_test.go new file mode 100644 index 00000000..de30bac7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/legal/legal_test.go @@ -0,0 +1,39 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package legal + +import ( + "testing" +) + +func TestRegisterLicense(t *testing.T) { + initial := len(licenses) + RegisterLicense("dummy") + if initial+1 != len(licenses) { + t.Fatal("didn't add a license") + } +} + +func TestLicenses(t *testing.T) { + licenses := Licenses() + if len(licenses) < 2 { + t.Fatal("no second license text") + } + if licenses[1] != "dummy" { + t.Error("license text mismatch") + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/legal/legalprint/legalprint.go b/vendor/github.com/camlistore/camlistore/pkg/legal/legalprint/legalprint.go new file mode 100644 index 00000000..e4a3205a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/legal/legalprint/legalprint.go @@ -0,0 +1,42 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package legalprint provides a printing helper for the legal package. +package legalprint + +import ( + "flag" + "fmt" + "io" + + "camlistore.org/pkg/legal" +) + +var ( + flagLegal = flag.Bool("legal", false, "show licenses") +) + +// MaybePrint will print the licenses if flagLegal has been set. +// It will return the value of the flagLegal. +func MaybePrint(out io.Writer) bool { + if !*flagLegal { + return false + } + for _, text := range legal.Licenses() { + fmt.Fprintln(out, text) + } + return true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/lru/cache.go b/vendor/github.com/camlistore/camlistore/pkg/lru/cache.go new file mode 100644 index 00000000..228b088e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/lru/cache.go @@ -0,0 +1,109 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package lru implements an LRU cache. +package lru + +import ( + "container/list" + "sync" +) + +// Cache is an LRU cache, safe for concurrent access. +type Cache struct { + maxEntries int + + mu sync.Mutex + ll *list.List + cache map[string]*list.Element +} + +// *entry is the type stored in each *list.Element. +type entry struct { + key string + value interface{} +} + +// New returns a new cache with the provided maximum items. +func New(maxEntries int) *Cache { + return &Cache{ + maxEntries: maxEntries, + ll: list.New(), + cache: make(map[string]*list.Element), + } +} + +// Add adds the provided key and value to the cache, evicting +// an old item if necessary. +func (c *Cache) Add(key string, value interface{}) { + c.mu.Lock() + defer c.mu.Unlock() + + // Already in cache? + if ee, ok := c.cache[key]; ok { + c.ll.MoveToFront(ee) + ee.Value.(*entry).value = value + return + } + + // Add to cache if not present + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + + if c.ll.Len() > c.maxEntries { + c.removeOldest() + } +} + +// Get fetches the key's value from the cache. +// The ok result will be true if the item was found. +func (c *Cache) Get(key string) (value interface{}, ok bool) { + c.mu.Lock() + defer c.mu.Unlock() + if ele, hit := c.cache[key]; hit { + c.ll.MoveToFront(ele) + return ele.Value.(*entry).value, true + } + return +} + +// RemoveOldest removes the oldest item in the cache and returns its key and value. +// If the cache is empty, the empty string and nil are returned. +func (c *Cache) RemoveOldest() (key string, value interface{}) { + c.mu.Lock() + defer c.mu.Unlock() + return c.removeOldest() +} + +// note: must hold c.mu +func (c *Cache) removeOldest() (key string, value interface{}) { + ele := c.ll.Back() + if ele == nil { + return + } + c.ll.Remove(ele) + ent := ele.Value.(*entry) + delete(c.cache, ent.key) + return ent.key, ent.value + +} + +// Len returns the number of items in the cache. +func (c *Cache) Len() int { + c.mu.Lock() + defer c.mu.Unlock() + return c.ll.Len() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/lru/cache_test.go b/vendor/github.com/camlistore/camlistore/pkg/lru/cache_test.go new file mode 100644 index 00000000..48473266 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/lru/cache_test.go @@ -0,0 +1,71 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lru + +import ( + "reflect" + "testing" +) + +func TestLRU(t *testing.T) { + c := New(2) + + expectMiss := func(k string) { + v, ok := c.Get(k) + if ok { + t.Fatalf("expected cache miss on key %q but hit value %v", k, v) + } + } + + expectHit := func(k string, ev interface{}) { + v, ok := c.Get(k) + if !ok { + t.Fatalf("expected cache(%q)=%v; but missed", k, ev) + } + if !reflect.DeepEqual(v, ev) { + t.Fatalf("expected cache(%q)=%v; but got %v", k, ev, v) + } + } + + expectMiss("1") + c.Add("1", "one") + expectHit("1", "one") + + c.Add("2", "two") + expectHit("1", "one") + expectHit("2", "two") + + c.Add("3", "three") + expectHit("3", "three") + expectHit("2", "two") + expectMiss("1") +} + +func TestRemoveOldest(t *testing.T) { + c := New(2) + c.Add("1", "one") + c.Add("2", "two") + if k, v := c.RemoveOldest(); k != "1" || v != "one" { + t.Fatalf("oldest = %q, %q; want 1, one", k, v) + } + if k, v := c.RemoveOldest(); k != "2" || v != "two" { + t.Fatalf("oldest = %q, %q; want 2, two", k, v) + } + if k, v := c.RemoveOldest(); k != "" || v != nil { + t.Fatalf("oldest = %v, %v; want \"\", nil", k, v) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/magic.go b/vendor/github.com/camlistore/camlistore/pkg/magic/magic.go new file mode 100644 index 00000000..7495dba9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/magic/magic.go @@ -0,0 +1,118 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +nYou may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package magic implements MIME type sniffing of data based on the +// well-known "magic" number prefixes in the file. +package magic + +import ( + "bytes" + "io" + "net/http" + "strings" +) + +type prefixEntry struct { + prefix []byte + mtype string +} + +// usable source: http://www.garykessler.net/library/file_sigs.html +// mime types: http://www.iana.org/assignments/media-types/media-types.xhtml +var prefixTable = []prefixEntry{ + {[]byte("GIF87a"), "image/gif"}, + {[]byte("GIF89a"), "image/gif"}, // TODO: Others? + {[]byte("\xff\xd8\xff\xe2"), "image/jpeg"}, + {[]byte("\xff\xd8\xff\xe1"), "image/jpeg"}, + {[]byte("\xff\xd8\xff\xe0"), "image/jpeg"}, + {[]byte("\xff\xd8\xff\xdb"), "image/jpeg"}, + {[]byte("\x49\x49\x2a\x00\x10\x00\x00\x00\x43\x52\x02"), "image/cr2"}, + {[]byte{137, 'P', 'N', 'G', '\r', '\n', 26, 10}, "image/png"}, + {[]byte{0x49, 0x20, 0x49}, "image/tiff"}, + {[]byte{0x49, 0x49, 0x2A, 0}, "image/tiff"}, + {[]byte{0x4D, 0x4D, 0, 0x2A}, "image/tiff"}, + {[]byte{0x4D, 0x4D, 0, 0x2B}, "image/tiff"}, + {[]byte("8BPS"), "image/vnd.adobe.photoshop"}, + {[]byte("gimp xcf "), "image/xcf"}, + {[]byte("-----BEGIN PGP PUBLIC KEY BLOCK---"), "text/x-openpgp-public-key"}, + {[]byte("fLaC\x00\x00\x00"), "audio/flac"}, + {[]byte{'I', 'D', '3'}, "audio/mpeg"}, + {[]byte{0, 0, 1, 0xB7}, "video/mpeg"}, + {[]byte{0, 0, 0, 0x14, 0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20}, "video/quicktime"}, + {[]byte{0, 0x6E, 0x1E, 0xF0}, "application/vnd.ms-powerpoint"}, + {[]byte{0x1A, 0x45, 0xDF, 0xA3}, "video/webm"}, + {[]byte("FLV\x01"), "application/vnd.adobe.flash.video"}, + {[]byte{0x1F, 0x8B, 0x08}, "application/gzip"}, + {[]byte{0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C}, "application/x-7z-compressed"}, + {[]byte("BZh"), "application/bzip2"}, + {[]byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0}, "application/x-xz"}, + {[]byte{'P', 'K', 3, 4, 0x0A, 0, 2, 0}, "application/epub+zip"}, + {[]byte{0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1}, "application/vnd.ms-word"}, + {[]byte{'P', 'K', 3, 4, 0x0A, 0x14, 0, 6, 0}, "application/vnd.openxmlformats-officedocument.custom-properties+xml"}, + {[]byte{'P', 'K', 3, 4}, "application/zip"}, + {[]byte("%PDF"), "application/pdf"}, + {[]byte("{rtf"), "text/rtf1"}, + {[]byte("BEGIN:VCARD\x0D\x0A"), "text/vcard"}, + {[]byte("Return-Path: "), "message/rfc822"}, + + // TODO(bradfitz): popular audio & video formats at least +} + +// MIMEType returns the MIME type from the data in the provided header +// of the data. +// It returns the empty string if the MIME type can't be determined. +func MIMEType(hdr []byte) string { + hlen := len(hdr) + for _, pte := range prefixTable { + plen := len(pte.prefix) + if hlen > plen && bytes.Equal(hdr[:plen], pte.prefix) { + return pte.mtype + } + } + t := http.DetectContentType(hdr) + t = strings.Replace(t, "; charset=utf-8", "", 1) + if t != "application/octet-stream" && t != "text/plain" { + return t + } + return "" +} + +// MIMETypeFromReader takes a reader, sniffs the beginning of it, +// and returns the mime (if sniffed, else "") and a new reader +// that's the concatenation of the bytes sniffed and the remaining +// reader. +func MIMETypeFromReader(r io.Reader) (mime string, reader io.Reader) { + var buf bytes.Buffer + _, err := io.Copy(&buf, io.LimitReader(r, 1024)) + mime = MIMEType(buf.Bytes()) + if err != nil { + return mime, io.MultiReader(&buf, errReader{err}) + } + return mime, io.MultiReader(&buf, r) +} + +// MIMETypeFromReader takes a ReaderAt, sniffs the beginning of it, +// and returns the MIME type if sniffed, else the empty string. +func MIMETypeFromReaderAt(ra io.ReaderAt) (mime string) { + var buf [1024]byte + n, _ := ra.ReadAt(buf[:], 0) + return MIMEType(buf[:n]) +} + +// errReader is an io.Reader which just returns err. +type errReader struct{ err error } + +func (er errReader) Read([]byte) (int, error) { return 0, er.err } diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/magic_test.go b/vendor/github.com/camlistore/camlistore/pkg/magic/magic_test.go new file mode 100644 index 00000000..633b7b5a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/magic/magic_test.go @@ -0,0 +1,95 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +nYou may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package magic + +import ( + "errors" + "io" + "io/ioutil" + "strings" + "testing" +) + +type magicTest struct { + fileName, data string // one of these set + want string +} + +var tests = []magicTest{ + {fileName: "smile.jpg", want: "image/jpeg"}, + {fileName: "smile.png", want: "image/png"}, + {fileName: "smile.psd", want: "image/vnd.adobe.photoshop"}, + {fileName: "smile.tiff", want: "image/tiff"}, + {fileName: "smile.xcf", want: "image/xcf"}, + {fileName: "smile.gif", want: "image/gif"}, + {fileName: "foo.tar.gz", want: "application/gzip"}, + {fileName: "foo.tar.xz", want: "application/x-xz"}, + {fileName: "foo.tbz2", want: "application/bzip2"}, + {fileName: "foo.zip", want: "application/zip"}, + {fileName: "magic.pdf", want: "application/pdf"}, + {data: "foo", want: "text/html"}, + {data: "\xff", want: ""}, +} + +func TestMagic(t *testing.T) { + for i, tt := range tests { + var err error + data := []byte(tt.data) + if tt.fileName != "" { + data, err = ioutil.ReadFile("testdata/" + tt.fileName) + if err != nil { + t.Fatalf("Error reading %s: %v", tt.fileName, + err) + } + } + mime := MIMEType(data) + if mime != tt.want { + t.Errorf("%d. got %q; want %q", i, mime, tt.want) + } + } +} + +func TestMIMETypeFromReader(t *testing.T) { + someErr := errors.New("some error") + const content = "foobar" + mime, r := MIMETypeFromReader(io.MultiReader( + strings.NewReader(content), + &onceErrReader{someErr}, + )) + if want := "text/html"; mime != want { + t.Errorf("mime = %q; want %q", mime, want) + } + slurp, err := ioutil.ReadAll(r) + if string(slurp) != "foobar" { + t.Errorf("read = %q; want %q", slurp, content) + } + if err != someErr { + t.Errorf("read error = %v; want %v", err, someErr) + } +} + +// errReader is an io.Reader which just returns err, once. +type onceErrReader struct{ err error } + +func (er *onceErrReader) Read([]byte) (int, error) { + if er.err != nil { + err := er.err + er.err = nil + return 0, err + } + return 0, io.EOF +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.tar b/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.tar new file mode 100644 index 0000000000000000000000000000000000000000..495006e342199c75919fe42d2519ade95e4ae49b GIT binary patch literal 10240 zcmeIvJr2S!42EIPoFX^CE^cz3RuIHOrBW77Pl`~P%0!S5uebcjPM$9h!&WMnmP)OP zTGCzPqG}ectY0N&t(!(`Vk-AEr6i%P{H{m)>t*W35TqZweLhXMwV8+WaqZ}r_jAd= z@5ZWMm}8FqE`MM0cb)HRYz9$O+ literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.tar.gz b/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..f735f229923a728ed8e41d0b60e79f0809e04199 GIT binary patch literal 163 zcmb2|=3r1wZt-PcetXf8>yUv!>%%p+u6Yl9=Bhl;yx1Yly)n;b@5(9fN)(sq{n;OT zsJZgPCyRHn$w5mM{7g+*F>Mitr?KPk8{Fb M&hDPhpuxZZ07F|%^8f$< literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.tar.xz b/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..bfcceda15ef0dde916e186a3230dfff82c9c9ee9 GIT binary patch literal 208 zcmV;>05AXjH+ooF000E$*0e?f03iVu0001VFXf})C;tGCT>vv1>|OxfG-0AA4ZN_c z-IZM!&Q&e9df}W#tgiFND4G>6i5VA9i>gcsgEZn&`q3B$V`#yyV~EV z8-gkUaTX8IE06_*zMU298@wY+LdM~TC-GVx^hPkaCQ?7o#vChn=a-XRE$+}qn9Cj^ z*2ON#9HF@bwBvpy4%Yh_Hk5eFNqZ8EJwzpqh2S^L1gprO?G8vW;VZx*Yfr>JS!y1vqemF2R z0Po^ri30LUgohk~6NG{h5eS&8s2KWS(X1Xqb$X6!pjK*lW%8SupA8>#V`DLig$oue TSgFuRKa05{oG3_3MgbfEcWpj4 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.zip b/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/foo.zip new file mode 100644 index 0000000000000000000000000000000000000000..9ee5fe500acf789b5090b8fa70778a0146fafe7f GIT binary patch literal 300 zcmWIWW@h1H009|}VmmMcO0Y1xYJLGB9sXYVmyx#HAJ742&#SJ}@w_bOb&3GP3r7$W;v80pb4W0WodISN1H*zwkS{?d Wft`lSFjh8@g-k#=9Z1)MI1B(vTQ5HV literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/magic.pdf b/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/magic.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0e9340c04b35adb19ff57bface3e97f77ae14810 GIT binary patch literal 21668 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.bmp b/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.bmp new file mode 100644 index 0000000000000000000000000000000000000000..ef4ac5d2cffea0de5ae7d28e61d625e12e71d72c GIT binary patch literal 7654 zcmeI0Jxe296o3=IHHuH+^lQ&ffdAG4-e7ofNn(Kk0Yk!Pwn zVUoNmS|%5ocv@SiNF;*KRRvB^YP*TKyu3{6%OZ;5| zY6^OxEZ72biD@nyt*oqM>BNe})z;P~i%<=g!d9CaP1bY#k>(<~EbAWbv(*%ABM5-V z{$N=mnrSXIxo8O%!ATo|BYZNNQ8mRa$%XGUmj`lWVPT=BrexM(I9WBunBO3aIB?(XiAQCvnG=jP_*v>O{6ON-|1Zg8EP zoUk{6#zlc@-gwf64X%%mkJHoB|BQ?gm%htUDVna5?+NuIHcj7w>-k{r&8@ zd`y_1pMQCIv2#&Io1VY!?(TR8H2128hK7TK1O2Zw%x=rOA18ZAGcc5j1{ce4Pfw4$ zA(WMsadc30**EO$?8sxyGVjOl@9*{lj8+Yn+S+m%sF7cc<}hP4=TU+V*mgE literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.gif b/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.gif new file mode 100644 index 0000000000000000000000000000000000000000..7467497921ffdcaa5171f436921e6326f662d891 GIT binary patch literal 927 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.png b/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.png new file mode 100644 index 0000000000000000000000000000000000000000..0de7d14d974407933a1da2b46842c754a8b72f19 GIT binary patch literal 1221 zcmV;$1UmbPP)Px#32;bRa{vGf6951U69E94oEQKA00(qQO+^RW0}}`cEJZVOz5oCK8FWQhbVF}# zZDnqB07G(RVRU6=Aa`kWXdp*PO;A^X4i^9b1TslPK~z}7?O07nGF=p&t7HCU4JH|b z!3-58Z6r-U9TC@mpwzd-56tt0AyJ!^%)+PyS6~RD3ix{JzO+s9upOuT6 z83L)8Z{bJTI6CubzHjjLEXRAC=bq=BJI}d56h-9cL4*8A{L=cR^*^J9s$ZJR<@WaW zj*gD<`Mk|$o0ymg27?4aG=dgTd2VGgnT3UgY&KiE-r;a05($c;s>cd~z_RSg$w@w+ z@9*#T`FzTo5!J!(_X7aR!zqfY9+=H$$H&Jtnt!VW!?4xWRmIn-qQ$bTPN%DFW5Hlh zDi8pZlap6hS9Q^X5V~BhN}Lq4rPu3aN(X@D<>k6)ZEtU*sC;MG?RJJ?6y&IYk&%%q zQ5rQ3`1bZz{=BoZb9Z-FvEF4X5!Uz^S5SS;r8cof5}R;%oDpU+oU zCqf8&dwWYmnwpxD$)w5_URha@Rsa6}zTQqeJw5e$y~PT%*=(^`jN>?!v>qNFNRlk& zgke}Zo&JUcqE4jKX_w2T)9Fkm)9mc*_4T!42>5lUzqA@?Y#tvU4-O6v4-XLpnVOpN z`~5hM|7BjHD8}P)9LE6w0011vpBuSD)L{VH{Uze;<^aURXg+dmKr5NS$ zc#8J52Jk#j)AXkcDjG_(@ArUMPw}2ph_YWm&V?ERipjN`0|` z<>j@xx!F+p+-|qzjWkUcn+7TEi00O literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.psd b/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.psd new file mode 100644 index 0000000000000000000000000000000000000000..6be6eb8a365d3203b8282f4068061a0e3ae1efd8 GIT binary patch literal 5236 zcmeHK%}Z2K6hHTU%#5Q`lOHoS7>#{kwg^$NS(ONhl7e6mwGq*niBeM#gf<3kBEg+l zt3a5g3;%()XlqyyF5Fy6jSBo|o-^Z&GoF5T#s>QXYGLnP-n;kx&N;txKhAykyhul1 zKM5*cI;!Tv?IG1x*Y!w8cdx!(X#1%PwLm$pmCPuJNK=)!7k77wPeDb#0-Iit;0ay5 zTiK3|O^mS8B8n6%*mn#LJ&8_?O^yze(s?>L`WUiW++OOSK^meb6r~9oqe&X2VR+O2 zC>0c-8mgjds>HJfB|zmR4KG-{bRH!L&Jdl0p35E#mKx&0TsRCkf=S)uC+TDuzxzmNfygyhSE&^InqSe zrvpy0kWHC_9=Q)mh=~IB&d8DoT4{#pmO^M6zy)|9ktOOXDhaTZ$8CUB0B4JA4d{w1 zzR&a=bivI7cy+yRGpPDi0-f5bhuFWA=5;ognoRHg&#BVLT)Z{k(=)%d$}XM@2@#rG zWsUENBcj`@EVvh@K6{_dtg7Jivmuw9?UDox29Op7(71jba3 zb1aswWXY`uueA-l{b*-Y=!~}oOe}Ms%<&=XiN!{dB#-V1UcOpiS81jc5xn`#N+fxkD-tVj z1|p3Y&w7Fnt;F7O05}`T*+|YtayAmaYR-w|oJjx8iBw7ri)rLvNgw}(kCO@N3OgyP alcJuw6x9iF=P&G(|Ajdr?u7U$5Ak1cSLFu) literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.tiff b/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.tiff new file mode 100644 index 0000000000000000000000000000000000000000..cb23fc1caacd14427baf379fb68d7a8a69c980d7 GIT binary patch literal 1502 zcmebD)MA*#!objA|Gf$|a$$>kbsAH9ThqdHV&Z|{%4-#)CGNXV%9%6c zh48z)=WFKY0s8PN*goJ%8O+$%xLPItW&`2D|F||qy;W}?l}3e+dOG%HeRD( z=gRDJ-FD%N6|3zxYcAWyw)f06ej+hX()8|)6Oh5GYrnDi;mlIEYkBdy% zc6kRYk63`hiZv=u=Ux=Yy;ogq8uMf-``nbKwy7(9qg%F~S+il@M1xoUH+UC*4qD*- za+_$Rpp=;L&6cBcrni;#IHfBV>`2W%b+mTND(>{XdsIPn4Oc9zw~Rt*@!Ur zxtY=7qF3fz?hIeKzWSn0#>Lvn=C8z+7?zi@`m36(F82uzdHmwH=GCXQb(6I>MLNx3 z;?oV)4^0)Ae%rNnXTIEv2;qrKr#?`fyvhCU^t78lZbetcT|Xpp`)0G%!a^2vCDCHH z$f6LN7c5~d2j`!N2wptxme<1CUWq0#rYtd|+w)Zd_Oi*(+jY)$;*?#lgEUr2EYnd~ zcqHvCe_z!ii_4oHEzxVwY>9j7sU`a470>mKh{C-ye!siY`-1sgi)%(l%mrSFJ$}oY z^scVR+`iC3US*v~$vesY3wte+&(4#1CC2=P<++Pq$#iC~=HTSXzuXs|ogiRP#=JlE z=w}XXzdL=Z?WH^7t0#&#DV*M>p;$5Tc7AqSMC&5SZwuI&Hw#V+5|xttw?6Z-mxgV{ zMb_(9dfyw9^N-H@^=WR!#+j|^R|9@|_$^@Ko8QU*bK%812d~VYe@g!(L;eA!{|8DK zGaf9FJrIz~cuFRgeRz^9 zcF{347ni*P9yvOe4}QrzZh!Ui#Ov+msvNP-jBoDWul&I(*rR$Z?6&B_BM~M?e%|Hg zu+wLkDGcxWQ0X2PWS@LctM-APo!DgsKBt<-$}F3yE|ao$yS?d@4AM#VxWZ%R-ZQcL zTuFtFMiBEMnVCGB@*Ed!St+%sdFzBrt+uBuJ!W-qFbFaHV_;!mWPk!jW-!|b$Yn%g zGXdGmK(PW=1_oxRIFQM}24yn=*}RNQ3~oTW3@E_|WD5b=dw^^~Mu_vz17?kY*WQ#-9cLK{vDMnVXd(HvPk%qFb0ogK0=4e3KAUD_|v4LC$4lvZu$j?pH zPbx}GNy{v$($7dt&el&(%+1LxF3B%S)z8UH(ofHai0J1gre`MWm!uY#q$HLk>KEr` m=A`PCWTvGtI2WZRmZYXAlxLP?D7bt227oN^0Ai45z%&5H)jm`J literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.xcf b/vendor/github.com/camlistore/camlistore/pkg/magic/testdata/smile.xcf new file mode 100644 index 0000000000000000000000000000000000000000..3e0914f1e6c723bd30403e9085c8b55dabef4b33 GIT binary patch literal 2951 zcmeHI&2Jl35P#3!P5hA%H)&(AQo7+owUJV#Nfmm6sED?RMv4Ry0tr>FvtF<8M`F9e z6^En}7eu6TssbmZN;&3`f4~Q>L4e?t%e#<&z$=ls_1ob$n{{zT_zUop#&7mFGrxH| zGtcX|m36CCC|X6gYzwS~P*^_)krF>skl2mygkZrs4>2GS$aJW)-v#*J!anYRcCJvX zRP1UK&w`DU@73*m(_XW_bekpX)(0!AY)J*KZYA&7b57k|V+`I>WE;%~W!q}h%I;b& zku5rfTDexYvcWc2ckbn_`786mIzzb{nsNxecSdO6ZvD5yp{>SxzTj3J>uNt2zz1i6 zAvSlv>NYL6S}55Kj;|Gq4ZHdLfE@jA=oEP|lKx!2dQ5B4W{BVYK!sn_mTgR*fq zc#S}~I|PY^IyWF5>exhLKu-?nV*~p5fIb18x%z7q3g_T8I1Q(Whk`0}DL;QN(CjXY@xym_n!9AQ%S2b(rMN<2^Y67{?i7aZ31 zX+kUDiioFiBF|(thc1O2T9)Xa96(zUrpzfu19n5vMf4@faD0yd}W+ zUYm|Z!PGU> (32 - numBits) + } + + if getBits(0, 11) != 0x7ff { + return 0, errors.New("Missing sync bits in MPEG frame header") + } + var version mpegVersion + var ok bool + if version, ok = mpegVersionsById[getBits(11, 2)]; !ok { + return 0, errors.New("Invalid MPEG version index") + } + var layer mpegLayer + if layer, ok = mpegLayersByIndex[getBits(13, 2)]; !ok { + return 0, errors.New("Invalid MPEG layer index") + } + bitrate := mpegBitrates[version][layer][getBits(16, 4)] + if bitrate == 0 { + return 0, errors.New("Invalid MPEG bitrate") + } + samplingRate := mpegSamplingRates[version][getBits(20, 2)] + if samplingRate == 0 { + return 0, errors.New("Invalid MPEG sample rate") + } + samplesPerFrame := mpegSamplesPerFrame[version][layer] + + var xingHeaderStart int64 = 4 + // Skip "side information". + if getBits(24, 2) == 0x3 { // Channel mode; 0x3 is mono. + xingHeaderStart += 17 + } else { + xingHeaderStart += 32 + } + // Skip 16-bit CRC if present. + if getBits(15, 1) == 0x0 { // 0x0 means "has protection". + xingHeaderStart += 2 + } + + b := make([]byte, 12, 12) + if _, err := r.ReadAt(b, xingHeaderStart); err != nil { + return 0, fmt.Errorf("Unable to read Xing header at %d: %v", xingHeaderStart, err) + } + var ms int64 + if bytes.Equal(b[0:4], xingHeaderName) || bytes.Equal(b[0:4], infoHeaderName) { + r := bytes.NewReader(b[4:]) + var xingFlags uint32 + binary.Read(r, binary.BigEndian, &xingFlags) + if xingFlags&0x1 == 0x0 { + return 0, fmt.Errorf("Xing header at %d lacks number of frames", xingHeaderStart) + } + var numFrames uint32 + binary.Read(r, binary.BigEndian, &numFrames) + ms = int64(samplesPerFrame) * int64(numFrames) * 1000 / int64(samplingRate) + } else { + // Okay, no Xing VBR header. Assume that the file has a constant bitrate. + // (The other alternative is to read the whole file and examine each frame.) + ms = r.Size() / int64(bitrate) * 8 + } + return time.Duration(ms) * time.Millisecond, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/media/audio_test.go b/vendor/github.com/camlistore/camlistore/pkg/media/audio_test.go new file mode 100644 index 00000000..ea514400 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/media/audio_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package media + +import ( + "io" + "os" + "path/filepath" + "testing" + "time" +) + +// openFile opens fn, a file within the testdata dir, and returns an FD and the file's size. +func openFile(fn string) (*os.File, int64, error) { + f, err := os.Open(filepath.Join("testdata", fn)) + if err != nil { + return nil, 0, err + } + s, err := f.Stat() + if err != nil { + f.Close() + return nil, 0, err + } + return f, s.Size(), nil +} + +func TestHasID3v1Tag(t *testing.T) { + tests := []struct { + fn string + hasTag bool + }{ + {"xing_header.mp3", false}, + {"id3v1.mp3", true}, + } + for _, tt := range tests { + f, s, err := openFile(tt.fn) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + hasTag, err := HasID3v1Tag(io.NewSectionReader(f, 0, s)) + if err != nil { + t.Fatal(err) + } + if hasTag != tt.hasTag { + t.Errorf("Expected %v for %s but got %v", tt.hasTag, tt.fn, hasTag) + } + } +} + +func TestGetMPEGAudioDuration(t *testing.T) { + tests := []struct { + fn string + d time.Duration + }{ + {"128_cbr.mp3", time.Duration(1088) * time.Millisecond}, + {"xing_header.mp3", time.Duration(1097) * time.Millisecond}, + } + for _, tt := range tests { + f, s, err := openFile(tt.fn) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + d, err := GetMPEGAudioDuration(io.NewSectionReader(f, 0, s)) + if err != nil { + t.Fatal(err) + } + if d != tt.d { + t.Errorf("Expected %d for %s but got %d", tt.d, tt.fn, d) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/media/testdata/128_cbr.mp3 b/vendor/github.com/camlistore/camlistore/pkg/media/testdata/128_cbr.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..196f3148725cc15befff4afb0fc4a702c3098e56 GIT binary patch literal 4368 zcmezWTOfsj{{sUg?R=M0_fx!aFFU8AHeY#buf+o(Ng zfj1g%Sj$ngP{QPnhTCYkfl|e2Iv7m{qv-%=dla+aL=zd!Kco3)H2)0#{DYQlv2j8D zMvB|l*bGJ!8?^^5K#1fHw{Qz_bPvfa$w>tce_#iWMJ1WVCD`Q{7#wqwN^@~Z5+e=( D_P+FI literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/media/testdata/xing_header.mp3 b/vendor/github.com/camlistore/camlistore/pkg/media/testdata/xing_header.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..8dc443b3ea9ec93a40cf743b3c7c3b8ad1fbfa5b GIT binary patch literal 4785 zcmezWdqN5W0T7Xymkw0I55!sw3_=?j*w}b@goK17B;@2&R8+LI3=9m+%xr9&ot-^B z{ry8jL!+Y;6BEg?R=M0_fx!aFFU8AHeY#buf+o(Ng zfj1g%Sj$ngP{QPnhTCYkfl|e2Iv7m{qv-%=dla+aL=zd!Kco3)H2)0#{DYQlv2j8D XMvB|l*bGJ!8?^^5K#1fHw{QahowMyB literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/auth.go b/vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/auth.go new file mode 100644 index 00000000..d8c45e24 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/auth.go @@ -0,0 +1,206 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package s3 + +import ( + "bytes" + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "fmt" + "io" + "net/http" + "net/url" + "sort" + "strings" + "time" +) + +// See http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html + +type Auth struct { + AccessKey string + SecretAccessKey string + + // Hostname is the S3 hostname to use. + // If empty, the standard US region of "s3.amazonaws.com" is + // used. + Hostname string +} + +const standardUSRegionAWS = "s3.amazonaws.com" + +func (a *Auth) hostname() string { + if a.Hostname != "" { + return a.Hostname + } + return standardUSRegionAWS +} + +func (a *Auth) SignRequest(req *http.Request) { + if date := req.Header.Get("Date"); date == "" { + req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat)) + } + hm := hmac.New(sha1.New, []byte(a.SecretAccessKey)) + ss := a.stringToSign(req) + // log.Printf("String to sign: %q (%x)", ss, ss) + io.WriteString(hm, ss) + + authHeader := new(bytes.Buffer) + fmt.Fprintf(authHeader, "AWS %s:", a.AccessKey) + encoder := base64.NewEncoder(base64.StdEncoding, authHeader) + encoder.Write(hm.Sum(nil)) + encoder.Close() + req.Header.Set("Authorization", authHeader.String()) +} + +func firstNonEmptyString(strs ...string) string { + for _, s := range strs { + if s != "" { + return s + } + } + return "" +} + +// From the Amazon docs: +// +// StringToSign = HTTP-Verb + "\n" + +// Content-MD5 + "\n" + +// Content-Type + "\n" + +// Date + "\n" + +// CanonicalizedAmzHeaders + +// CanonicalizedResource; +func (a *Auth) stringToSign(req *http.Request) string { + buf := new(bytes.Buffer) + buf.WriteString(req.Method) + buf.WriteByte('\n') + buf.WriteString(req.Header.Get("Content-MD5")) + buf.WriteByte('\n') + buf.WriteString(req.Header.Get("Content-Type")) + buf.WriteByte('\n') + if req.Header.Get("x-amz-date") == "" { + buf.WriteString(req.Header.Get("Date")) + } + buf.WriteByte('\n') + a.writeCanonicalizedAmzHeaders(buf, req) + a.writeCanonicalizedResource(buf, req) + return buf.String() +} + +func hasPrefixCaseInsensitive(s, pfx string) bool { + if len(pfx) > len(s) { + return false + } + shead := s[:len(pfx)] + if shead == pfx { + return true + } + shead = strings.ToLower(shead) + return shead == pfx || shead == strings.ToLower(pfx) +} + +func (a *Auth) writeCanonicalizedAmzHeaders(buf *bytes.Buffer, req *http.Request) { + amzHeaders := make([]string, 0) + vals := make(map[string][]string) + for k, vv := range req.Header { + if hasPrefixCaseInsensitive(k, "x-amz-") { + lk := strings.ToLower(k) + amzHeaders = append(amzHeaders, lk) + vals[lk] = vv + } + } + sort.Strings(amzHeaders) + for _, k := range amzHeaders { + buf.WriteString(k) + buf.WriteByte(':') + for idx, v := range vals[k] { + if idx > 0 { + buf.WriteByte(',') + } + if strings.Contains(v, "\n") { + // TODO: "Unfold" long headers that + // span multiple lines (as allowed by + // RFC 2616, section 4.2) by replacing + // the folding white-space (including + // new-line) by a single space. + buf.WriteString(v) + } else { + buf.WriteString(v) + } + } + buf.WriteByte('\n') + } +} + +// Must be sorted: +var subResList = []string{"acl", "lifecycle", "location", "logging", "notification", "partNumber", "policy", "requestPayment", "torrent", "uploadId", "uploads", "versionId", "versioning", "versions", "website"} + +// From the Amazon docs: +// +// CanonicalizedResource = [ "/" + Bucket ] + +// + +// [ sub-resource, if present. For example "?acl", "?location", "?logging", or "?torrent"]; +func (a *Auth) writeCanonicalizedResource(buf *bytes.Buffer, req *http.Request) { + bucket := a.bucketFromHostname(req) + if bucket != "" { + buf.WriteByte('/') + buf.WriteString(bucket) + } + buf.WriteString(req.URL.Path) + if req.URL.RawQuery != "" { + n := 0 + vals, _ := url.ParseQuery(req.URL.RawQuery) + for _, subres := range subResList { + if vv, ok := vals[subres]; ok && len(vv) > 0 { + n++ + if n == 1 { + buf.WriteByte('?') + } else { + buf.WriteByte('&') + } + buf.WriteString(subres) + if len(vv[0]) > 0 { + buf.WriteByte('=') + buf.WriteString(url.QueryEscape(vv[0])) + } + } + } + } +} + +// hasDotSuffix reports whether s ends with "." + suffix. +func hasDotSuffix(s string, suffix string) bool { + return len(s) >= len(suffix)+1 && strings.HasSuffix(s, suffix) && s[len(s)-len(suffix)-1] == '.' +} + +func (a *Auth) bucketFromHostname(req *http.Request) string { + host := req.Host + if host == "" { + host = req.URL.Host + } + if host == a.hostname() { + return "" + } + if hostSuffix := a.hostname(); hasDotSuffix(host, hostSuffix) { + return host[:len(host)-len(hostSuffix)-1] + } + if lastColon := strings.LastIndex(host, ":"); lastColon != -1 { + return host[:lastColon] + } + return host +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/auth_test.go b/vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/auth_test.go new file mode 100644 index 00000000..531667d1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/auth_test.go @@ -0,0 +1,139 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package s3 + +import ( + "bufio" + "fmt" + "net/http" + "strings" + "testing" +) + +type reqAndExpected struct { + req, expected string +} + +func req(s string) *http.Request { + req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(s))) + if err != nil { + panic(fmt.Sprintf("bad request in test: %v (error: %v)", req, err)) + } + return req +} + +func TestStringToSign(t *testing.T) { + var a Auth + tests := []reqAndExpected{ + {`GET /photos/puppy.jpg HTTP/1.1 +Host: johnsmith.s3.amazonaws.com +Date: Tue, 27 Mar 2007 19:36:42 +0000 + +`, + "GET\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/johnsmith/photos/puppy.jpg"}, + {`PUT /photos/puppy.jpg HTTP/1.1 +Content-Type: image/jpeg +Content-Length: 94328 +Host: johnsmith.s3.amazonaws.com +Date: Tue, 27 Mar 2007 21:15:45 +0000 + +`, + "PUT\n\nimage/jpeg\nTue, 27 Mar 2007 21:15:45 +0000\n/johnsmith/photos/puppy.jpg"}, + {`GET /?prefix=photos&max-keys=50&marker=puppy HTTP/1.1 +User-Agent: Mozilla/5.0 +Host: johnsmith.s3.amazonaws.com +Date: Tue, 27 Mar 2007 19:42:41 +0000 + +`, + "GET\n\n\nTue, 27 Mar 2007 19:42:41 +0000\n/johnsmith/"}, + {`DELETE /johnsmith/photos/puppy.jpg HTTP/1.1 +User-Agent: dotnet +Host: s3.amazonaws.com +Date: Tue, 27 Mar 2007 21:20:27 +0000 +x-amz-date: Tue, 27 Mar 2007 21:20:26 +0000 + +`, + "DELETE\n\n\n\nx-amz-date:Tue, 27 Mar 2007 21:20:26 +0000\n/johnsmith/photos/puppy.jpg"}, + {`PUT /db-backup.dat.gz HTTP/1.1 +User-Agent: curl/7.15.5 +Host: static.johnsmith.net:8080 +Date: Tue, 27 Mar 2007 21:06:08 +0000 +x-amz-acl: public-read +content-type: application/x-download +Content-MD5: 4gJE4saaMU4BqNR0kLY+lw== +X-Amz-Meta-ReviewedBy: joe@johnsmith.net +X-Amz-Meta-ReviewedBy: jane@johnsmith.net +X-Amz-Meta-FileChecksum: 0x02661779 +X-Amz-Meta-ChecksumAlgorithm: crc32 +Content-Disposition: attachment; filename=database.dat +Content-Encoding: gzip +Content-Length: 5913339 + +`, + "PUT\n4gJE4saaMU4BqNR0kLY+lw==\napplication/x-download\nTue, 27 Mar 2007 21:06:08 +0000\nx-amz-acl:public-read\nx-amz-meta-checksumalgorithm:crc32\nx-amz-meta-filechecksum:0x02661779\nx-amz-meta-reviewedby:joe@johnsmith.net,jane@johnsmith.net\n/static.johnsmith.net/db-backup.dat.gz"}, + } + for idx, test := range tests { + got := a.stringToSign(req(test.req)) + if got != test.expected { + t.Errorf("test %d: expected %q", idx, test.expected) + t.Errorf("test %d: got %q", idx, got) + } + } +} + +func TestBucketFromHostname(t *testing.T) { + var a Auth + tests := []reqAndExpected{ + {"GET / HTTP/1.0\n\n", ""}, + {"GET / HTTP/1.0\nHost: s3.amazonaws.com\n\n", ""}, + {"GET / HTTP/1.0\nHost: foo.s3.amazonaws.com\n\n", "foo"}, + {"GET / HTTP/1.0\nHost: foo.com:123\n\n", "foo.com"}, + {"GET / HTTP/1.0\nHost: bar.com\n\n", "bar.com"}, + } + for idx, test := range tests { + got := a.bucketFromHostname(req(test.req)) + if got != test.expected { + t.Errorf("test %d: expected %q; got %q", idx, test.expected, got) + } + } +} + +func TestSignRequest(t *testing.T) { + r := req("GET /foo HTTP/1.1\n\n") + auth := &Auth{AccessKey: "key", SecretAccessKey: "secretkey"} + auth.SignRequest(r) + if r.Header.Get("Date") == "" { + t.Error("expected a Date set") + } + r.Header.Set("Date", "Sat, 02 Apr 2011 04:23:52 GMT") + auth.SignRequest(r) + if e, g := r.Header.Get("Authorization"), "AWS key:kHpCR/N7Rw3PwRlDd8+5X40CFVc="; e != g { + t.Errorf("got header %q; expected %q", g, e) + } +} + +func TestHasDotSuffix(t *testing.T) { + if !hasDotSuffix("foo.com", "com") { + t.Fail() + } + if hasDotSuffix("foocom", "com") { + t.Fail() + } + if hasDotSuffix("com", "com") { + t.Fail() + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/client.go b/vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/client.go new file mode 100644 index 00000000..a7b44e95 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/client.go @@ -0,0 +1,445 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package s3 implements a generic Amazon S3 client, not specific +// to Camlistore. +package s3 + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "encoding/xml" + "errors" + "fmt" + "hash" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/httputil" +) + +const maxList = 1000 + +// Client is an Amazon S3 client. +type Client struct { + *Auth + Transport http.RoundTripper // or nil for the default +} + +type Bucket struct { + Name string + CreationDate string // 2006-02-03T16:45:09.000Z +} + +func (c *Client) transport() http.RoundTripper { + if c.Transport != nil { + return c.Transport + } + return http.DefaultTransport +} + +// bucketURL returns the URL prefix of the bucket, with trailing slash +func (c *Client) bucketURL(bucket string) string { + if IsValidBucket(bucket) && !strings.Contains(bucket, ".") { + return fmt.Sprintf("https://%s.%s/", bucket, c.hostname()) + } + return fmt.Sprintf("https://%s/%s/", c.hostname(), bucket) +} + +func (c *Client) keyURL(bucket, key string) string { + return c.bucketURL(bucket) + key +} + +func newReq(url_ string) *http.Request { + req, err := http.NewRequest("GET", url_, nil) + if err != nil { + panic(fmt.Sprintf("s3 client; invalid URL: %v", err)) + } + req.Header.Set("User-Agent", "go-camlistore-s3") + return req +} + +func (c *Client) Buckets() ([]*Bucket, error) { + req := newReq("https://" + c.hostname() + "/") + c.Auth.SignRequest(req) + res, err := c.transport().RoundTrip(req) + if err != nil { + return nil, err + } + defer httputil.CloseBody(res.Body) + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("s3: Unexpected status code %d fetching bucket list", res.StatusCode) + } + return parseListAllMyBuckets(res.Body) +} + +func parseListAllMyBuckets(r io.Reader) ([]*Bucket, error) { + type allMyBuckets struct { + Buckets struct { + Bucket []*Bucket + } + } + var res allMyBuckets + if err := xml.NewDecoder(r).Decode(&res); err != nil { + return nil, err + } + return res.Buckets.Bucket, nil +} + +// Returns 0, os.ErrNotExist if not on S3, otherwise reterr is real. +func (c *Client) Stat(key, bucket string) (size int64, reterr error) { + req := newReq(c.keyURL(bucket, key)) + req.Method = "HEAD" + c.Auth.SignRequest(req) + res, err := c.transport().RoundTrip(req) + if err != nil { + return 0, err + } + if res.Body != nil { + defer res.Body.Close() + } + switch res.StatusCode { + case http.StatusNotFound: + return 0, os.ErrNotExist + case http.StatusOK: + return strconv.ParseInt(res.Header.Get("Content-Length"), 10, 64) + } + return 0, fmt.Errorf("s3: Unexpected status code %d statting object %v", res.StatusCode, key) +} + +func (c *Client) PutObject(key, bucket string, md5 hash.Hash, size int64, body io.Reader) error { + req := newReq(c.keyURL(bucket, key)) + req.Method = "PUT" + req.ContentLength = size + if md5 != nil { + b64 := new(bytes.Buffer) + encoder := base64.NewEncoder(base64.StdEncoding, b64) + encoder.Write(md5.Sum(nil)) + encoder.Close() + req.Header.Set("Content-MD5", b64.String()) + } + c.Auth.SignRequest(req) + req.Body = ioutil.NopCloser(body) + + res, err := c.transport().RoundTrip(req) + if res != nil && res.Body != nil { + defer httputil.CloseBody(res.Body) + } + if err != nil { + return err + } + if res.StatusCode != http.StatusOK { + // res.Write(os.Stderr) + return fmt.Errorf("Got response code %d from s3", res.StatusCode) + } + return nil +} + +type Item struct { + Key string + Size int64 +} + +type listBucketResults struct { + Contents []*Item + IsTruncated bool + MaxKeys int + Name string // bucket name + Marker string +} + +// BucketLocation returns the S3 hostname to be used with the given bucket. +func (c *Client) BucketLocation(bucket string) (location string, err error) { + if !strings.HasSuffix(c.hostname(), "amazonaws.com") { + return "", errors.New("BucketLocation not implemented for non-Amazon S3 hostnames") + } + url_ := fmt.Sprintf("https://s3.amazonaws.com/%s/?location", url.QueryEscape(bucket)) + req := newReq(url_) + c.Auth.SignRequest(req) + res, err := c.transport().RoundTrip(req) + if err != nil { + return + } + var xres xmlLocationConstraint + if err := xml.NewDecoder(res.Body).Decode(&xres); err != nil { + return "", err + } + if xres.Location == "" { + return "s3.amazonaws.com", nil + } + return "s3-" + xres.Location + ".amazonaws.com", nil +} + +// ListBucket returns 0 to maxKeys (inclusive) items from the provided +// bucket. Keys before startAt will be skipped. (This is the S3 +// 'marker' value). If the length of the returned items is equal to +// maxKeys, there is no indication whether or not the returned list is +// truncated. +func (c *Client) ListBucket(bucket string, startAt string, maxKeys int) (items []*Item, err error) { + if maxKeys < 0 { + return nil, errors.New("invalid negative maxKeys") + } + marker := startAt + for len(items) < maxKeys { + fetchN := maxKeys - len(items) + if fetchN > maxList { + fetchN = maxList + } + var bres listBucketResults + + url_ := fmt.Sprintf("%s?marker=%s&max-keys=%d", + c.bucketURL(bucket), url.QueryEscape(marker), fetchN) + + // Try the enumerate three times, since Amazon likes to close + // https connections a lot, and Go sucks at dealing with it: + // https://code.google.com/p/go/issues/detail?id=3514 + const maxTries = 5 + for try := 1; try <= maxTries; try++ { + time.Sleep(time.Duration(try-1) * 100 * time.Millisecond) + req := newReq(url_) + c.Auth.SignRequest(req) + res, err := c.transport().RoundTrip(req) + if err != nil { + if try < maxTries { + continue + } + return nil, err + } + if res.StatusCode != http.StatusOK { + if res.StatusCode < 500 { + body, _ := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20)) + aerr := &Error{ + Op: "ListBucket", + Code: res.StatusCode, + Body: body, + Header: res.Header, + } + aerr.parseXML() + res.Body.Close() + return nil, aerr + } + } else { + bres = listBucketResults{} + var logbuf bytes.Buffer + err = xml.NewDecoder(io.TeeReader(res.Body, &logbuf)).Decode(&bres) + if err != nil { + log.Printf("Error parsing s3 XML response: %v for %q", err, logbuf.Bytes()) + } else if bres.MaxKeys != fetchN || bres.Name != bucket || bres.Marker != marker { + err = fmt.Errorf("Unexpected parse from server: %#v from: %s", bres, logbuf.Bytes()) + log.Print(err) + } + } + httputil.CloseBody(res.Body) + if err != nil { + if try < maxTries-1 { + continue + } + log.Print(err) + return nil, err + } + break + } + for _, it := range bres.Contents { + if it.Key == marker && it.Key != startAt { + // Skip first dup on pages 2 and higher. + continue + } + if it.Key < startAt { + return nil, fmt.Errorf("Unexpected response from Amazon: item key %q but wanted greater than %q", it.Key, startAt) + } + items = append(items, it) + marker = it.Key + } + if !bres.IsTruncated { + // log.Printf("Not truncated. so breaking. items = %d; len Contents = %d, url = %s", len(items), len(bres.Contents), url_) + break + } + } + return items, nil +} + +func (c *Client) Get(bucket, key string) (body io.ReadCloser, size int64, err error) { + req := newReq(c.keyURL(bucket, key)) + c.Auth.SignRequest(req) + res, err := c.transport().RoundTrip(req) + if err != nil { + return + } + switch res.StatusCode { + case http.StatusOK: + return res.Body, res.ContentLength, nil + case http.StatusNotFound: + res.Body.Close() + return nil, 0, os.ErrNotExist + default: + res.Body.Close() + return nil, 0, fmt.Errorf("Amazon HTTP error on GET: %d", res.StatusCode) + } +} + +// GetPartial fetches part of the s3 key object in bucket. +// If length is negative, the rest of the object is returned. +// The caller must close rc. +func (c *Client) GetPartial(bucket, key string, offset, length int64) (rc io.ReadCloser, err error) { + if offset < 0 { + return nil, errors.New("invalid negative length") + } + + req := newReq(c.keyURL(bucket, key)) + if length >= 0 { + req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1)) + } else { + req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset)) + } + c.Auth.SignRequest(req) + + res, err := c.transport().RoundTrip(req) + if err != nil { + return + } + switch res.StatusCode { + case http.StatusOK, http.StatusPartialContent: + return res.Body, nil + case http.StatusNotFound: + res.Body.Close() + return nil, os.ErrNotExist + case http.StatusRequestedRangeNotSatisfiable: + res.Body.Close() + return nil, blob.ErrOutOfRangeOffsetSubFetch + default: + res.Body.Close() + return nil, fmt.Errorf("Amazon HTTP error on GET: %d", res.StatusCode) + } +} + +func (c *Client) Delete(bucket, key string) error { + req := newReq(c.keyURL(bucket, key)) + req.Method = "DELETE" + c.Auth.SignRequest(req) + res, err := c.transport().RoundTrip(req) + if err != nil { + return err + } + if res != nil && res.Body != nil { + defer res.Body.Close() + } + if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusNoContent || + res.StatusCode == http.StatusOK { + return nil + } + return fmt.Errorf("Amazon HTTP error on DELETE: %d", res.StatusCode) +} + +// IsValid reports whether bucket is a valid bucket name, per Amazon's naming restrictions. +// +// See http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html +func IsValidBucket(bucket string) bool { + l := len(bucket) + if l < 3 || l > 63 { + return false + } + + valid := false + prev := byte('.') + for i := 0; i < len(bucket); i++ { + c := bucket[i] + switch { + default: + return false + case 'a' <= c && c <= 'z': + valid = true + case '0' <= c && c <= '9': + // Is allowed, but bucketname can't be just numbers. + // Therefore, don't set valid to true + case c == '-': + if prev == '.' { + return false + } + case c == '.': + if prev == '.' || prev == '-' { + return false + } + } + prev = c + } + + if prev == '-' || prev == '.' { + return false + } + return valid +} + +// Error is the type returned by some API operations. +// +// TODO: it should be more/all of them. +type Error struct { + Op string + Code int // HTTP status code + Body []byte // response body + Header http.Header // response headers + + // UsedEndpoint and AmazonCode are the XML response's Endpoint and + // Code fields, respectively. + UseEndpoint string // if a temporary redirect (wrong hostname) + AmazonCode string +} + +func (e *Error) Error() string { + if bytes.Contains(e.Body, []byte("")) { + return fmt.Sprintf("s3.%s: status %d: %s", e.Op, e.Code, e.Body) + } + return fmt.Sprintf("s3.%s: status %d", e.Op, e.Code) +} + +func (e *Error) parseXML() { + var xe xmlError + _ = xml.NewDecoder(bytes.NewReader(e.Body)).Decode(&xe) + e.AmazonCode = xe.Code + if xe.Code == "TemporaryRedirect" { + e.UseEndpoint = xe.Endpoint + } + if xe.Code == "SignatureDoesNotMatch" { + want, _ := hex.DecodeString(strings.Replace(xe.StringToSignBytes, " ", "", -1)) + log.Printf("S3 SignatureDoesNotMatch. StringToSign should be %d bytes: %q (%x)", len(want), want, want) + } + +} + +// xmlError is the Error response from Amazon. +type xmlError struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + RequestId string + Bucket string + Endpoint string + StringToSignBytes string +} + +// xmlLocationConstraint is the LocationConstraint returned from BucketLocation. +type xmlLocationConstraint struct { + XMLName xml.Name `xml:"LocationConstraint"` + Location string `xml:",chardata"` +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/client_test.go b/vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/client_test.go new file mode 100644 index 00000000..d8cbd4e5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/misc/amazon/s3/client_test.go @@ -0,0 +1,95 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package s3 + +import ( + "net/http" + "os" + "reflect" + "strings" + "testing" +) + +var tc *Client + +func getTestClient(t *testing.T) bool { + accessKey := os.Getenv("AWS_ACCESS_KEY_ID") + secret := os.Getenv("AWS_ACCESS_KEY_SECRET") + if accessKey == "" || secret == "" { + t.Logf("Skipping test; no AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY_SECRET set in environment") + return false + } + tc = &Client{&Auth{AccessKey: accessKey, SecretAccessKey: secret}, http.DefaultTransport} + return true +} + +func TestBuckets(t *testing.T) { + if !getTestClient(t) { + return + } + tc.Buckets() +} + +func TestParseBuckets(t *testing.T) { + res := "\nownerIDFieldbobDisplayNamebucketOne2006-06-21T07:04:31.000ZbucketTwo2006-06-21T07:04:32.000Z" + buckets, err := parseListAllMyBuckets(strings.NewReader(res)) + if err != nil { + t.Fatal(err) + } + if g, w := len(buckets), 2; g != w { + t.Errorf("num parsed buckets = %d; want %d", g, w) + } + want := []*Bucket{ + {Name: "bucketOne", CreationDate: "2006-06-21T07:04:31.000Z"}, + {Name: "bucketTwo", CreationDate: "2006-06-21T07:04:32.000Z"}, + } + dump := func(v []*Bucket) { + for i, b := range v { + t.Logf("Bucket #%d: %#v", i, b) + } + } + if !reflect.DeepEqual(buckets, want) { + t.Error("mismatch; GOT:") + dump(buckets) + t.Error("WANT:") + dump(want) + } +} + +func TestValidBucketNames(t *testing.T) { + m := []struct { + in string + want bool + }{ + {"myawsbucket", true}, + {"my.aws.bucket", true}, + {"my-aws-bucket.1", true}, + {"my---bucket.1", true}, + {".myawsbucket", false}, + {"-myawsbucket", false}, + {"myawsbucket.", false}, + {"myawsbucket-", false}, + {"my..awsbucket", false}, + } + + for _, bt := range m { + got := IsValidBucket(bt.in) + if got != bt.want { + t.Errorf("func(%q) = %v; want %v", bt.in, got, bt.want) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/misc/closure/genclosuredeps/genclosuredeps.go b/vendor/github.com/camlistore/camlistore/pkg/misc/closure/genclosuredeps/genclosuredeps.go new file mode 100644 index 00000000..7d23df49 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/misc/closure/genclosuredeps/genclosuredeps.go @@ -0,0 +1,51 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// The genclosuredeps command, similarly to the closure depswriter.py tool, +// outputs to os.Stdout for each .js file, which namespaces +// it provides, and the namespaces it requires, hence helping +// the closure library to resolve dependencies between those files. +package main + +import ( + "bytes" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" + + "camlistore.org/pkg/misc/closure" +) + +func usage() { + fmt.Fprintf(os.Stderr, "Usage: genclosuredeps

    \n") + os.Exit(1) +} + +func main() { + flag.Parse() + args := flag.Args() + if len(args) != 1 { + usage() + } + b, err := closure.GenDeps(http.Dir(args[0])) + if err != nil { + log.Fatal(err) + } + io.Copy(os.Stdout, bytes.NewReader(b)) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/misc/closure/gendeps.go b/vendor/github.com/camlistore/camlistore/pkg/misc/closure/gendeps.go new file mode 100644 index 00000000..69178c24 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/misc/closure/gendeps.go @@ -0,0 +1,224 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package closure provides tools to help with the use of the +// closure library. +// +// See https://code.google.com/p/closure-library/ +package closure + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net/http" + "os" + "regexp" + "strings" + "sync" + "time" +) + +// GenDeps returns the namespace dependencies between the closure javascript files in root. It does not descend in directories. +// Each of the files listed in the output is prepended with the path "../../", which is assumed to be the location where these files can be found, relative to Closure's base.js. +// +// The format for each relevant javascript file is: +// goog.addDependency("filepath", ["namespace provided"], ["required namespace 1", "required namespace 2", ...]); +func GenDeps(root http.FileSystem) ([]byte, error) { + // In the typical configuration, Closure is served at 'closure/goog/...'' + return GenDepsWithPath("../../", root) +} + +// GenDepsWithPath is like GenDeps, but you can specify a path where the files are to be found at runtime relative to Closure's base.js. +func GenDepsWithPath(pathPrefix string, root http.FileSystem) ([]byte, error) { + d, err := root.Open("/") + if err != nil { + return nil, fmt.Errorf("Failed to open root of %v: %v", root, err) + } + fi, err := d.Stat() + if err != nil { + return nil, err + } + if !fi.IsDir() { + return nil, fmt.Errorf("root of %v is not a dir", root) + } + ent, err := d.Readdir(-1) + if err != nil { + return nil, fmt.Errorf("Could not read dir entries of root: %v", err) + } + var buf bytes.Buffer + for _, info := range ent { + name := info.Name() + if !strings.HasSuffix(name, ".js") { + continue + } + if strings.HasPrefix(name, ".#") { + // Emacs noise. + continue + } + f, err := root.Open(name) + if err != nil { + return nil, fmt.Errorf("Could not open %v: %v", name, err) + } + prov, req, err := parseProvidesRequires(info, name, f) + f.Close() + if err != nil { + return nil, fmt.Errorf("Could not parse deps for %v: %v", name, err) + } + if len(prov) > 0 { + fmt.Fprintf(&buf, "goog.addDependency(%q, %v, %v);\n", pathPrefix+name, jsList(prov), jsList(req)) + } + } + return buf.Bytes(), nil +} + +var provReqRx = regexp.MustCompile(`^goog\.(provide|require)\(['"]([\w\.]+)['"]\)`) + +type depCacheItem struct { + modTime time.Time + provides, requires []string +} + +var ( + depCacheMu sync.Mutex + depCache = map[string]depCacheItem{} +) + +func parseProvidesRequires(fi os.FileInfo, path string, f io.Reader) (provides, requires []string, err error) { + mt := fi.ModTime() + depCacheMu.Lock() + defer depCacheMu.Unlock() + if ci := depCache[path]; ci.modTime.Equal(mt) { + return ci.provides, ci.requires, nil + } + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + l := scanner.Text() + if !strings.HasPrefix(l, "goog.") { + continue + } + m := provReqRx.FindStringSubmatch(l) + if m != nil { + if m[1] == "provide" { + provides = append(provides, m[2]) + } else { + requires = append(requires, m[2]) + } + } + } + if err := scanner.Err(); err != nil { + return nil, nil, err + } + depCache[path] = depCacheItem{provides: provides, requires: requires, modTime: mt} + return provides, requires, nil +} + +// jsList prints a list of strings as JavaScript list. +type jsList []string + +func (s jsList) String() string { + var buf bytes.Buffer + buf.WriteByte('[') + for i, v := range s { + if i > 0 { + buf.WriteString(", ") + } + fmt.Fprintf(&buf, "%q", v) + } + buf.WriteByte(']') + return buf.String() +} + +// Example of a match: +// goog.addDependency('asserts/asserts.js', ['goog.asserts', 'goog.asserts.AssertionError'], ['goog.debug.Error', 'goog.string']); +// So with m := depsRx.FindStringSubmatch, +// the provider: m[1] == "asserts/asserts.js" +// the provided namespaces: m[2] == "'goog.asserts', 'goog.asserts.AssertionError'" +// the required namespaces: m[5] == "'goog.debug.Error', 'goog.string'" +var depsRx = regexp.MustCompile(`^goog.addDependency\(['"]([^/]+[a-zA-Z0-9\-\_/\.]*\.js)['"], \[((['"][\w\.]+['"])+(, ['"][\w\.]+['"])*)\], \[((['"][\w\.]+['"])+(, ['"][\w\.]+['"])*)?\]\);`) + +// ParseDeps reads closure namespace dependency lines and +// returns a map giving the js file provider for each namespace, +// and a map giving the namespace dependencies for each namespace. +func ParseDeps(r io.Reader) (providedBy map[string]string, requires map[string][]string, err error) { + providedBy = make(map[string]string) + requires = make(map[string][]string) + scanner := bufio.NewScanner(r) + for scanner.Scan() { + l := scanner.Text() + if strings.HasPrefix(l, "//") { + continue + } + if l == "" { + continue + } + m := depsRx.FindStringSubmatch(l) + if m == nil { + return nil, nil, fmt.Errorf("Invalid line in deps: %q", l) + } + jsfile := m[1] + provides := strings.Split(m[2], ", ") + var required []string + if m[5] != "" { + required = strings.Split( + strings.Replace(strings.Replace(m[5], "'", "", -1), `"`, "", -1), ", ") + } + for _, v := range provides { + namespace := strings.Trim(v, `'"`) + if otherjs, ok := providedBy[namespace]; ok { + return nil, nil, fmt.Errorf("Name %v is provided by both %v and %v", namespace, jsfile, otherjs) + } + providedBy[namespace] = jsfile + if _, ok := requires[namespace]; ok { + return nil, nil, fmt.Errorf("Name %v has two sets of dependencies", namespace) + } + if required != nil { + requires[namespace] = required + } + } + } + if err := scanner.Err(); err != nil { + return nil, nil, err + } + return providedBy, requires, nil +} + +// DeepParseDeps reads closure namespace dependency lines and +// returns a map giving all the required js files for each namespace. +func DeepParseDeps(r io.Reader) (map[string][]string, error) { + providedBy, requires, err := ParseDeps(r) + if err != nil { + return nil, err + } + filesDeps := make(map[string][]string) + var deeperDeps func(namespace string) []string + deeperDeps = func(namespace string) []string { + if jsdeps, ok := filesDeps[namespace]; ok { + return jsdeps + } + jsfiles := []string{providedBy[namespace]} + for _, dep := range requires[namespace] { + jsfiles = append(jsfiles, deeperDeps(dep)...) + } + return jsfiles + } + for namespace, _ := range providedBy { + filesDeps[namespace] = deeperDeps(namespace) + } + return filesDeps, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/misc/closure/gendeps_test.go b/vendor/github.com/camlistore/camlistore/pkg/misc/closure/gendeps_test.go new file mode 100644 index 00000000..6326c84a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/misc/closure/gendeps_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package closure + +import ( + "reflect" + "strings" + "testing" +) + +var testdata = ` +goog.addDependency('asserts/asserts.js', ['goog.asserts', 'goog.asserts.AssertionError'], ['goog.debug.Error', 'goog.string']); +goog.addDependency('debug/error.js', ['goog.debug.Error'], []); +goog.addDependency('string/string.js', ['goog.string', 'goog.string.Unicode'], []); +` + +type parsedDeps struct { + providedBy map[string]string + requires map[string][]string +} + +var parsedWant = parsedDeps{ + providedBy: map[string]string{ + "goog.asserts": "asserts/asserts.js", + "goog.asserts.AssertionError": "asserts/asserts.js", + "goog.debug.Error": "debug/error.js", + "goog.string": "string/string.js", + "goog.string.Unicode": "string/string.js", + }, + requires: map[string][]string{ + "goog.asserts": []string{"goog.debug.Error", "goog.string"}, + "goog.asserts.AssertionError": []string{"goog.debug.Error", "goog.string"}, + }, +} + +var deepParsedWant = map[string][]string{ + "goog.asserts": []string{"asserts/asserts.js", "debug/error.js", "string/string.js"}, + "goog.asserts.AssertionError": []string{"asserts/asserts.js", "debug/error.js", "string/string.js"}, + "goog.debug.Error": []string{"debug/error.js"}, + "goog.string": []string{"string/string.js"}, + "goog.string.Unicode": []string{"string/string.js"}, +} + +func TestParseDeps(t *testing.T) { + providedBy, requires, err := ParseDeps(strings.NewReader(testdata)) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(parsedWant.providedBy, providedBy) { + t.Fatalf("Failed to parse closure deps: wanted %v, got %v", parsedWant.providedBy, providedBy) + } + if !reflect.DeepEqual(parsedWant.requires, requires) { + t.Fatalf("Failed to parse closure deps: wanted %v, got %v", parsedWant.requires, requires) + } +} + +func TestDeepParseDeps(t *testing.T) { + deps, err := DeepParseDeps(strings.NewReader(testdata)) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(deepParsedWant, deps) { + t.Fatalf("Failed to parse closure deps: wanted %v, got %v", deepParsedWant, deps) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/misc/closure/jstest/jstest.go b/vendor/github.com/camlistore/camlistore/pkg/misc/closure/jstest/jstest.go new file mode 100644 index 00000000..3716a313 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/misc/closure/jstest/jstest.go @@ -0,0 +1,133 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package jstest uses the Go testing package to test JavaScript code using Node and Mocha. +package jstest + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "camlistore.org/pkg/misc/closure" +) + +// checkSystemRequirements checks whether system dependencies such as node and npm are present. +func checkSystemRequirements() error { + binaries := []string{"mocha", "node", "npm"} + for _, b := range binaries { + if _, err := exec.LookPath(b); err != nil { + return fmt.Errorf("Required dependency %q not present", b) + } + } + + checkModules := func(globally bool) error { + args := []string{"list", "--depth=0"} + if globally { + args = append([]string{"-g"}, args...) + } + c := exec.Command("npm", args...) + b, _ := c.Output() + s := string(b) + modules := []string{"mocha", "assert"} + for _, m := range modules { + if !strings.Contains(s, fmt.Sprintf(" %s@", m)) { + return fmt.Errorf("Required npm module %v not present", m) + } + } + return nil + } + if err := checkModules(true); err != nil { + if err := checkModules(false); err != nil { + return err + } + } + return nil +} + +func getRepoRoot(target string) (string, error) { + dir, err := filepath.Abs(filepath.Dir(target)) + if err != nil { + return "", fmt.Errorf("Could not get working directory: %v", err) + } + for ; dir != "" && filepath.Base(dir) != "camlistore.org"; dir = filepath.Dir(dir) { + } + if dir == "" { + return "", fmt.Errorf("Could not find Camlistore repo in ancestors of %q", target) + } + return dir, nil +} + +// writeDeps runs closure.GenDeps() on targetDir and writes the resulting dependencies to a temporary file which will be used during the test run. The entries in the deps files are generated with paths relative to baseJS, which should be Closure's base.js file. +func writeDeps(baseJS, targetDir string) (string, error) { + closureBaseDir := filepath.Dir(baseJS) + depPrefix, err := filepath.Rel(closureBaseDir, targetDir) + if err != nil { + return "", fmt.Errorf("Could not compute relative path from %q to %q: %v", baseJS, targetDir, err) + } + + depPrefix += string(os.PathSeparator) + b, err := closure.GenDepsWithPath(depPrefix, http.Dir(targetDir)) + if err != nil { + return "", fmt.Errorf("GenDepsWithPath failed: %v", err) + } + depsFile, err := ioutil.TempFile("", "camlistore_closure_test_runner") + if err != nil { + return "", fmt.Errorf("Could not create temp js deps file: %v", err) + } + err = ioutil.WriteFile(depsFile.Name(), b, 0644) + if err != nil { + return "", fmt.Errorf("Could not write js deps file: %v", err) + } + return depsFile.Name(), nil +} + +// TestCwd runs all the tests in the current working directory. +func TestCwd(t *testing.T) { + err := checkSystemRequirements() + if err != nil { + t.Logf("WARNING: JavaScript unit tests could not be run due to a missing system dependency: %v.\nIf you are doing something that might affect JavaScript, you might want to fix this.", err) + t.Log(err) + t.Skip() + } + + path, err := os.Getwd() + if err != nil { + t.Fatalf("Could not determine current directory: %v.", err) + } + + repoRoot, err := getRepoRoot(path) + if err != nil { + t.Fatalf("Could not find repository root: %v", err) + } + baseJS := filepath.Join(repoRoot, "third_party", "closure", "lib", "closure", "goog", "base.js") + bootstrap := filepath.Join(filepath.Dir(baseJS), "bootstrap", "nodejs.js") + depsFile, err := writeDeps(baseJS, path) + if err != nil { + t.Fatal(err) + } + + c := exec.Command("mocha", "-r", bootstrap, "-r", depsFile, filepath.Join(path, "*test.js")) + b, err := c.CombinedOutput() + if err != nil { + t.Fatalf(string(b)) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/misc/gpgagent/gpgagent.go b/vendor/github.com/camlistore/camlistore/pkg/misc/gpgagent/gpgagent.go new file mode 100644 index 00000000..9c6ddfcf --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/misc/gpgagent/gpgagent.go @@ -0,0 +1,177 @@ +// +build !appengine + +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package gpgagent interacts with the local GPG Agent. +package gpgagent + +import ( + "bufio" + "encoding/hex" + "errors" + "fmt" + + "io" + "net" + "net/url" + "os" + "strings" +) + +// Conn is a connection to the GPG agent. +type Conn struct { + c io.ReadWriteCloser + br *bufio.Reader +} + +var ( + ErrNoAgent = errors.New("GPG_AGENT_INFO not set in environment") + ErrNoData = errors.New("GPG_ERR_NO_DATA cache miss") + ErrCancel = errors.New("gpgagent: Cancel") +) + +// NewConn connects to the GPG Agent as described in the +// GPG_AGENT_INFO environment variable. +func NewConn() (*Conn, error) { + sp := strings.SplitN(os.Getenv("GPG_AGENT_INFO"), ":", 3) + if len(sp) == 0 || len(sp[0]) == 0 { + return nil, ErrNoAgent + } + addr := &net.UnixAddr{Net: "unix", Name: sp[0]} + uc, err := net.DialUnix("unix", nil, addr) + if err != nil { + return nil, err + } + br := bufio.NewReader(uc) + lineb, err := br.ReadSlice('\n') + if err != nil { + return nil, err + } + line := string(lineb) + if !strings.HasPrefix(line, "OK") { + return nil, fmt.Errorf("gpgagent: didn't get OK; got %q", line) + } + return &Conn{uc, br}, nil +} + +func (c *Conn) Close() error { + c.br = nil + return c.c.Close() +} + +// PassphraseRequest is a request to get a passphrase from the GPG +// Agent. +type PassphraseRequest struct { + CacheKey, Error, Prompt, Desc string + + // If the option --no-ask is used and the passphrase is not in + // the cache the user will not be asked to enter a passphrase + // but the error code GPG_ERR_NO_DATA is returned. (ErrNoData) + NoAsk bool +} + +func (c *Conn) RemoveFromCache(cacheKey string) error { + _, err := fmt.Fprintf(c.c, "CLEAR_PASSPHRASE %s\n", url.QueryEscape(cacheKey)) + if err != nil { + return err + } + lineb, err := c.br.ReadSlice('\n') + if err != nil { + return err + } + line := string(lineb) + if !strings.HasPrefix(line, "OK") { + return fmt.Errorf("gpgagent: CLEAR_PASSPHRASE returned %q", line) + } + return nil +} + +func (c *Conn) GetPassphrase(pr *PassphraseRequest) (passphrase string, outerr error) { + defer func() { + if e, ok := recover().(string); ok { + passphrase = "" + outerr = errors.New(e) + } + }() + set := func(cmd string, val string) { + if val == "" { + return + } + _, err := fmt.Fprintf(c.c, "%s %s\n", cmd, val) + if err != nil { + panic("gpgagent: failed to send " + cmd) + } + line, _, err := c.br.ReadLine() + if err != nil { + panic("gpgagent: failed to read " + cmd) + } + if !strings.HasPrefix(string(line), "OK") { + panic("gpgagent: response to " + cmd + " was " + string(line)) + } + } + if d := os.Getenv("DISPLAY"); d != "" { + set("OPTION", "display="+d) + } + tty, err := os.Readlink("/proc/self/fd/0") + if err == nil { + set("OPTION", "ttyname="+tty) + } + set("OPTION", "ttytype="+os.Getenv("TERM")) + opts := "" + if pr.NoAsk { + opts += "--no-ask " + } + + encOrX := func(s string) string { + if s == "" { + return "X" + } + return url.QueryEscape(s) + } + + _, err = fmt.Fprintf(c.c, "GET_PASSPHRASE %s%s %s %s %s\n", + opts, + url.QueryEscape(pr.CacheKey), + encOrX(pr.Error), + encOrX(pr.Prompt), + encOrX(pr.Desc)) + if err != nil { + return "", err + } + lineb, err := c.br.ReadSlice('\n') + if err != nil { + return "", err + } + line := string(lineb) + if strings.HasPrefix(line, "OK ") { + decb, err := hex.DecodeString(line[3 : len(line)-1]) + if err != nil { + return "", err + } + return string(decb), nil + } + fields := strings.Split(line, " ") + if len(fields) >= 2 && fields[0] == "ERR" { + switch fields[1] { + case "67108922": + return "", ErrNoData + case "83886179": + return "", ErrCancel + } + } + return "", errors.New(line) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/misc/gpgagent/gpgagent_test.go b/vendor/github.com/camlistore/camlistore/pkg/misc/gpgagent/gpgagent_test.go new file mode 100644 index 00000000..ae0b0c23 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/misc/gpgagent/gpgagent_test.go @@ -0,0 +1,81 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gpgagent + +import ( + "fmt" + "os" + "testing" + "time" +) + +func TestPrompt(t *testing.T) { + if os.Getenv("TEST_GPGAGENT_LIB") != "1" { + t.Logf("skipping TestPrompt without $TEST_GPGAGENT_LIB == 1") + return + } + conn, err := NewConn() + if err != nil { + t.Fatal(err) + } + defer conn.Close() + req := &PassphraseRequest{ + Desc: "Type 'foo' for testing", + Error: "seriously, or I'll be an error.", + Prompt: "foo", + CacheKey: fmt.Sprintf("gpgagent_test-cachekey-%d", time.Now()), + } + s1, err := conn.GetPassphrase(req) + if err != nil { + t.Fatal(err) + } + t1 := time.Now() + s2, err := conn.GetPassphrase(req) + if err != nil { + t.Fatal(err) + } + t2 := time.Now() + if td := t2.Sub(t1); td > 1e9/5 { + t.Errorf("cached passphrase took more than 1/5 second; took %d ns", td) + } + if s1 != s2 { + t.Errorf("cached passphrase differed; got %q, want %q", s2, s1) + } + if s1 != "foo" { + t.Errorf("got passphrase %q; want %q", s1, "foo") + } + err = conn.RemoveFromCache(req.CacheKey) + if err != nil { + t.Fatal(err) + } + + req.NoAsk = true + s3, err := conn.GetPassphrase(req) + if err != ErrNoData { + t.Errorf("after remove from cache, expected gpgagent.ErrNoData, got %q, %v", s3, err) + } + + s4, err := conn.GetPassphrase(&PassphraseRequest{ + Desc: "Press Cancel for testing", + Error: "seriously, or I'll be an error.", + Prompt: "cancel!", + CacheKey: fmt.Sprintf("gpgagent_test-cachekey-%d", time.Now()), + }) + if err != ErrCancel { + t.Errorf("expected cancel, got %q, %v", s4, err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/misc/pinentry/pinentry.go b/vendor/github.com/camlistore/camlistore/pkg/misc/pinentry/pinentry.go new file mode 100644 index 00000000..1e8241a4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/misc/pinentry/pinentry.go @@ -0,0 +1,147 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package pinentry interfaces with the pinentry(1) command to securely +// prompt the user for a password using whichever user interface the +// user is currently using. +package pinentry + +import ( + "bufio" + "errors" + "fmt" + "os" + "os/exec" + "strings" +) + +// ErrCancel is returned when the user explicitly aborts the password +// request. +var ErrCancel = errors.New("pinentry: Cancel") + +// Request describes what the user should see during the request for +// their password. +type Request struct { + Desc, Prompt, OK, Cancel, Error string +} + +func catch(err *error) { + rerr := recover() + if rerr == nil { + return + } + if e, ok := rerr.(string); ok { + *err = errors.New(e) + } + if e, ok := rerr.(error); ok { + *err = e + } +} + +func check(err error) { + if err != nil { + panic(err) + } +} + +func (r *Request) GetPIN() (pin string, outerr error) { + defer catch(&outerr) + bin, err := exec.LookPath("pinentry") + if err != nil { + return r.getPINNaïve() + } + cmd := exec.Command(bin) + stdin, _ := cmd.StdinPipe() + stdout, _ := cmd.StdoutPipe() + check(cmd.Start()) + defer cmd.Wait() + defer stdin.Close() + br := bufio.NewReader(stdout) + lineb, _, err := br.ReadLine() + if err != nil { + return "", fmt.Errorf("Failed to get getpin greeting") + } + line := string(lineb) + if !strings.HasPrefix(line, "OK") { + return "", fmt.Errorf("getpin greeting said %q", line) + } + set := func(cmd string, val string) { + if val == "" { + return + } + fmt.Fprintf(stdin, "%s %s\n", cmd, val) + line, _, err := br.ReadLine() + if err != nil { + panic("Failed to " + cmd) + } + if string(line) != "OK" { + panic("Response to " + cmd + " was " + string(line)) + } + } + set("SETPROMPT", r.Prompt) + set("SETDESC", r.Desc) + set("SETOK", r.OK) + set("SETCANCEL", r.Cancel) + set("SETERROR", r.Error) + set("OPTION", "ttytype="+os.Getenv("TERM")) + tty, err := os.Readlink("/proc/self/fd/0") + if err == nil { + set("OPTION", "ttyname="+tty) + } + fmt.Fprintf(stdin, "GETPIN\n") + lineb, _, err = br.ReadLine() + if err != nil { + return "", fmt.Errorf("Failed to read line after GETPIN: %v", err) + } + line = string(lineb) + if strings.HasPrefix(line, "D ") { + return line[2:], nil + } + if strings.HasPrefix(line, "ERR 83886179 ") { + return "", ErrCancel + } + return "", fmt.Errorf("GETPIN response didn't start with D; got %q", line) +} + +func runPass(bin string, args ...string) { + cmd := exec.Command(bin, args...) + cmd.Stdout = os.Stdout + cmd.Run() +} + +func (r *Request) getPINNaïve() (string, error) { + stty, err := exec.LookPath("stty") + if err != nil { + return "", errors.New("no pinentry or stty found") + } + runPass(stty, "-echo") + defer runPass(stty, "echo") + + if r.Desc != "" { + fmt.Printf("%s\n\n", r.Desc) + } + prompt := r.Prompt + if prompt == "" { + prompt = "Password" + } + fmt.Printf("%s: ", prompt) + br := bufio.NewReader(os.Stdin) + line, _, err := br.ReadLine() + if err != nil { + return "", err + } + return string(line), nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/netutil/ident.go b/vendor/github.com/camlistore/camlistore/pkg/netutil/ident.go new file mode 100644 index 00000000..ba3f5786 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/netutil/ident.go @@ -0,0 +1,275 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package netutil identifies the system userid responsible for +// localhost TCP connections. +package netutil + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "os" + "os/exec" + "os/user" + "regexp" + "runtime" + "strconv" + "strings" +) + +var ( + ErrNotFound = errors.New("netutil: connection not found") + ErrUnsupportedOS = errors.New("netutil: not implemented on this operating system") +) + +// ConnUserid returns the uid that owns the given localhost connection. +// The returned error is ErrNotFound if the connection wasn't found. +func ConnUserid(conn net.Conn) (uid int, err error) { + return AddrPairUserid(conn.LocalAddr(), conn.RemoteAddr()) +} + +// HostPortToIP parses a host:port to a TCPAddr without resolving names. +// If given a context IP, it will resolve localhost to match the context's IP family. +func HostPortToIP(hostport string, ctx *net.TCPAddr) (hostaddr *net.TCPAddr, err error) { + host, port, err := net.SplitHostPort(hostport) + if err != nil { + return nil, err + } + iport, err := strconv.Atoi(port) + if err != nil || iport < 0 || iport > 0xFFFF { + return nil, fmt.Errorf("invalid port %d", iport) + } + var addr net.IP + if ctx != nil && host == "localhost" { + if ctx.IP.To4() != nil { + addr = net.IPv4(127, 0, 0, 1) + } else { + addr = net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + } + } else if addr = net.ParseIP(host); addr == nil { + return nil, fmt.Errorf("could not parse IP %s", host) + } + + return &net.TCPAddr{IP: addr, Port: iport}, nil +} + +// AddrPairUserid returns the local userid who owns the TCP connection +// given by the local and remote ip:port (lipport and ripport, +// respectively). Returns ErrNotFound for the error if the TCP connection +// isn't found. +func AddrPairUserid(local, remote net.Addr) (uid int, err error) { + lAddr, lOk := local.(*net.TCPAddr) + rAddr, rOk := remote.(*net.TCPAddr) + if !(lOk && rOk) { + return -1, fmt.Errorf("netutil: Could not convert Addr to TCPAddr.") + } + + localv4 := (lAddr.IP.To4() != nil) + remotev4 := (rAddr.IP.To4() != nil) + if localv4 != remotev4 { + return -1, fmt.Errorf("netutil: address pairs of different families; localv4=%v, remotev4=%v", + localv4, remotev4) + } + + switch runtime.GOOS { + case "darwin": + return uidFromLsof(lAddr.IP, lAddr.Port, rAddr.IP, rAddr.Port) + case "freebsd": + return uidFromSockstat(lAddr.IP, lAddr.Port, rAddr.IP, rAddr.Port) + case "linux": + file := "/proc/net/tcp" + if !localv4 { + file = "/proc/net/tcp6" + } + f, err := os.Open(file) + if err != nil { + return -1, fmt.Errorf("Error opening %s: %v", file, err) + } + defer f.Close() + return uidFromProcReader(lAddr.IP, lAddr.Port, rAddr.IP, rAddr.Port, f) + } + return 0, ErrUnsupportedOS +} + +func toLinuxIPv4Order(b []byte) []byte { + binary.BigEndian.PutUint32(b, binary.LittleEndian.Uint32(b)) + return b +} + +func toLinuxIPv6Order(b []byte) []byte { + for i := 0; i < 16; i += 4 { + sb := b[i : i+4] + binary.BigEndian.PutUint32(sb, binary.LittleEndian.Uint32(sb)) + } + return b +} + +type maybeBrackets net.IP + +func (p maybeBrackets) String() string { + s := net.IP(p).String() + if strings.Contains(s, ":") { + return "[" + s + "]" + } + return s +} + +// Changed by tests. +var uidFromUsername = uidFromUsernameFn + +func uidFromUsernameFn(username string) (uid int, err error) { + if uid := os.Getuid(); uid != 0 && username == os.Getenv("USER") { + return uid, nil + } + u, err := user.Lookup(username) + if err == nil { + uid, err := strconv.Atoi(u.Uid) + return uid, err + } + return 0, err +} + +func uidFromLsof(lip net.IP, lport int, rip net.IP, rport int) (uid int, err error) { + seek := fmt.Sprintf("%s:%d->%s:%d", maybeBrackets(lip), lport, maybeBrackets(rip), rport) + seekb := []byte(seek) + if _, err = exec.LookPath("lsof"); err != nil { + return + } + cmd := exec.Command("lsof", + "-b", // avoid system calls that could block + "-w", // and don't warn about cases where -b fails + "-n", // don't resolve network names + "-P", // don't resolve network ports, + // TODO(bradfitz): pass down the uid we care about, then do: ? + //"-a", // AND the following together: + // "-u", strconv.Itoa(uid) // just this uid + "-itcp") // we only care about TCP connections + stdout, err := cmd.StdoutPipe() + if err != nil { + return + } + defer cmd.Wait() + defer stdout.Close() + err = cmd.Start() + if err != nil { + return + } + defer cmd.Process.Kill() + br := bufio.NewReader(stdout) + for { + line, err := br.ReadSlice('\n') + if err == io.EOF { + break + } + if err != nil { + return -1, err + } + if !bytes.Contains(line, seekb) { + continue + } + // SystemUIS 276 bradfitz 15u IPv4 0xffffff801a7c74e0 0t0 TCP 127.0.0.1:56718->127.0.0.1:5204 (ESTABLISHED) + f := bytes.Fields(line) + if len(f) < 8 { + continue + } + username := string(f[2]) + return uidFromUsername(username) + } + return -1, ErrNotFound + +} + +func uidFromSockstat(lip net.IP, lport int, rip net.IP, rport int) (int, error) { + cmd := exec.Command("sockstat", "-Ptcp") + stdout, err := cmd.StdoutPipe() + if err != nil { + return -1, err + } + defer cmd.Wait() + defer stdout.Close() + err = cmd.Start() + if err != nil { + return -1, err + } + defer cmd.Process.Kill() + + return uidFromSockstatReader(lip, lport, rip, rport, stdout) +} + +func uidFromSockstatReader(lip net.IP, lport int, rip net.IP, rport int, r io.Reader) (int, error) { + pat, err := regexp.Compile(fmt.Sprintf(`^([^ ]+).*%s:%d *%s:%d$`, + lip.String(), lport, rip.String(), rport)) + if err != nil { + return -1, err + } + scanner := bufio.NewScanner(r) + for scanner.Scan() { + l := scanner.Text() + m := pat.FindStringSubmatch(l) + if len(m) == 2 { + return uidFromUsername(m[1]) + } + } + + if err := scanner.Err(); err != nil { + return -1, err + } + + return -1, ErrNotFound +} + +func uidFromProcReader(lip net.IP, lport int, rip net.IP, rport int, r io.Reader) (uid int, err error) { + buf := bufio.NewReader(r) + + localHex := "" + remoteHex := "" + ipv4 := lip.To4() != nil + if ipv4 { + // In the kernel, the port is run through ntohs(), and + // the inet_request_socket in + // include/net/inet_socket.h says the "loc_addr" and + // "rmt_addr" fields are __be32, but get_openreq4's + // printf of them is raw, without byte order + // converstion. + localHex = fmt.Sprintf("%08X:%04X", toLinuxIPv4Order([]byte(lip.To4())), lport) + remoteHex = fmt.Sprintf("%08X:%04X", toLinuxIPv4Order([]byte(rip.To4())), rport) + } else { + localHex = fmt.Sprintf("%032X:%04X", toLinuxIPv6Order([]byte(lip.To16())), lport) + remoteHex = fmt.Sprintf("%032X:%04X", toLinuxIPv6Order([]byte(rip.To16())), rport) + } + + for { + line, err := buf.ReadString('\n') + if err != nil { + return -1, ErrNotFound + } + parts := strings.Fields(strings.TrimSpace(line)) + if len(parts) < 8 { + continue + } + // log.Printf("parts[1] = %q; localHex = %q", parts[1], localHex) + if parts[1] == localHex && parts[2] == remoteHex { + uid, err = strconv.Atoi(parts[7]) + return uid, err + } + } + panic("unreachable") +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/netutil/ident_test.go b/vendor/github.com/camlistore/camlistore/pkg/netutil/ident_test.go new file mode 100644 index 00000000..a28f210b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/netutil/ident_test.go @@ -0,0 +1,248 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package netutil + +import ( + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "os" + "runtime" + "strings" + "testing" + "time" +) + +func TestLocalIPv4(t *testing.T) { + // Start listening on localhost IPv4, on some port. + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + testLocalListener(t, ln) +} + +func TestLocalIPv6(t *testing.T) { + ln, err := net.Listen("tcp", "[::1]:0") + if err != nil { + t.Logf("skipping IPv6 test; not supported on host machine?") + return + } + testLocalListener(t, ln) +} + +func testLocalListener(t *testing.T, ln net.Listener) { + defer ln.Close() + + // Accept a connection, run ConnUserId (what we're testing), and + // send its result on c. + type uidErr struct { + uid int + err error + } + c := make(chan uidErr, 2) + go func() { + conn, err := ln.Accept() + if err != nil { + c <- uidErr{0, err} + } + uid, err := ConnUserid(conn) + c <- uidErr{uid, err} + }() + + // Connect to our dummy server. Keep the connection open until + // the test is done. + donec := make(chan bool) + defer close(donec) + go func() { + c, err := net.Dial("tcp", ln.Addr().String()) + if err != nil { + return + } + <-donec + c.Close() + }() + + select { + case r := <-c: + if r.err != nil { + if r.err == ErrUnsupportedOS { + t.Skipf("Skipping test; not implemented on " + runtime.GOOS) + } + t.Fatal(r.err) + } + if r.uid != os.Getuid() { + t.Errorf("got uid %d; want %d", r.uid, os.Getuid()) + } + case <-time.After(3 * time.Second): + t.Fatal("timeout") + } +} + +func TestHTTPAuth(t *testing.T) { + var ts *httptest.Server + ts = httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + from, err := HostPortToIP(r.RemoteAddr, nil) + if err != nil { + t.Fatal(err) + } + to := ts.Listener.Addr() + uid, err := AddrPairUserid(from, to) + if err != nil { + fmt.Fprintf(rw, "ERR: %v", err) + return + } + fmt.Fprintf(rw, "uid=%d", uid) + })) + defer ts.Close() + res, err := http.Get(ts.URL) + if err != nil { + t.Fatal(err) + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + if g, e := string(body), fmt.Sprintf("uid=%d", os.Getuid()); g != e { + if g == "ERR: "+ErrUnsupportedOS.Error() { + t.Skipf("Skipping test; not implemented on " + runtime.GOOS) + } + t.Errorf("got body %q; want %q", g, e) + } +} + +func testUidFromUsername(username string) (int, error) { + switch username { + case "really-long-user": + return 1000, nil + case "root": + return 0, nil + } + panic("Unhandled username specified in test") +} + +func TestParseFreeBSDSockstat(t *testing.T) { + uidFromUsername = testUidFromUsername + pairs := []struct { + uid int + lip, rip net.IP + lport, rport int + }{ + { + // "really-long-user" + uid: 1000, + lip: net.ParseIP("192.168.123.5"), lport: 8000, + rip: net.ParseIP("192.168.123.21"), rport: 49826, + }, + { + // "really-long-user" + uid: 1000, + lip: net.ParseIP("192.168.123.5"), lport: 9000, + rip: net.ParseIP("192.168.123.21"), rport: 49866, + }, + { + // "root" + uid: 0, + lip: net.ParseIP("192.168.123.5"), lport: 22, + rip: net.ParseIP("192.168.123.21"), rport: 49747, + }, + } + + for _, p := range pairs { + uid, err := uidFromSockstatReader(p.lip, p.lport, p.rip, p.rport, strings.NewReader(sockstatPtcp)) + if err != nil { + t.Error(err) + } + + if p.uid != uid { + t.Error("Got", uid, "want", p.uid) + } + } +} + +func TestParseLinuxTCPStat4(t *testing.T) { + lip, lport := net.ParseIP("67.218.110.129"), 43436 + rip, rport := net.ParseIP("207.7.148.195"), 80 + + // 816EDA43:A9AC C39407CF:0050 + // 43436 80 + uid, err := uidFromProcReader(lip, lport, rip, rport, strings.NewReader(tcpstat4)) + if err != nil { + t.Error(err) + } + if e, g := 61652, uid; e != g { + t.Errorf("expected uid %d, got %d", e, g) + } +} + +var tcpstat4 = ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode +0: 0100007F:C204 00000000:0000 0A 00000000:00000000 00:00000000 00000000 61652 0 8722922 1 ffff880036b36180 300 0 0 2 -1 +1: 0100007F:0CEA 00000000:0000 0A 00000000:00000000 00:00000000 00000000 120 0 5714729 1 ffff880036b35480 300 0 0 2 -1 +2: 0100007F:2BCB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 65534 0 7381 1 ffff880136370000 300 0 0 2 -1 +3: 0100007F:13AD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 61652 0 4846349 1 ffff880123eb5480 300 0 0 2 -1 +4: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 8307 1 ffff880123eb0d00 300 0 0 2 -1 +5: 00000000:0071 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 8558503 1 ffff88001a242080 300 0 0 2 -1 6: 0100007F:7533 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 8686 1 ffff880136371380 300 0 0 2 -1 +7: 017AA8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 6015 1 ffff880123eb0680 300 0 0 2 -1 +8: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 8705543 1 ffff88001a242d80 300 0 0 2 -1 +9: 816EDA43:D4DC 35E07D4A:01BB 01 00000000:00000000 02:00000E25 00000000 61652 0 8720744 2 ffff88001a243a80 346 4 24 3 2 +10: 0100007F:C204 0100007F:D981 01 00000000:00000000 00:00000000 00000000 61652 0 8722934 1 ffff88006712a700 21 4 30 5 -1 +11: 816EDA43:A9AC C39407CF:0050 01 00000000:00000000 00:00000000 00000000 61652 0 8754873 1 ffff88006712db00 27 0 0 3 -1 +12: 816EDA43:AFEF 51357D4A:01BB 01 00000000:00000000 02:00000685 00000000 61652 0 8752937 2 ffff880136375480 87 4 2 4 -1 +13: 0100007F:D981 0100007F:C204 01 00000000:00000000 00:00000000 00000000 61652 0 8722933 1 ffff880036b30d00 21 4 0 3 -1 +` + +// Output of 'sockstat -Ptcp'. User 'really-long-user' running two instances +// of nc copied to 'really-only-process-name' and 'spc in name' run with -l +// 8000 and -l 9000 respectively. Two connections were then open from +// 192.167.123.21 using 'nc 192.168.123.5 8000' and 'nc 192.168.123.5 9000'. +var sockstatPtcp = ` +sockstat -Ptcp +USER COMMAND PID FD PROTO LOCAL ADDRESS FOREIGN ADDRESS +really-long-user spc in nam63210 3 tcp4 *:9000 *:* +really-long-user spc in nam63210 4 tcp4 192.168.123.5:9000192.168.123.21:49866 +www nginx 62982 7 tcp4 *:80 *:* +www nginx 62982 8 tcp6 *:80 *:* +really-long-user really-lon62928 3 tcp4 *:8000 *:* +really-long-user really-lon62928 4 tcp4 192.168.123.5:8000192.168.123.21:49826 +root sshd 62849 5 tcp4 192.168.123.5:22 192.168.123.21:49819 +root sshd 61819 5 tcp4 192.168.123.5:22 192.168.123.21:49747 +camlistore sshd 61746 5 tcp4 192.168.123.5:22 192.168.123.21:49739 +root sshd 61744 5 tcp4 192.168.123.5:22 192.168.123.21:49739 +camlistore camlistore10941 7 tcp4 6 *:3179 *:* +camlistore sshd 91620 5 tcp4 192.168.123.5:22 192.168.123.2:13404 +root sshd 91618 5 tcp4 192.168.123.5:22 192.168.123.2:13404 +root sshd 2309 4 tcp6 *:22 *:* +root sshd 2309 5 tcp4 *:22 *:* +root nginx 2152 7 tcp4 *:80 *:* +root nginx 2152 8 tcp6 *:80 *:* +root python2.7 2076 3 tcp4 127.0.0.1:9042 *:* +root python2.7 2076 6 tcp4 127.0.0.1:9042 127.0.0.1:51930 +root python2.7 2076 7 tcp4 127.0.0.1:9042 127.0.0.1:20433 +root python2.7 2076 8 tcp4 127.0.0.1:9042 127.0.0.1:55807 +root rpc.statd 1630 5 tcp6 *:664 *:* +root rpc.statd 1630 7 tcp4 *:664 *:* +root nfsd 1618 5 tcp4 *:2049 *:* +root nfsd 1618 6 tcp6 *:2049 *:* +root mountd 1604 6 tcp6 *:792 *:* +root mountd 1604 8 tcp4 *:792 *:* +root rpcbind 1600 8 tcp6 *:111 *:* +root rpcbind 1600 11 tcp4 *:111 *:* +? ? ? ? tcp4 *:895 *:* +? ? ? ? tcp6 *:777 *:* +` diff --git a/vendor/github.com/camlistore/camlistore/pkg/netutil/netutil.go b/vendor/github.com/camlistore/camlistore/pkg/netutil/netutil.go new file mode 100644 index 00000000..b7bfb2cc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/netutil/netutil.go @@ -0,0 +1,123 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package netutil + +import ( + "errors" + "fmt" + "net" + "net/url" + "strings" + "time" +) + +// AwaitReachable tries to make a TCP connection to addr regularly. +// It returns an error if it's unable to make a connection before maxWait. +func AwaitReachable(addr string, maxWait time.Duration) error { + done := time.Now().Add(maxWait) + for time.Now().Before(done) { + c, err := net.Dial("tcp", addr) + if err == nil { + c.Close() + return nil + } + time.Sleep(100 * time.Millisecond) + } + return fmt.Errorf("%v unreachable for %v", addr, maxWait) +} + +// HostPort takes a urlStr string URL, and returns a host:port string suitable +// to passing to net.Dial, with the port set as the scheme's default port if +// absent. +func HostPort(urlStr string) (string, error) { + u, err := url.Parse(urlStr) + if err != nil { + return "", fmt.Errorf("could not parse %q as a url: %v", urlStr, err) + } + if u.Scheme == "" { + return "", fmt.Errorf("url %q has no scheme", urlStr) + } + hostPort := u.Host + if hostPort == "" || strings.HasPrefix(hostPort, ":") { + return "", fmt.Errorf("url %q has no host", urlStr) + } + idx := strings.Index(hostPort, "]") + if idx == -1 { + idx = 0 + } + if !strings.Contains(hostPort[idx:], ":") { + if u.Scheme == "https" { + hostPort += ":443" + } else { + hostPort += ":80" + } + } + return hostPort, nil +} + +// ListenOnLocalRandomPort returns a TCP listener on a random +// localhost port. +func ListenOnLocalRandomPort() (net.Listener, error) { + ip, err := Localhost() + if err != nil { + return nil, err + } + return net.ListenTCP("tcp", &net.TCPAddr{IP: ip, Port: 0}) +} + +// Localhost returns the first address found when +// doing a lookup of "localhost". If not successful, +// it looks for an ip on the loopback interfaces. +func Localhost() (net.IP, error) { + if ip := localhostLookup(); ip != nil { + return ip, nil + } + if ip := loopbackIP(); ip != nil { + return ip, nil + } + return nil, errors.New("No loopback ip found.") +} + +// localhostLookup looks for a loopback IP by resolving localhost. +func localhostLookup() net.IP { + if ips, err := net.LookupIP("localhost"); err == nil && len(ips) > 0 { + return ips[0] + } + return nil +} + +// loopbackIP returns the first loopback IP address sniffing network +// interfaces or nil if none is found. +func loopbackIP() net.IP { + interfaces, err := net.Interfaces() + if err != nil { + return nil + } + for _, inf := range interfaces { + const flagUpLoopback = net.FlagUp | net.FlagLoopback + if inf.Flags&flagUpLoopback == flagUpLoopback { + addrs, _ := inf.Addrs() + for _, addr := range addrs { + ip, _, err := net.ParseCIDR(addr.String()) + if err == nil && ip.IsLoopback() { + return ip + } + } + } + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/netutil/netutil_test.go b/vendor/github.com/camlistore/camlistore/pkg/netutil/netutil_test.go new file mode 100644 index 00000000..644d7eb6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/netutil/netutil_test.go @@ -0,0 +1,181 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package netutil + +import ( + "net" + "strconv" + "testing" +) + +func TestHostPort(t *testing.T) { + tests := []struct { + baseURL string + wantNetAddr string + }{ + // IPv4, no prefix + { + baseURL: "http://foo.com/", + wantNetAddr: "foo.com:80", + }, + + { + baseURL: "https://foo.com/", + wantNetAddr: "foo.com:443", + }, + + { + baseURL: "http://foo.com:8080/", + wantNetAddr: "foo.com:8080", + }, + + { + baseURL: "https://foo.com:8080/", + wantNetAddr: "foo.com:8080", + }, + + // IPv4, with prefix + { + baseURL: "http://foo.com/pics/", + wantNetAddr: "foo.com:80", + }, + + { + baseURL: "https://foo.com/pics/", + wantNetAddr: "foo.com:443", + }, + + { + baseURL: "http://foo.com:8080/pics/", + wantNetAddr: "foo.com:8080", + }, + + { + baseURL: "https://foo.com:8080/pics/", + wantNetAddr: "foo.com:8080", + }, + + // IPv6, no prefix + { + baseURL: "http://[::1]/", + wantNetAddr: "[::1]:80", + }, + + { + baseURL: "https://[::1]/", + wantNetAddr: "[::1]:443", + }, + + { + baseURL: "http://[::1]:8080/", + wantNetAddr: "[::1]:8080", + }, + + { + baseURL: "https://[::1]:8080/", + wantNetAddr: "[::1]:8080", + }, + + // IPv6, with prefix + { + baseURL: "http://[::1]/pics/", + wantNetAddr: "[::1]:80", + }, + + { + baseURL: "https://[::1]/pics/", + wantNetAddr: "[::1]:443", + }, + + { + baseURL: "http://[::1]:8080/pics/", + wantNetAddr: "[::1]:8080", + }, + + { + baseURL: "https://[::1]:8080/pics/", + wantNetAddr: "[::1]:8080", + }, + } + for _, v := range tests { + got, err := HostPort(v.baseURL) + if err != nil { + t.Error(err) + continue + } + if got != v.wantNetAddr { + t.Errorf("got: %v for %v, want: %v", got, v.baseURL, v.wantNetAddr) + } + } +} + +func testLocalhostResolver(t *testing.T, resolve func() net.IP) { + ip := resolve() + if ip == nil { + t.Fatal("no ip found.") + } + if !ip.IsLoopback() { + t.Errorf("expected a loopback address: %s", ip) + } +} + +func testLocalhost(t *testing.T) { + testLocalhostResolver(t, localhostLookup) +} + +func testLoopbackIp(t *testing.T) { + testLocalhostResolver(t, loopbackIP) +} + +func TestLocalhost(t *testing.T) { + _, err := Localhost() + if err != nil { + t.Fatal(err) + } +} + +func TestListenOnLocalRandomPort(t *testing.T) { + l, err := ListenOnLocalRandomPort() + if err != nil { + t.Fatalf("unexpected error %v", err) + } + defer l.Close() + + _, port, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + t.Fatal(err) + } + if p, _ := strconv.Atoi(port); p < 1 { + t.Fatalf("expected port(%d) to be > 0", p) + } +} + +func BenchmarkLocalhostLookup(b *testing.B) { + for i := 0; i < b.N; i++ { + if ip := localhostLookup(); ip == nil { + b.Fatal("no ip found.") + } + } +} + +func BenchmarkLoopbackIP(b *testing.B) { + for i := 0; i < b.N; i++ { + if ip := loopbackIP(); ip == nil { + b.Fatal("no ip found.") + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/oauthutil/oauth.go b/vendor/github.com/camlistore/camlistore/pkg/oauthutil/oauth.go new file mode 100644 index 00000000..48dc9177 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/oauthutil/oauth.go @@ -0,0 +1,121 @@ +/* +Copyright 2015 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oauthutil + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "camlistore.org/pkg/wkfs" + + "golang.org/x/oauth2" +) + +// TitleBarRedirectURL is the OAuth2 redirect URL to use when the authorization +// code should be returned in the title bar of the browser, with the page text +// prompting the user to copy the code and paste it in the application. +const TitleBarRedirectURL = "urn:ietf:wg:oauth:2.0:oob" + +// ErrNoAuthCode is returned when Token() has not found any valid cached token +// and TokenSource does not have an AuthCode for getting a new token. +var ErrNoAuthCode = errors.New("oauthutil: unspecified TokenSource.AuthCode") + +// TokenSource is an implementation of oauth2.TokenSource. It uses CacheFile to store and +// reuse the the acquired token, and AuthCode to provide the authorization code that will be +// exchanged for a token otherwise. +type TokenSource struct { + Config *oauth2.Config + + // CacheFile is where the token will be stored JSON-encoded. Any call to Token + // first tries to read a valid token from CacheFile. + CacheFile string + + // AuthCode provides the authorization code that Token will exchange for a token. + // It usually is a way to prompt the user for the code. If CacheFile does not provide + // a token and AuthCode is nil, Token returns ErrNoAuthCode. + AuthCode func() string +} + +var errExpiredToken = errors.New("expired token") + +// cachedToken returns the token saved in cacheFile. It specifically returns +// errTokenExpired if the token is expired. +func cachedToken(cacheFile string) (*oauth2.Token, error) { + tok := new(oauth2.Token) + tokenData, err := wkfs.ReadFile(cacheFile) + if err != nil { + return nil, err + } + if err = json.Unmarshal(tokenData, tok); err != nil { + return nil, err + } + if !tok.Valid() { + if tok != nil && time.Now().After(tok.Expiry) { + return nil, errExpiredToken + } + return nil, errors.New("invalid token") + } + return tok, nil +} + +// Token first tries to find a valid token in CacheFile, and otherwise uses +// Config and AuthCode to fetch a new token. This new token is saved in CacheFile +// (if not blank). If CacheFile did not provide a token and AuthCode is nil, +// ErrNoAuthCode is returned. +func (src TokenSource) Token() (*oauth2.Token, error) { + var tok *oauth2.Token + var err error + if src.CacheFile != "" { + tok, err = cachedToken(src.CacheFile) + if err == nil { + return tok, nil + } + if err != errExpiredToken { + fmt.Printf("Error getting token from %s: %v\n", src.CacheFile, err) + } + } + if src.AuthCode == nil { + return nil, ErrNoAuthCode + } + tok, err = src.Config.Exchange(oauth2.NoContext, src.AuthCode()) + if err != nil { + return nil, fmt.Errorf("could not exchange auth code for a token: %v", err) + } + if src.CacheFile == "" { + return tok, nil + } + tokenData, err := json.Marshal(&tok) + if err != nil { + return nil, fmt.Errorf("could not encode token as json: %v", err) + } + if err := wkfs.WriteFile(src.CacheFile, tokenData, 0600); err != nil { + return nil, fmt.Errorf("could not cache token in %v: %v", src.CacheFile, err) + } + return tok, nil +} + +// NewRefreshTokenSource returns a token source that obtains its initial token +// based on the provided config and the refresh token. +func NewRefreshTokenSource(config *oauth2.Config, refreshToken string) oauth2.TokenSource { + var noInitialToken *oauth2.Token = nil + return oauth2.ReuseTokenSource(noInitialToken, config.TokenSource( + oauth2.NoContext, // TODO: maybe accept a context later. + &oauth2.Token{RefreshToken: refreshToken}, + )) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/cpu.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/cpu.go new file mode 100644 index 00000000..48eca43b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/cpu.go @@ -0,0 +1,30 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import "time" + +var cpuUsage func() time.Duration + +// CPUUsage returns how much cumulative user CPU time the process has +// used. On unsupported operating systems, it returns zero. +func CPUUsage() time.Duration { + if f := cpuUsage; f != nil { + return f() + } + return 0 +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/cpu_freebsd.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/cpu_freebsd.go new file mode 100644 index 00000000..b80f6b30 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/cpu_freebsd.go @@ -0,0 +1,32 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "syscall" + "time" +) + +func init() { + cpuUsage = cpuFreeBSD +} + +func cpuFreeBSD() time.Duration { + var ru syscall.Rusage + syscall.Getrusage(0, &ru) + return time.Duration(ru.Utime.Nano()) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/cpu_linux.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/cpu_linux.go new file mode 100644 index 00000000..f8acb66c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/cpu_linux.go @@ -0,0 +1,34 @@ +// +build linux,!appengine + +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "syscall" + "time" +) + +func init() { + cpuUsage = cpuLinux +} + +func cpuLinux() time.Duration { + var ru syscall.Rusage + syscall.Getrusage(0, &ru) + return time.Duration(ru.Utime.Nano()) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/findproc_appengine.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/findproc_appengine.go new file mode 100644 index 00000000..4da511c4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/findproc_appengine.go @@ -0,0 +1,29 @@ +// +build appengine + +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "log" +) + +func DieOnParentDeath() { + // TODO(mpl): maybe the way it's done in findproc_normal.go actually works + // on appengine too? Verify that. + log.Fatal("DieOnParentDeath not implemented on appengine.") +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/findproc_normal.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/findproc_normal.go new file mode 100644 index 00000000..abb8e4d6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/findproc_normal.go @@ -0,0 +1,43 @@ +// +build !appengine + +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "os" + "time" +) + +// DieOnParentDeath starts a goroutine that regularly checks that +// the current process can find its parent, and calls os.Exit(0) +// as soon as it cannot. +func DieOnParentDeath() { + // TODO: on Linux, use PR_SET_PDEATHSIG later. For now, the portable way: + go func() { + pollParent(30 * time.Second) + os.Exit(0) + }() +} + +// pollParent checks every t that the ppid of the current +// process has not changed (i.e that the process has not +// been orphaned). It returns as soon as that ppid changes. +func pollParent(t time.Duration) { + for initial := os.Getppid(); initial == os.Getppid(); time.Sleep(t) { + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/gce/gce.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/gce/gce.go new file mode 100644 index 00000000..64850b55 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/gce/gce.go @@ -0,0 +1,98 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package gce configures hooks for running Camlistore for Google Compute Engine. +package gce + +import ( + "errors" + "fmt" + "io" + "log" + "os" + "path" + "strings" + + "camlistore.org/pkg/env" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/osutil" + _ "camlistore.org/pkg/wkfs/gcs" + "golang.org/x/net/context" + + "google.golang.org/cloud/compute/metadata" + "google.golang.org/cloud/logging" +) + +func init() { + if !env.OnGCE() { + return + } + osutil.RegisterConfigDirFunc(func() string { + v, _ := metadata.InstanceAttributeValue("camlistore-config-dir") + if v == "" { + return v + } + return path.Clean("/gcs/" + strings.TrimPrefix(v, "gs://")) + }) + jsonconfig.RegisterFunc("_gce_instance_meta", func(c *jsonconfig.ConfigParser, v []interface{}) (interface{}, error) { + if len(v) != 1 { + return nil, errors.New("only 1 argument supported after _gce_instance_meta") + } + attr, ok := v[0].(string) + if !ok { + return nil, errors.New("expected argument after _gce_instance_meta to be a string") + } + val, err := metadata.InstanceAttributeValue(attr) + if err != nil { + return nil, fmt.Errorf("error reading GCE instance attribute %q: %v", attr, err) + } + return val, nil + }) +} + +// LogWriter returns an environment-specific io.Writer suitable for passing +// to log.SetOutput. It will also include writing to os.Stderr as well. +func LogWriter() (w io.Writer) { + w = os.Stderr + if !env.OnGCE() { + return + } + projID, err := metadata.ProjectID() + if projID == "" { + log.Printf("Error getting project ID: %v", err) + return + } + scopes, _ := metadata.Scopes("default") + haveScope := func(scope string) bool { + for _, x := range scopes { + if x == scope { + return true + } + } + return false + } + if !haveScope(logging.Scope) { + log.Printf("when this Google Compute Engine VM instance was created, it wasn't granted enough access to use Google Cloud Logging (Scope URL: %v).", logging.Scope) + return + } + + logc, err := logging.NewClient(context.Background(), projID, "camlistored-stderr") + if err != nil { + log.Printf("Error creating Google logging client: %v", err) + return + } + return io.MultiWriter(w, logc.Writer(logging.Debug)) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/mem.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/mem.go new file mode 100644 index 00000000..c00ed2d1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/mem.go @@ -0,0 +1,28 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +var memUsage func() int64 + +// MemUsage returns the number of bytes used by the process. +// On unsupported operating systems, it returns zero. +func MemUsage() int64 { + if f := memUsage; f != nil { + return f() + } + return 0 +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/mem_unix.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/mem_unix.go new file mode 100644 index 00000000..743f23b0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/mem_unix.go @@ -0,0 +1,39 @@ +// +build linux,!appengine darwin freebsd + +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "runtime" + "syscall" +) + +func init() { + memUsage = memUnix +} + +func memUnix() int64 { + var ru syscall.Rusage + syscall.Getrusage(0, &ru) + if runtime.GOOS == "linux" { + // in KB + return int64(ru.Maxrss) << 10 + } + // In bytes: + return int64(ru.Maxrss) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/openurl.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/openurl.go new file mode 100644 index 00000000..e33b0f6d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/openurl.go @@ -0,0 +1,34 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "os/exec" + "runtime" +) + +func OpenURL(url string) error { + if runtime.GOOS == "windows" { + return exec.Command("cmd.exe", "/C", "start "+url).Run() + } + + if runtime.GOOS == "darwin" { + return exec.Command("open", url).Run() + } + + return exec.Command("xdg-open", url).Run() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/osutil.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/osutil.go new file mode 100644 index 00000000..dac0c197 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/osutil.go @@ -0,0 +1,36 @@ +/* +Copyright 2014 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package osutil provides operating system-specific path information, +// and other utility functions. +package osutil + +import ( + "errors" + "os" +) + +// ErrNotSupported is returned by functions (like Mkfifo and Mksocket) +// when the underlying operating system or environment doesn't support +// the operation. +var ErrNotSupported = errors.New("operation not supported") + +// DirExists reports whether dir exists. Errors are ignored and are +// reported as false. +func DirExists(dir string) bool { + fi, err := os.Stat(dir) + return err == nil && fi.IsDir() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/paths.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/paths.go new file mode 100644 index 00000000..1358e2ec --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/paths.go @@ -0,0 +1,283 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "flag" + "log" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + + "camlistore.org/pkg/buildinfo" +) + +// HomeDir returns the path to the user's home directory. +// It returns the empty string if the value isn't known. +func HomeDir() string { + failInTests() + if runtime.GOOS == "windows" { + return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + } + return os.Getenv("HOME") +} + +// Username returns the current user's username, as +// reported by the relevant environment variable. +func Username() string { + if runtime.GOOS == "windows" { + return os.Getenv("USERNAME") + } + return os.Getenv("USER") +} + +var cacheDirOnce sync.Once + +func CacheDir() string { + cacheDirOnce.Do(makeCacheDir) + return cacheDir() +} + +func cacheDir() string { + if d := os.Getenv("CAMLI_CACHE_DIR"); d != "" { + return d + } + failInTests() + switch runtime.GOOS { + case "darwin": + return filepath.Join(HomeDir(), "Library", "Caches", "Camlistore") + case "windows": + // Per http://technet.microsoft.com/en-us/library/cc749104(v=ws.10).aspx + // these should both exist. But that page overwhelms me. Just try them + // both. This seems to work. + for _, ev := range []string{"TEMP", "TMP"} { + if v := os.Getenv(ev); v != "" { + return filepath.Join(v, "Camlistore") + } + } + panic("No Windows TEMP or TMP environment variables found; please file a bug report.") + } + if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" { + return filepath.Join(xdg, "camlistore") + } + return filepath.Join(HomeDir(), ".cache", "camlistore") +} + +func makeCacheDir() { + err := os.MkdirAll(cacheDir(), 0700) + if err != nil { + log.Fatalf("Could not create cacheDir %v: %v", cacheDir(), err) + } +} + +func CamliVarDir() string { + if d := os.Getenv("CAMLI_VAR_DIR"); d != "" { + return d + } + failInTests() + switch runtime.GOOS { + case "windows": + return filepath.Join(os.Getenv("APPDATA"), "Camlistore") + case "darwin": + return filepath.Join(HomeDir(), "Library", "Camlistore") + } + return filepath.Join(HomeDir(), "var", "camlistore") +} + +func CamliBlobRoot() string { + return filepath.Join(CamliVarDir(), "blobs") +} + +// RegisterConfigDirFunc registers a func f to return the Camlistore configuration directory. +// It may skip by returning the empty string. +func RegisterConfigDirFunc(f func() string) { + configDirFuncs = append(configDirFuncs, f) +} + +var configDirFuncs []func() string + +func CamliConfigDir() string { + if p := os.Getenv("CAMLI_CONFIG_DIR"); p != "" { + return p + } + for _, f := range configDirFuncs { + if v := f(); v != "" { + return v + } + } + + failInTests() + return camliConfigDir() +} + +func camliConfigDir() string { + if runtime.GOOS == "windows" { + return filepath.Join(os.Getenv("APPDATA"), "Camlistore") + } + if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { + return filepath.Join(xdg, "camlistore") + } + return filepath.Join(HomeDir(), ".config", "camlistore") +} + +func UserServerConfigPath() string { + return filepath.Join(CamliConfigDir(), "server-config.json") +} + +func UserClientConfigPath() string { + return filepath.Join(CamliConfigDir(), "client-config.json") +} + +// If set, flagSecretRing overrides the JSON config file +// ~/.config/camlistore/client-config.json +// (i.e. UserClientConfigPath()) "identitySecretRing" key. +var ( + flagSecretRing string + secretRingFlagAdded bool +) + +func AddSecretRingFlag() { + flag.StringVar(&flagSecretRing, "secret-keyring", "", "GnuPG secret keyring file to use.") + secretRingFlagAdded = true +} + +// ExplicitSecretRingFile returns the path to the user's GPG secret ring +// file and true if it was ever set through the --secret-keyring flag or +// the CAMLI_SECRET_RING var. It returns "", false otherwise. +// Use of this function requires the program to call AddSecretRingFlag, +// and before flag.Parse is called. +func ExplicitSecretRingFile() (string, bool) { + if !secretRingFlagAdded { + panic("proper use of ExplicitSecretRingFile requires exposing flagSecretRing with AddSecretRingFlag") + } + if flagSecretRing != "" { + return flagSecretRing, true + } + if e := os.Getenv("CAMLI_SECRET_RING"); e != "" { + return e, true + } + return "", false +} + +// DefaultSecretRingFile returns the path to the default GPG secret +// keyring. It is not influenced by any flag or CAMLI* env var. +func DefaultSecretRingFile() string { + return filepath.Join(camliConfigDir(), "identity-secring.gpg") +} + +// identitySecretRing returns the path to the default GPG +// secret keyring. It is still affected by CAMLI_CONFIG_DIR. +func identitySecretRing() string { + return filepath.Join(CamliConfigDir(), "identity-secring.gpg") +} + +// SecretRingFile returns the path to the user's GPG secret ring file. +// The value comes from either the --secret-keyring flag (if previously +// registered with AddSecretRingFlag), or the CAMLI_SECRET_RING environment +// variable, or the operating system default location. +func SecretRingFile() string { + if flagSecretRing != "" { + return flagSecretRing + } + if e := os.Getenv("CAMLI_SECRET_RING"); e != "" { + return e + } + return identitySecretRing() +} + +// DefaultTLSCert returns the path to the default TLS certificate +// file that is used (creating if necessary) when TLS is specified +// without the cert file. +func DefaultTLSCert() string { + return filepath.Join(CamliConfigDir(), "tls.crt") +} + +// DefaultTLSKey returns the path to the default TLS key +// file that is used (creating if necessary) when TLS is specified +// without the key file. +func DefaultTLSKey() string { + return filepath.Join(CamliConfigDir(), "tls.key") +} + +// Find the correct absolute path corresponding to a relative path, +// searching the following sequence of directories: +// 1. Working Directory +// 2. CAMLI_CONFIG_DIR (deprecated, will complain if this is on env) +// 3. (windows only) APPDATA/camli +// 4. All directories in CAMLI_INCLUDE_PATH (standard PATH form for OS) +func FindCamliInclude(configFile string) (absPath string, err error) { + // Try to open as absolute / relative to CWD + _, err = os.Stat(configFile) + if err == nil { + return configFile, nil + } + if filepath.IsAbs(configFile) { + // End of the line for absolute path + return "", err + } + + // Try the config dir + configDir := CamliConfigDir() + if _, err = os.Stat(filepath.Join(configDir, configFile)); err == nil { + return filepath.Join(configDir, configFile), nil + } + + // Finally, search CAMLI_INCLUDE_PATH + p := os.Getenv("CAMLI_INCLUDE_PATH") + for _, d := range strings.Split(p, string(filepath.ListSeparator)) { + if _, err = os.Stat(filepath.Join(d, configFile)); err == nil { + return filepath.Join(d, configFile), nil + } + } + + return "", os.ErrNotExist +} + +// GoPackagePath returns the path to the provided Go package's +// source directory. +// pkg may be a path prefix without any *.go files. +// The error is os.ErrNotExist if GOPATH is unset or the directory +// doesn't exist in any GOPATH component. +func GoPackagePath(pkg string) (path string, err error) { + gp := os.Getenv("GOPATH") + if gp == "" { + return path, os.ErrNotExist + } + for _, p := range filepath.SplitList(gp) { + dir := filepath.Join(p, "src", filepath.FromSlash(pkg)) + fi, err := os.Stat(dir) + if os.IsNotExist(err) { + continue + } + if err != nil { + return "", err + } + if !fi.IsDir() { + continue + } + return dir, nil + } + return path, os.ErrNotExist +} + +func failInTests() { + if buildinfo.TestingLinked() { + panic("Unexpected non-hermetic use of host configuration during testing. (alternatively: the 'testing' package got accidentally linked in)") + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/paths_test.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/paths_test.go new file mode 100644 index 00000000..00c5ee6d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/paths_test.go @@ -0,0 +1,121 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "fmt" + "os" + "path/filepath" + "testing" +) + +// Creates a file with the content "test" at path +func createTestInclude(path string) error { + // Create a config file for OpenCamliInclude to play with + cf, e := os.Create(path) + if e != nil { + return e + } + fmt.Fprintf(cf, "test") + return cf.Close() +} + +// Calls OpenCamliInclude to open path, and checks that it containts "test" +func checkOpen(t *testing.T, path string) { + found, e := FindCamliInclude(path) + if e != nil { + t.Errorf("Failed to find %v", path) + return + } + var file *os.File + file, e = os.Open(found) + if e != nil { + t.Errorf("Failed to open %v", path) + } else { + var d [10]byte + if n, _ := file.Read(d[:]); n != 4 { + t.Errorf("Read incorrect number of chars from test.config, wrong file?") + } + if string(d[0:4]) != "test" { + t.Errorf("Wrong test file content: %v", string(d[0:4])) + } + file.Close() + } +} + +// Test for error when file doesn't exist +func TestOpenCamliIncludeNoFile(t *testing.T) { + // Test that error occurs if no such file + const notExist = "this_config_doesnt_exist.config" + + defer os.Setenv("CAMLI_CONFIG_DIR", os.Getenv("CAMLI_CONFIG_DIR")) + os.Setenv("CAMLI_CONFIG_DIR", filepath.Join(os.TempDir(), "/x/y/z/not-exist")) + + _, e := FindCamliInclude(notExist) + if e == nil { + t.Errorf("Successfully opened config which doesn't exist: %v", notExist) + } +} + +// Test for when a file exists in CWD +func TestOpenCamliIncludeCWD(t *testing.T) { + const path string = "TestOpenCamliIncludeCWD.config" + if e := createTestInclude(path); e != nil { + t.Errorf("Couldn't create test config file, aborting test: %v", e) + return + } + defer os.Remove(path) + + checkOpen(t, path) +} + +// Test for when a file exists in CAMLI_CONFIG_DIR +func TestOpenCamliIncludeDir(t *testing.T) { + const name string = "TestOpenCamliIncludeDir.config" + if e := createTestInclude("/tmp/" + name); e != nil { + t.Errorf("Couldn't create test config file, aborting test: %v", e) + return + } + defer os.Remove("/tmp/" + name) + os.Setenv("CAMLI_CONFIG_DIR", "/tmp") + defer os.Setenv("CAMLI_CONFIG_DIR", "") + + checkOpen(t, name) +} + +// Test for when a file exits in CAMLI_INCLUDE_PATH +func TestOpenCamliIncludePath(t *testing.T) { + const name string = "TestOpenCamliIncludePath.config" + if e := createTestInclude("/tmp/" + name); e != nil { + t.Errorf("Couldn't create test config file, aborting test: %v", e) + return + } + defer os.Remove("/tmp/" + name) + defer os.Setenv("CAMLI_INCLUDE_PATH", "") + + defer os.Setenv("CAMLI_CONFIG_DIR", os.Getenv("CAMLI_CONFIG_DIR")) + os.Setenv("CAMLI_CONFIG_DIR", filepath.Join(os.TempDir(), "/x/y/z/not-exist")) + + os.Setenv("CAMLI_INCLUDE_PATH", "/tmp") + checkOpen(t, name) + + os.Setenv("CAMLI_INCLUDE_PATH", "/not/a/camli/config/dir:/tmp") + checkOpen(t, name) + + os.Setenv("CAMLI_INCLUDE_PATH", "/not/a/camli/config/dir:/tmp:/another/fake/camli/dir") + checkOpen(t, name) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/restart_freebsd.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/restart_freebsd.go new file mode 100644 index 00000000..835dca70 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/restart_freebsd.go @@ -0,0 +1,51 @@ +// +build freebsd + +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "syscall" + "unsafe" +) + +func init() { + osSelfPath = selfPathFreeBSD +} + +func selfPathFreeBSD() (string, error) { + mib := [4]int32{1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1} + + n := uintptr(0) + // get length + _, _, err := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0) + if err != 0 { + return "", err + } + if n == 0 { // shouldn't happen + return "", nil + } + buf := make([]byte, n) + _, _, err = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0) + if err != 0 { + return "", err + } + if n == 0 { // shouldn't happen + return "", nil + } + return string(buf[:n-1]), nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/restart_stub.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/restart_stub.go new file mode 100644 index 00000000..cfb51a6a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/restart_stub.go @@ -0,0 +1,39 @@ +// +build appengine + +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "errors" + "log" + "runtime" +) + +// SelfPath returns the path of the executable for the currently running +// process. +func SelfPath() (string, error) { + return "", errors.New("SelfPath not implemented on App Engine.") +} + +// RestartProcess returns an error if things couldn't be +// restarted. On success, this function never returns +// because the process becomes the new process. +func RestartProcess() error { + log.Print("RestartProcess not implemented on this platform.") + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/restart_unix.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/restart_unix.go new file mode 100644 index 00000000..84d5a5cb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/restart_unix.go @@ -0,0 +1,68 @@ +// +build !appengine +// +build linux darwin freebsd netbsd openbsd solaris + +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "errors" + "fmt" + "os" + "os/exec" + "runtime" + "syscall" +) + +// if non-nil, osSelfPath is used from selfPath. +var osSelfPath func() (string, error) + +// TODO(mpl): document the symlink behaviour in SelfPath for the BSDs when +// I know for sure. + +// SelfPath returns the path of the executable for the currently running +// process. At least on linux, the returned path is a symlink to the actual +// executable. +func SelfPath() (string, error) { + if f := osSelfPath; f != nil { + return f() + } + switch runtime.GOOS { + case "linux": + return "/proc/self/exe", nil + case "netbsd": + return "/proc/curproc/exe", nil + case "openbsd": + return "/proc/curproc/file", nil + case "darwin": + // TODO(mpl): maybe do the right thing for darwin too, but that may require changes to runtime. + // See https://codereview.appspot.com/6736069/ + return exec.LookPath(os.Args[0]) + } + return "", errors.New("SelfPath not implemented for " + runtime.GOOS) +} + +// RestartProcess returns an error if things couldn't be +// restarted. On success, this function never returns +// because the process becomes the new process. +func RestartProcess() error { + path, err := SelfPath() + if err != nil { + return fmt.Errorf("RestartProcess failed: %v", err) + } + return syscall.Exec(path, os.Args, os.Environ()) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/restart_windows.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/restart_windows.go new file mode 100644 index 00000000..3363ad07 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/restart_windows.go @@ -0,0 +1,54 @@ +// +build windows + +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "log" + "syscall" + "unicode/utf16" + "unsafe" +) + +// SelfPath returns the path of the executable for the currently running +// process. +func SelfPath() (string, error) { + kernel32, err := syscall.LoadDLL("kernel32.dll") + if err != nil { + return "", err + } + sysproc, err := kernel32.FindProc("GetModuleFileNameW") + if err != nil { + return "", err + } + b := make([]uint16, syscall.MAX_PATH) + r, _, err := sysproc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(len(b))) + n := uint32(r) + if n == 0 { + return "", err + } + return string(utf16.Decode(b[0:n])), nil +} + +// RestartProcess returns an error if things couldn't be +// restarted. On success, this function never returns +// because the process becomes the new process. +func RestartProcess() error { + log.Print("RestartProcess not implemented on this platform.") + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_appengine.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_appengine.go new file mode 100644 index 00000000..2f0181ae --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_appengine.go @@ -0,0 +1,22 @@ +// +build appengine + +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +func Mkfifo(path string, mode uint32) error { return ErrNotSupported } +func Mksocket(path string) error { return ErrNotSupported } diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_posix.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_posix.go new file mode 100644 index 00000000..726397d3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_posix.go @@ -0,0 +1,52 @@ +// +build !windows,!appengine,!solaris + +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "net" + "os" + "path/filepath" + "syscall" +) + +func Mkfifo(path string, mode uint32) error { + return syscall.Mkfifo(path, mode) +} + +// Mksocket creates a socket file (a Unix Domain Socket) named path. +func Mksocket(path string) error { + dir := filepath.Dir(path) + base := filepath.Base(path) + tmp := filepath.Join(dir, "."+base) + l, err := net.ListenUnix("unix", &net.UnixAddr{tmp, "unix"}) + if err != nil { + return err + } + + err = os.Rename(tmp, path) + if err != nil { + l.Close() + os.Remove(tmp) // Ignore error + return err + } + + l.Close() + + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_solaris.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_solaris.go new file mode 100644 index 00000000..db0593c8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_solaris.go @@ -0,0 +1,53 @@ +// +build solaris + +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +import ( + "net" + "os" + "path/filepath" + "syscall" +) + +func Mkfifo(path string, mode uint32) error { + // Mkfifo is missing from syscall, thus call Mknod as it does on Linux. + return syscall.Mknod(path, mode|syscall.S_IFIFO, 0) +} + +// Mksocket creates a socket file (a Unix Domain Socket) named path. +func Mksocket(path string) error { + dir := filepath.Dir(path) + base := filepath.Base(path) + tmp := filepath.Join(dir, "."+base) + l, err := net.ListenUnix("unix", &net.UnixAddr{tmp, "unix"}) + if err != nil { + return err + } + + err = os.Rename(tmp, path) + if err != nil { + l.Close() + os.Remove(tmp) // Ignore error + return err + } + + l.Close() + + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_windows.go b/vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_windows.go new file mode 100644 index 00000000..7eedb32d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/osutil/syscall_windows.go @@ -0,0 +1,20 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osutil + +func Mkfifo(path string, mode uint32) error { return ErrNotSupported } +func Mksocket(path string) error { return ErrNotSupported } diff --git a/vendor/github.com/camlistore/camlistore/pkg/pools/pools.go b/vendor/github.com/camlistore/camlistore/pkg/pools/pools.go new file mode 100644 index 00000000..4fc2f2e9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/pools/pools.go @@ -0,0 +1,41 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pools + +import ( + "bytes" + "sync" +) + +// bytesBuffer is a pool of *bytes.Buffer. +// Callers must Reset the buffer after obtaining it. +var bytesBuffer = sync.Pool{ + New: func() interface{} { return new(bytes.Buffer) }, +} + +// BytesBuffer returns an empty bytes.Buffer. +// It should be returned with PutBuffer. +func BytesBuffer() *bytes.Buffer { + buf := bytesBuffer.Get().(*bytes.Buffer) + buf.Reset() + return buf +} + +// PutBuffer returns a bytes.Buffer previously obtained with BytesBuffer. +func PutBuffer(buf *bytes.Buffer) { + bytesBuffer.Put(buf) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/publish/types.go b/vendor/github.com/camlistore/camlistore/pkg/publish/types.go new file mode 100644 index 00000000..8e2f43d2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/publish/types.go @@ -0,0 +1,89 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package publish exposes the types and functions that can be used +// from a Go template, for publishing. +package publish + +import ( + "html/template" + + "camlistore.org/pkg/search" +) + +// SubjectPage is the data structure used when serving a +// publishing template. It contains the functions that can be called +// from the template. +type SubjectPage struct { + Header func() *PageHeader + File func() *PageFile + Members func() *PageMembers +} + +// PageHeader contains the data available to the template, +// and relevant to the page header. +type PageHeader struct { + Title string // Page title. + CSSFiles []string // Available CSS files. + JSDeps []string // Dependencies (for e.g closure) that can/should be included as javascript files. + CamliClosure template.JS // Closure namespace defined in the provided js. e.g camlistore.GalleryPage from pics.js + Subject string // Subject of this page (i.e the object which is described and published). + Meta string // All the metadata describing the subject of this page. + ViewerIsOwner bool // Whether the viewer of the page is also the owner of the displayed subject. (localhost check for now.) +} + +// PageFile contains the file related data available to the subject template, +// if the page describes some file contents. +type PageFile struct { + FileName string + Size int64 + MIMEType string + IsImage bool + DownloadURL string + ThumbnailURL string + DomID string + Nav func() *Nav +} + +// Nav holds links to the previous, next, and parent elements, +// when displaying members. +type Nav struct { + ParentPath string + PrevPath string + NextPath string +} + +// PageMembers contains the data relevant to the members if the published subject +// is a permanode with members. +type PageMembers struct { + SubjectPath string // URL prefix path to the subject (i.e the permanode). + ZipName string // Name of the downloadable zip file which contains all the members. + Members []*search.DescribedBlob // List of the members. + Description func(*search.DescribedBlob) string // Returns the description of the given member. + Title func(*search.DescribedBlob) string // Returns the title for the given member. + Path func(*search.DescribedBlob) string // Returns the url prefix path to the given the member. + DomID func(*search.DescribedBlob) string // Returns the Dom ID of the given member. + FileInfo func(*search.DescribedBlob) *MemberFileInfo // Returns some file info if the given member is a file permanode. +} + +// MemberFileInfo contains the file related data available for each member, +// if the member is the permanode for a file. +type MemberFileInfo struct { + FileName string + FileDomID string + FilePath string + FileThumbnailURL string +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/readerutil/countingreader.go b/vendor/github.com/camlistore/camlistore/pkg/readerutil/countingreader.go new file mode 100644 index 00000000..51b33137 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/readerutil/countingreader.go @@ -0,0 +1,32 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package readerutil + +import "io" + +// CountingReader wraps a Reader, incrementing N by the number of +// bytes read. No locking is performed. +type CountingReader struct { + Reader io.Reader + N *int64 +} + +func (cr CountingReader) Read(p []byte) (n int, err error) { + n, err = cr.Reader.Read(p) + *cr.N += int64(n) + return +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/readerutil/opener.go b/vendor/github.com/camlistore/camlistore/pkg/readerutil/opener.go new file mode 100644 index 00000000..7e7122ea --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/readerutil/opener.go @@ -0,0 +1,125 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package readerutil + +import ( + "os" + "sync" + + "camlistore.org/pkg/singleflight" + "camlistore.org/pkg/types" +) + +var ( + openerGroup singleflight.Group + + openFileMu sync.Mutex // guards openFiles + openFiles = make(map[string]*openFile) +) + +type openFile struct { + *os.File + path string // map key of openFiles + refCount int +} + +type openFileHandle struct { + closed bool + *openFile +} + +func (f *openFileHandle) Close() error { + openFileMu.Lock() + if f.closed { + openFileMu.Unlock() + return nil + } + f.closed = true + f.refCount-- + if f.refCount < 0 { + panic("unexpected negative refcount") + } + zero := f.refCount == 0 + if zero { + delete(openFiles, f.path) + } + openFileMu.Unlock() + if !zero { + return nil + } + return f.openFile.File.Close() +} + +type openingFile struct { + path string + mu sync.RWMutex // write-locked until Open is done + + // Results, once mu is unlocked: + of *openFile + err error +} + +// OpenSingle opens the given file path for reading, reusing existing file descriptors +// when possible. +func OpenSingle(path string) (types.ReaderAtCloser, error) { + openFileMu.Lock() + of := openFiles[path] + if of != nil { + of.refCount++ + openFileMu.Unlock() + return &openFileHandle{false, of}, nil + } + openFileMu.Unlock() // release the lock while we call os.Open + + winner := false // this goroutine made it into Do's func + + // Returns an *openFile + resi, err := openerGroup.Do(path, func() (interface{}, error) { + winner = true + f, err := os.Open(path) + if err != nil { + return nil, err + } + of := &openFile{ + File: f, + path: path, + refCount: 1, + } + openFileMu.Lock() + openFiles[path] = of + openFileMu.Unlock() + return of, nil + }) + if err != nil { + return nil, err + } + of = resi.(*openFile) + + // If our os.Open was dup-suppressed, we have to increment our + // reference count. + if !winner { + openFileMu.Lock() + if of.refCount == 0 { + // Winner already closed it. Try again (rare). + openFileMu.Unlock() + return OpenSingle(path) + } + of.refCount++ + openFileMu.Unlock() + } + return &openFileHandle{false, of}, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/readerutil/opener_test.go b/vendor/github.com/camlistore/camlistore/pkg/readerutil/opener_test.go new file mode 100644 index 00000000..41fa37b5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/readerutil/opener_test.go @@ -0,0 +1,77 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package readerutil + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "runtime" + "testing" +) + +func TestOpenSingle(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4)) + f, err := ioutil.TempFile("", "foo") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + contents := []byte("Some file contents") + if _, err := f.Write(contents); err != nil { + t.Fatal(err) + } + f.Close() + + const j = 4 + errc := make(chan error, j) + for i := 1; i < j; i++ { + go func() { + buf := make([]byte, len(contents)) + for i := 0; i < 400; i++ { + rac, err := OpenSingle(f.Name()) + if err != nil { + errc <- err + return + } + n, err := rac.ReadAt(buf, 0) + if err != nil { + errc <- err + return + } + if n != len(contents) || !bytes.Equal(buf, contents) { + errc <- fmt.Errorf("read %d, %q; want %d, %q", n, buf, len(contents), contents) + return + } + if err := rac.Close(); err != nil { + errc <- err + return + } + } + errc <- nil + }() + } + for i := 1; i < j; i++ { + if err := <-errc; err != nil { + t.Error(err) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/readerutil/readersize.go b/vendor/github.com/camlistore/camlistore/pkg/readerutil/readersize.go new file mode 100644 index 00000000..4cfc4116 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/readerutil/readersize.go @@ -0,0 +1,52 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package readerutil provides and operates on io.Readers. +package readerutil + +import ( + "bytes" + "io" + "os" +) + +// ReaderSize tries to determine the length of r. +func ReaderSize(r io.Reader) (size int64, ok bool) { + switch rt := r.(type) { + case io.Seeker: + pos, err := rt.Seek(0, os.SEEK_CUR) + if err != nil { + return + } + end, err := rt.Seek(0, os.SEEK_END) + if err != nil { + return + } + size = end - pos + pos1, err := rt.Seek(pos, os.SEEK_SET) + if err != nil || pos1 != pos { + msg := "failed to restore seek position" + if err != nil { + msg += ": " + err.Error() + } + panic(msg) + } + return size, true + case *bytes.Buffer: + return int64(rt.Len()), true + } + return +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/readerutil/readersize_test.go b/vendor/github.com/camlistore/camlistore/pkg/readerutil/readersize_test.go new file mode 100644 index 00000000..ed7a9e83 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/readerutil/readersize_test.go @@ -0,0 +1,68 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package readerutil + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "testing" +) + +const text = "HelloWorld" + +type testSrc struct { + name string + src io.Reader + want int64 +} + +func (tsrc *testSrc) run(t *testing.T) { + n, ok := ReaderSize(tsrc.src) + if !ok { + t.Fatalf("failed to read size for %q", tsrc.name) + } + if n != tsrc.want { + t.Fatalf("wanted %v, got %v", tsrc.want, n) + } +} + +func TestBytesBuffer(t *testing.T) { + buf := bytes.NewBuffer([]byte(text)) + tsrc := &testSrc{"buffer", buf, int64(len(text))} + tsrc.run(t) +} + +func TestSeeker(t *testing.T) { + f, err := ioutil.TempFile("", "camliTestReaderSize") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + defer f.Close() + size, err := f.Write([]byte(text)) + if err != nil { + t.Fatal(err) + } + pos, err := f.Seek(5, 0) + if err != nil { + t.Fatal(err) + } + tsrc := &testSrc{"seeker", f, int64(size) - pos} + tsrc.run(t) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/rollsum/rollsum.go b/vendor/github.com/camlistore/camlistore/pkg/rollsum/rollsum.go new file mode 100644 index 00000000..8f84b46e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/rollsum/rollsum.go @@ -0,0 +1,81 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package rollsum implements rolling checksums similar to apenwarr's bup, which +// is similar to librsync. +// +// The bup project is at https://github.com/apenwarr/bup and its splitting in +// particular is at https://github.com/apenwarr/bup/blob/master/lib/bup/bupsplit.c +package rollsum + +import () + +const windowSize = 64 +const charOffset = 31 + +const blobBits = 13 +const blobSize = 1 << blobBits // 8k + +type RollSum struct { + s1, s2 uint32 + window [windowSize]uint8 + wofs int +} + +func New() *RollSum { + return &RollSum{ + s1: windowSize * charOffset, + s2: windowSize * (windowSize - 1) * charOffset, + } +} + +func (rs *RollSum) add(drop, add uint8) { + rs.s1 += uint32(add) - uint32(drop) + rs.s2 += rs.s1 - uint32(windowSize)*uint32(drop+charOffset) +} + +func (rs *RollSum) Roll(ch byte) { + rs.add(rs.window[rs.wofs], ch) + rs.window[rs.wofs] = ch + rs.wofs = (rs.wofs + 1) % windowSize +} + +// OnSplit returns whether at least 13 consecutive trailing bits of +// the current checksum are set the same way. +func (rs *RollSum) OnSplit() bool { + return (rs.s2 & (blobSize - 1)) == ((^0) & (blobSize - 1)) +} + +// OnSplit returns whether at least n consecutive trailing bits +// of the current checksum are set the same way. +func (rs *RollSum) OnSplitWithBits(n uint32) bool { + mask := (uint32(1) << n) - 1 + return rs.s2&mask == (^uint32(0))&mask +} + +func (rs *RollSum) Bits() int { + bits := blobBits + rsum := rs.Digest() + rsum >>= blobBits + for ; (rsum>>1)&1 != 0; bits++ { + rsum >>= 1 + } + return bits +} + +func (rs *RollSum) Digest() uint32 { + return (rs.s1 << 16) | (rs.s2 & 0xffff) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/rollsum/rollsum_test.go b/vendor/github.com/camlistore/camlistore/pkg/rollsum/rollsum_test.go new file mode 100644 index 00000000..edd4cff0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/rollsum/rollsum_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rollsum + +import ( + "math/rand" + "testing" +) + +func TestSum(t *testing.T) { + var buf [100000]uint8 + rnd := rand.New(rand.NewSource(4)) + for i := range buf { + buf[i] = uint8(rnd.Intn(256)) + } + + sum := func(offset, len int) uint32 { + rs := New() + for count := offset; count < len; count++ { + rs.Roll(buf[count]) + } + return rs.Digest() + } + + sum1a := sum(0, len(buf)) + sum1b := sum(1, len(buf)) + sum2a := sum(len(buf)-windowSize*5/2, len(buf)-windowSize) + sum2b := sum(0, len(buf)-windowSize) + sum3a := sum(0, windowSize+3) + sum3b := sum(3, windowSize+3) + + if sum1a != sum1b { + t.Errorf("sum1a=%d sum1b=%d", sum1a, sum1b) + } + if sum2a != sum2b { + t.Errorf("sum2a=%d sum2b=%d", sum2a, sum2b) + } + if sum3a != sum3b { + t.Errorf("sum3a=%d sum3b=%d", sum3a, sum3b) + } +} + +func BenchmarkRollsum(b *testing.B) { + const bufSize = 5 << 20 + buf := make([]byte, bufSize) + for i := range buf { + buf[i] = byte(rand.Int63()) + } + + b.ResetTimer() + rs := New() + splits := 0 + for i := 0; i < b.N; i++ { + splits = 0 + for _, b := range buf { + rs.Roll(b) + if rs.OnSplit() { + _ = rs.Bits() + splits++ + } + } + } + b.SetBytes(bufSize) + b.Logf("num splits = %d; every %d bytes", splits, int(float64(bufSize)/float64(splits))) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/.gitignore b/vendor/github.com/camlistore/camlistore/pkg/schema/.gitignore new file mode 100644 index 00000000..cde0389a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/.gitignore @@ -0,0 +1,3 @@ +_test* +*.out +*.[865] diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/blob.go b/vendor/github.com/camlistore/camlistore/pkg/schema/blob.go new file mode 100644 index 00000000..a69eab39 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/blob.go @@ -0,0 +1,589 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strings" + "time" + "unicode/utf8" + + "camlistore.org/pkg/blob" +) + +// A MissingFieldError represents a missing JSON field in a schema blob. +type MissingFieldError string + +func (e MissingFieldError) Error() string { + return fmt.Sprintf("schema: missing field %q", string(e)) +} + +// IsMissingField returns whether error is of type MissingFieldError. +func IsMissingField(err error) bool { + _, ok := err.(MissingFieldError) + return ok +} + +// AnyBlob represents any type of schema blob. +type AnyBlob interface { + Blob() *Blob +} + +// Buildable returns a Builder from a base. +type Buildable interface { + Builder() *Builder +} + +// A Blob represents a Camlistore schema blob. +// It is immutable. +type Blob struct { + br blob.Ref + str string + ss *superset +} + +// Type returns the blob's "camliType" field. +func (b *Blob) Type() string { return b.ss.Type } + +// BlobRef returns the schema blob's blobref. +func (b *Blob) BlobRef() blob.Ref { return b.br } + +// JSON returns the JSON bytes of the schema blob. +func (b *Blob) JSON() string { return b.str } + +// Blob returns itself, so it satisifies the AnyBlob interface. +func (b *Blob) Blob() *Blob { return b } + +// PartsSize returns the number of bytes represented by the "parts" field. +// TODO: move this off *Blob to a specialized type. +func (b *Blob) PartsSize() int64 { + n := int64(0) + for _, part := range b.ss.Parts { + n += int64(part.Size) + } + return n +} + +// FileName returns the file, directory, or symlink's filename, or the empty string. +// TODO: move this off *Blob to a specialized type. +func (b *Blob) FileName() string { + return b.ss.FileNameString() +} + +// ClaimDate returns the "claimDate" field. +// If there is no claimDate, the error will be a MissingFieldError. +func (b *Blob) ClaimDate() (time.Time, error) { + var ct time.Time + claimDate := b.ss.ClaimDate + if claimDate.IsZero() { + return ct, MissingFieldError("claimDate") + } + return claimDate.Time(), nil +} + +// ByteParts returns the "parts" field. The caller owns the returned +// slice. +func (b *Blob) ByteParts() []BytesPart { + // TODO: move this method off Blob, and make the caller go + // through a (*Blob).ByteBackedBlob() comma-ok accessor first. + s := make([]BytesPart, len(b.ss.Parts)) + for i, part := range b.ss.Parts { + s[i] = *part + } + return s +} + +func (b *Blob) Builder() *Builder { + var m map[string]interface{} + dec := json.NewDecoder(strings.NewReader(b.str)) + dec.UseNumber() + err := dec.Decode(&m) + if err != nil { + panic("failed to decode previously-thought-valid Blob's JSON: " + err.Error()) + } + return &Builder{m} +} + +// AsClaim returns a Claim if the receiver Blob has all the required fields. +func (b *Blob) AsClaim() (c Claim, ok bool) { + if b.ss.Signer.Valid() && b.ss.Sig != "" && b.ss.ClaimType != "" && !b.ss.ClaimDate.IsZero() { + return Claim{b}, true + } + return +} + +// AsShare returns a Share if the receiver Blob has all the required fields. +func (b *Blob) AsShare() (s Share, ok bool) { + c, isClaim := b.AsClaim() + if !isClaim { + return + } + + if ClaimType(b.ss.ClaimType) == ShareClaim && b.ss.AuthType == ShareHaveRef && (b.ss.Target.Valid() || b.ss.Search != nil) { + return Share{c}, true + } + return s, false +} + +// DirectoryEntries the "entries" field if valid and b's type is "directory". +func (b *Blob) DirectoryEntries() (br blob.Ref, ok bool) { + if b.Type() != "directory" { + return + } + return b.ss.Entries, true +} + +func (b *Blob) StaticSetMembers() []blob.Ref { + if b.Type() != "static-set" { + return nil + } + s := make([]blob.Ref, 0, len(b.ss.Members)) + for _, ref := range b.ss.Members { + if ref.Valid() { + s = append(s, ref) + } + } + return s +} + +func (b *Blob) ShareAuthType() string { + s, ok := b.AsShare() + if !ok { + return "" + } + return s.AuthType() +} + +func (b *Blob) ShareTarget() blob.Ref { + s, ok := b.AsShare() + if !ok { + return blob.Ref{} + } + return s.Target() +} + +// ModTime returns the "unixMtime" field, or the zero time. +func (b *Blob) ModTime() time.Time { return b.ss.ModTime() } + +// A Claim is a Blob that is signed. +type Claim struct { + b *Blob +} + +// Blob returns the claim's Blob. +func (c Claim) Blob() *Blob { return c.b } + +// ClaimDate returns the blob's "claimDate" field. +func (c Claim) ClaimDateString() string { return c.b.ss.ClaimDate.String() } + +// ClaimType returns the blob's "claimType" field. +func (c Claim) ClaimType() string { return c.b.ss.ClaimType } + +// Attribute returns the "attribute" field, if set. +func (c Claim) Attribute() string { return c.b.ss.Attribute } + +// Value returns the "value" field, if set. +func (c Claim) Value() string { return c.b.ss.Value } + +// ModifiedPermanode returns the claim's "permaNode" field, if it's +// a claim that modifies a permanode. Otherwise a zero blob.Ref is +// returned. +func (c Claim) ModifiedPermanode() blob.Ref { + return c.b.ss.Permanode +} + +// Target returns the blob referenced by the Share if it's +// a ShareClaim claim, or the object being deleted if it's a +// DeleteClaim claim. +// Otherwise a zero blob.Ref is returned. +func (c Claim) Target() blob.Ref { + return c.b.ss.Target +} + +// A Share is a claim for giving access to a user's blob(s). +// When returned from (*Blob).AsShare, it always represents +// a valid share with all required fields. +type Share struct { + Claim +} + +// AuthType returns the AuthType of the Share. +func (s Share) AuthType() string { + return s.b.ss.AuthType +} + +// IsTransitive returns whether the Share transitively +// gives access to everything reachable from the referenced +// blob. +func (s Share) IsTransitive() bool { + return s.b.ss.Transitive +} + +// IsExpired reports whether this share has expired. +func (s Share) IsExpired() bool { + t := time.Time(s.b.ss.Expires) + return !t.IsZero() && clockNow().After(t) +} + +// A StaticFile is a Blob representing a file, symlink fifo or socket +// (or device file, when support for these is added). +type StaticFile struct { + b *Blob +} + +// FileName returns the StaticFile's FileName if is not the empty string, otherwise it returns its FileNameBytes concatenated into a string. +func (sf StaticFile) FileName() string { + return sf.b.ss.FileNameString() +} + +// AsStaticFile returns the Blob as a StaticFile if it represents +// one. Otherwise, it returns false in the boolean parameter and the +// zero value of StaticFile. +func (b *Blob) AsStaticFile() (sf StaticFile, ok bool) { + // TODO (marete) Add support for device files to + // Camlistore and change the implementation of StaticFile to + // reflect that. + t := b.ss.Type + if t == "file" || t == "symlink" || t == "fifo" || t == "socket" { + return StaticFile{b}, true + } + + return +} + +// A StaticFIFO is a StaticFile that is also a fifo. +type StaticFIFO struct { + StaticFile +} + +// A StaticSocket is a StaticFile that is also a socket. +type StaticSocket struct { + StaticFile +} + +// A StaticSymlink is a StaticFile that is also a symbolic link. +type StaticSymlink struct { + // We name it `StaticSymlink' rather than just `Symlink' since + // a type called Symlink is already in schema.go. + StaticFile +} + +// SymlinkTargetString returns the field symlinkTarget if is +// non-empty. Otherwise it returns the contents of symlinkTargetBytes +// concatenated as a string. +func (sl StaticSymlink) SymlinkTargetString() string { + return sl.StaticFile.b.ss.SymlinkTargetString() +} + +// AsStaticSymlink returns the StaticFile as a StaticSymlink if the +// StaticFile represents a symlink. Othwerwise, it retuns the zero +// value of StaticSymlink and false. +func (sf StaticFile) AsStaticSymlink() (s StaticSymlink, ok bool) { + if sf.b.ss.Type == "symlink" { + return StaticSymlink{sf}, true + } + + return +} + +// AsStaticFIFO returns the StatifFile as a StaticFIFO if the +// StaticFile represents a fifo. Otherwise, it returns the zero value +// of StaticFIFO and false. +func (sf StaticFile) AsStaticFIFO() (fifo StaticFIFO, ok bool) { + if sf.b.ss.Type == "fifo" { + return StaticFIFO{sf}, true + } + + return +} + +// AsSataticSocket returns the StaticFile as a StaticSocket if the +// StaticFile represents a socket. Otherwise, it returns the zero +// value of StaticSocket and false. +func (sf StaticFile) AsStaticSocket() (ss StaticSocket, ok bool) { + if sf.b.ss.Type == "socket" { + return StaticSocket{sf}, true + } + + return +} + +// A Builder builds a JSON blob. +// After mutating the Builder, call Blob to get the built blob. +type Builder struct { + m map[string]interface{} +} + +// NewBuilder returns a new blob schema builder. +// The "camliVersion" field is set to "1" by default and the required +// "camliType" field is NOT set. +func NewBuilder() *Builder { + return &Builder{map[string]interface{}{ + "camliVersion": "1", + }} +} + +// SetShareTarget sets the target of share claim. +// It panics if bb isn't a "share" claim type. +func (bb *Builder) SetShareTarget(t blob.Ref) *Builder { + if bb.Type() != "claim" || bb.ClaimType() != ShareClaim { + panic("called SetShareTarget on non-share") + } + bb.m["target"] = t.String() + return bb +} + +// SetShareSearch sets the search of share claim. +// q is assumed to be of type *search.SearchQuery. +// It panics if bb isn't a "share" claim type. +func (bb *Builder) SetShareSearch(q SearchQuery) *Builder { + if bb.Type() != "claim" || bb.ClaimType() != ShareClaim { + panic("called SetShareSearch on non-share") + } + bb.m["search"] = q + return bb +} + +// SetShareExpiration sets the expiration time on share claim. +// It panics if bb isn't a "share" claim type. +// If t is zero, the expiration is removed. +func (bb *Builder) SetShareExpiration(t time.Time) *Builder { + if bb.Type() != "claim" || bb.ClaimType() != ShareClaim { + panic("called SetShareExpiration on non-share") + } + if t.IsZero() { + delete(bb.m, "expires") + } else { + bb.m["expires"] = RFC3339FromTime(t) + } + return bb +} + +func (bb *Builder) SetShareIsTransitive(b bool) *Builder { + if bb.Type() != "claim" || bb.ClaimType() != ShareClaim { + panic("called SetShareIsTransitive on non-share") + } + if !b { + delete(bb.m, "transitive") + } else { + bb.m["transitive"] = true + } + return bb +} + +// SetRawStringField sets a raw string field in the underlying map. +func (bb *Builder) SetRawStringField(key, value string) *Builder { + bb.m[key] = value + return bb +} + +// Blob builds the Blob. The builder continues to be usable after a call to Build. +func (bb *Builder) Blob() *Blob { + json, err := mapJSON(bb.m) + if err != nil { + panic(err) + } + ss, err := parseSuperset(strings.NewReader(json)) + if err != nil { + panic(err) + } + h := blob.NewHash() + h.Write([]byte(json)) + return &Blob{ + str: json, + ss: ss, + br: blob.RefFromHash(h), + } +} + +// Builder returns a clone of itself and satisifies the Buildable interface. +func (bb *Builder) Builder() *Builder { + return &Builder{clone(bb.m).(map[string]interface{})} +} + +// JSON returns the JSON of the blob as built so far. +func (bb *Builder) JSON() (string, error) { + return mapJSON(bb.m) +} + +// SetSigner sets the camliSigner field. +// Calling SetSigner is unnecessary if using Sign. +func (bb *Builder) SetSigner(signer blob.Ref) *Builder { + bb.m["camliSigner"] = signer.String() + return bb +} + +// SignAt sets the blob builder's camliSigner field with SetSigner +// and returns the signed JSON using the provided signer. +func (bb *Builder) Sign(signer *Signer) (string, error) { + return bb.SignAt(signer, time.Time{}) +} + +// SignAt sets the blob builder's camliSigner field with SetSigner +// and returns the signed JSON using the provided signer. +// The provided sigTime is the time of the signature, used mostly +// for planned permanodes. If the zero value, the current time is used. +func (bb *Builder) SignAt(signer *Signer, sigTime time.Time) (string, error) { + switch bb.Type() { + case "permanode", "claim": + default: + return "", fmt.Errorf("can't sign camliType %q", bb.Type()) + } + return signer.SignJSON(bb.SetSigner(signer.pubref).Blob().JSON(), sigTime) +} + +// SetType sets the camliType field. +func (bb *Builder) SetType(t string) *Builder { + bb.m["camliType"] = t + return bb +} + +// Type returns the camliType value. +func (bb *Builder) Type() string { + if s, ok := bb.m["camliType"].(string); ok { + return s + } + return "" +} + +// ClaimType returns the claimType value, or the empty string. +func (bb *Builder) ClaimType() ClaimType { + if s, ok := bb.m["claimType"].(string); ok { + return ClaimType(s) + } + return "" +} + +// SetFileName sets the fileName or fileNameBytes field. +// The filename is truncated to just the base. +func (bb *Builder) SetFileName(name string) *Builder { + baseName := filepath.Base(name) + if utf8.ValidString(baseName) { + bb.m["fileName"] = baseName + } else { + bb.m["fileNameBytes"] = mixedArrayFromString(baseName) + } + return bb +} + +// SetSymlinkTarget sets bb to be of type "symlink" and sets the symlink's target. +func (bb *Builder) SetSymlinkTarget(target string) *Builder { + bb.SetType("symlink") + if utf8.ValidString(target) { + bb.m["symlinkTarget"] = target + } else { + bb.m["symlinkTargetBytes"] = mixedArrayFromString(target) + } + return bb +} + +// IsClaimType returns whether this blob builder is for a type +// which should be signed. (a "claim" or "permanode") +func (bb *Builder) IsClaimType() bool { + switch bb.Type() { + case "claim", "permanode": + return true + } + return false +} + +// SetClaimDate sets the "claimDate" on a claim. +// It is a fatal error to call SetClaimDate if the Map isn't of Type "claim". +func (bb *Builder) SetClaimDate(t time.Time) *Builder { + if !bb.IsClaimType() { + // This is a little gross, using panic here, but I + // don't want all callers to check errors. This is + // really a programming error, not a runtime error + // that would arise from e.g. random user data. + panic("SetClaimDate called on non-claim *Builder; camliType=" + bb.Type()) + } + bb.m["claimDate"] = RFC3339FromTime(t) + return bb +} + +// SetModTime sets the "unixMtime" field. +func (bb *Builder) SetModTime(t time.Time) *Builder { + bb.m["unixMtime"] = RFC3339FromTime(t) + return bb +} + +// CapCreationTime caps the "unixCtime" field to be less or equal than "unixMtime" +func (bb *Builder) CapCreationTime() *Builder { + ctime, ok := bb.m["unixCtime"].(string) + if !ok { + return bb + } + mtime, ok := bb.m["unixMtime"].(string) + if ok && ctime > mtime { + bb.m["unixCtime"] = mtime + } + return bb +} + +// ModTime returns the "unixMtime" modtime field, if set. +func (bb *Builder) ModTime() (t time.Time, ok bool) { + s, ok := bb.m["unixMtime"].(string) + if !ok { + return + } + t, err := time.Parse(time.RFC3339, s) + if err != nil { + return + } + return t, true +} + +// PopulateDirectoryMap sets the type of *Builder to "directory" and sets +// the "entries" field to the provided staticSet blobref. +func (bb *Builder) PopulateDirectoryMap(staticSetRef blob.Ref) *Builder { + bb.m["camliType"] = "directory" + bb.m["entries"] = staticSetRef.String() + return bb +} + +// PartsSize returns the number of bytes represented by the "parts" field. +func (bb *Builder) PartsSize() int64 { + n := int64(0) + if parts, ok := bb.m["parts"].([]BytesPart); ok { + for _, part := range parts { + n += int64(part.Size) + } + } + return n +} + +func clone(i interface{}) interface{} { + switch t := i.(type) { + case map[string]interface{}: + m2 := make(map[string]interface{}) + for k, v := range t { + m2[k] = clone(v) + } + return m2 + case string, int, int64, float64, json.Number: + return t + case []interface{}: + s2 := make([]interface{}, len(t)) + for i, v := range t { + s2[i] = clone(v) + } + return s2 + } + panic(fmt.Sprintf("unsupported clone type %T", i)) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/dirreader.go b/vendor/github.com/camlistore/camlistore/pkg/schema/dirreader.go new file mode 100644 index 00000000..a6da856e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/dirreader.go @@ -0,0 +1,164 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "encoding/json" + "errors" + "fmt" + "io" + + "camlistore.org/pkg/blob" +) + +// A DirReader reads the entries of a "directory" schema blob's +// referenced "static-set" blob. +type DirReader struct { + fetcher blob.Fetcher + ss *superset + + staticSet []blob.Ref + current int +} + +// NewDirReader creates a new directory reader and prepares to +// fetch the static-set entries +func NewDirReader(fetcher blob.Fetcher, dirBlobRef blob.Ref) (*DirReader, error) { + ss := new(superset) + err := ss.setFromBlobRef(fetcher, dirBlobRef) + if err != nil { + return nil, err + } + if ss.Type != "directory" { + return nil, fmt.Errorf("schema/dirreader: expected \"directory\" schema blob for %s, got %q", dirBlobRef, ss.Type) + } + dr, err := ss.NewDirReader(fetcher) + if err != nil { + return nil, fmt.Errorf("schema/dirreader: creating DirReader for %s: %v", dirBlobRef, err) + } + dr.current = 0 + return dr, nil +} + +func (b *Blob) NewDirReader(fetcher blob.Fetcher) (*DirReader, error) { + return b.ss.NewDirReader(fetcher) +} + +func (ss *superset) NewDirReader(fetcher blob.Fetcher) (*DirReader, error) { + if ss.Type != "directory" { + return nil, fmt.Errorf("Superset not of type \"directory\"") + } + return &DirReader{fetcher: fetcher, ss: ss}, nil +} + +func (ss *superset) setFromBlobRef(fetcher blob.Fetcher, blobRef blob.Ref) error { + if !blobRef.Valid() { + return errors.New("schema/dirreader: blobref invalid") + } + ss.BlobRef = blobRef + rc, _, err := fetcher.Fetch(blobRef) + if err != nil { + return fmt.Errorf("schema/dirreader: fetching schema blob %s: %v", blobRef, err) + } + defer rc.Close() + if err := json.NewDecoder(rc).Decode(ss); err != nil { + return fmt.Errorf("schema/dirreader: decoding schema blob %s: %v", blobRef, err) + } + return nil +} + +// StaticSet returns the whole of the static set members of that directory +func (dr *DirReader) StaticSet() ([]blob.Ref, error) { + if dr.staticSet != nil { + return dr.staticSet, nil + } + staticSetBlobref := dr.ss.Entries + if !staticSetBlobref.Valid() { + return nil, errors.New("schema/dirreader: Invalid blobref") + } + rsc, _, err := dr.fetcher.Fetch(staticSetBlobref) + if err != nil { + return nil, fmt.Errorf("schema/dirreader: fetching schema blob %s: %v", staticSetBlobref, err) + } + defer rsc.Close() + ss, err := parseSuperset(rsc) + if err != nil { + return nil, fmt.Errorf("schema/dirreader: decoding schema blob %s: %v", staticSetBlobref, err) + } + if ss.Type != "static-set" { + return nil, fmt.Errorf("schema/dirreader: expected \"static-set\" schema blob for %s, got %q", staticSetBlobref, ss.Type) + } + for _, member := range ss.Members { + if !member.Valid() { + return nil, fmt.Errorf("schema/dirreader: invalid (static-set member) blobref referred by \"static-set\" schema blob %v", staticSetBlobref) + } + dr.staticSet = append(dr.staticSet, member) + } + return dr.staticSet, nil +} + +// Readdir implements the Directory interface. +func (dr *DirReader) Readdir(n int) (entries []DirectoryEntry, err error) { + sts, err := dr.StaticSet() + if err != nil { + return nil, fmt.Errorf("schema/dirreader: can't get StaticSet: %v", err) + } + up := dr.current + n + if n <= 0 { + dr.current = 0 + up = len(sts) + } else { + if n > (len(sts) - dr.current) { + err = io.EOF + up = len(sts) + } + } + + // TODO(bradfitz): push down information to the fetcher + // (e.g. cachingfetcher -> remote client http) that we're + // going to load a bunch, so the HTTP client (if not using + // SPDY) can do discovery and see if the server supports a + // batch handler, then get them all in one round-trip, rather + // than attacking the server with hundreds of parallel TLS + // setups. + + type res struct { + ent DirectoryEntry + err error + } + var cs []chan res + + // Kick off all directory entry loads. + // TODO: bound this? + for _, entRef := range sts[dr.current:up] { + c := make(chan res, 1) + cs = append(cs, c) + go func(entRef blob.Ref) { + entry, err := NewDirectoryEntryFromBlobRef(dr.fetcher, entRef) + c <- res{entry, err} + }(entRef) + } + + for _, c := range cs { + res := <-c + if res.err != nil { + return nil, fmt.Errorf("schema/dirreader: can't create dirEntry: %v", res.err) + } + entries = append(entries, res.ent) + } + return entries, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/fileread_test.go b/vendor/github.com/camlistore/camlistore/pkg/schema/fileread_test.go new file mode 100644 index 00000000..32db4610 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/fileread_test.go @@ -0,0 +1,453 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "math/rand" + "os" + "testing" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/test" +) + +var testFetcher = &test.Fetcher{} + +var blobA = &test.Blob{"AAAAAaaaaa"} +var blobB = &test.Blob{"BBBBBbbbbb"} +var blobC = &test.Blob{"CCCCCccccc"} + +func init() { + testFetcher.AddBlob(blobA) + testFetcher.AddBlob(blobB) + testFetcher.AddBlob(blobC) +} + +type readTest struct { + parts []*BytesPart + skip uint64 + expected string +} + +func part(blob *test.Blob, offset, size uint64) *BytesPart { + return &BytesPart{BlobRef: blob.BlobRef(), Size: size, Offset: offset} +} + +// filePart returns a BytesPart that references a file JSON schema +// blob made of the provided content parts. +func filePart(cps []*BytesPart, skip uint64) *BytesPart { + m := newBytes() + fileSize := int64(0) + cpl := []BytesPart{} + for _, cp := range cps { + fileSize += int64(cp.Size) + cpl = append(cpl, *cp) + } + err := m.PopulateParts(fileSize, cpl) + if err != nil { + panic(err) + } + json, err := m.JSON() + if err != nil { + panic(err) + } + tb := &test.Blob{json} + testFetcher.AddBlob(tb) + return &BytesPart{BytesRef: tb.BlobRef(), Size: uint64(fileSize) - skip, Offset: skip} +} + +func all(blob *test.Blob) *BytesPart { + return part(blob, 0, uint64(blob.Size())) +} + +func zero(size uint64) *BytesPart { + return &BytesPart{Size: size} +} + +func parts(parts ...*BytesPart) []*BytesPart { + return parts +} + +func sizeSum(parts []*BytesPart) (s uint64) { + for _, p := range parts { + s += uint64(p.Size) + } + return +} + +var readTests = []readTest{ + {parts(all(blobA)), 0, "AAAAAaaaaa"}, + {parts(all(blobA)), 2, "AAAaaaaa"}, + {parts(part(blobA, 0, 5)), 0, "AAAAA"}, + {parts(part(blobA, 2, 8)), 0, "AAAaaaaa"}, + {parts(part(blobA, 2, 8)), 1, "AAaaaaa"}, + {parts(part(blobA, 4, 6)), 0, "Aaaaaa"}, + {parts(all(blobA), all(blobB)), 0, "AAAAAaaaaaBBBBBbbbbb"}, + {parts(all(blobA), all(blobB)), 1, "AAAAaaaaaBBBBBbbbbb"}, + {parts(all(blobA), all(blobB)), 10, "BBBBBbbbbb"}, + {parts(all(blobA), all(blobB)), 11, "BBBBbbbbb"}, + {parts(all(blobA), all(blobB)), 100, ""}, + {parts(all(blobA), all(blobB), all(blobC)), 0, "AAAAAaaaaaBBBBBbbbbbCCCCCccccc"}, + {parts(all(blobA), all(blobB), all(blobC)), 20, "CCCCCccccc"}, + {parts(all(blobA), all(blobB), all(blobC)), 22, "CCCccccc"}, + {parts(part(blobA, 5, 5), part(blobB, 0, 5), part(blobC, 4, 2)), 1, "aaaaBBBBBCc"}, + {parts(all(blobA), zero(2), all(blobB)), 5, "aaaaa\x00\x00BBBBBbbbbb"}, + {parts(all(blobB), part(blobC, 4, 2)), 0, "BBBBBbbbbbCc"}, + {parts( + all(blobA), + filePart(parts(all(blobB), part(blobC, 4, 2)), 0), + part(blobA, 5, 5)), + 1, + "AAAAaaaaa" + "BBBBBbbbbb" + "Cc" + "aaaaa"}, + {parts( + all(blobA), + filePart(parts(all(blobB), part(blobC, 4, 2)), 4), + part(blobA, 5, 5)), + 1, + "AAAAaaaaa" + "Bbbbbb" + "Cc" + "aaaaa"}, +} + +func skipBytes(fr *FileReader, skipBytes uint64) uint64 { + oldOff, err := fr.Seek(0, os.SEEK_CUR) + if err != nil { + panic("Failed to seek") + } + remain := fr.size - oldOff + if int64(skipBytes) > remain { + skipBytes = uint64(remain) + } + newOff, err := fr.Seek(int64(skipBytes), os.SEEK_CUR) + if err != nil { + panic("Failed to seek") + } + skipped := newOff - oldOff + if skipped < 0 { + panic("") + } + return uint64(skipped) +} + +func TestReader(t *testing.T) { + for idx, rt := range readTests { + ss := new(superset) + ss.Type = "file" + ss.Version = 1 + ss.Parts = rt.parts + fr, err := ss.NewFileReader(testFetcher) + if err != nil { + t.Errorf("read error on test %d: %v", idx, err) + continue + } + skipBytes(fr, rt.skip) + all, err := ioutil.ReadAll(fr) + if err != nil { + t.Errorf("read error on test %d: %v", idx, err) + continue + } + if g, e := string(all), rt.expected; e != g { + t.Errorf("test %d\nwant %q\n got %q", idx, e, g) + } + } +} + +func TestReaderSeekStress(t *testing.T) { + const fileSize = 750<<10 + 123 + bigFile := make([]byte, fileSize) + rnd := rand.New(rand.NewSource(1)) + for i := range bigFile { + bigFile[i] = byte(rnd.Intn(256)) + } + + sto := new(test.Fetcher) // in-memory blob storage + fileMap := NewFileMap("testfile") + fileref, err := WriteFileMap(sto, fileMap, bytes.NewReader(bigFile)) + if err != nil { + t.Fatalf("WriteFileMap: %v", err) + } + c, ok := sto.BlobContents(fileref) + if !ok { + t.Fatal("expected file contents to be present") + } + const debug = false + if debug { + t.Logf("Fileref %s: %s", fileref, c) + } + + // Test a bunch of reads at different offsets, making sure we always + // get the same results. + skipBy := int64(999) + if testing.Short() { + skipBy += 10 << 10 + } + for off := int64(0); off < fileSize; off += skipBy { + fr, err := NewFileReader(sto, fileref) + if err != nil { + t.Fatal(err) + } + + skipBytes(fr, uint64(off)) + got, err := ioutil.ReadAll(fr) + if err != nil { + t.Fatal(err) + } + want := bigFile[off:] + if !bytes.Equal(got, want) { + t.Errorf("Incorrect read at offset %d:\n got: %s\n want: %s", off, summary(got), summary(want)) + off := 0 + for len(got) > 0 && len(want) > 0 && got[0] == want[0] { + off++ + got = got[1:] + want = want[1:] + } + t.Errorf(" differences start at offset %d:\n got: %s\n want: %s\n", off, summary(got), summary(want)) + break + } + fr.Close() + } +} + +/* + +1KB ReadAt calls before: +fileread_test.go:253: Blob Size: 4194304 raw, 4201523 with meta (1.00172x) +fileread_test.go:283: Blobs fetched: 4160 (63.03x) +fileread_test.go:284: Bytes fetched: 361174780 (85.96x) + +2KB ReadAt calls before: +fileread_test.go:253: Blob Size: 4194304 raw, 4201523 with meta (1.00172x) +fileread_test.go:283: Blobs fetched: 2112 (32.00x) +fileread_test.go:284: Bytes fetched: 182535389 (43.45x) + +After fix: +fileread_test.go:253: Blob Size: 4194304 raw, 4201523 with meta (1.00172x) +fileread_test.go:283: Blobs fetched: 66 (1.00x) +fileread_test.go:284: Bytes fetched: 4201523 (1.00x) +*/ +func TestReaderEfficiency(t *testing.T) { + const fileSize = 4 << 20 + bigFile := make([]byte, fileSize) + rnd := rand.New(rand.NewSource(1)) + for i := range bigFile { + bigFile[i] = byte(rnd.Intn(256)) + } + + sto := new(test.Fetcher) // in-memory blob storage + fileMap := NewFileMap("testfile") + fileref, err := WriteFileMap(sto, fileMap, bytes.NewReader(bigFile)) + if err != nil { + t.Fatalf("WriteFileMap: %v", err) + } + + fr, err := NewFileReader(sto, fileref) + if err != nil { + t.Fatal(err) + } + + numBlobs := sto.NumBlobs() + t.Logf("Num blobs = %d", numBlobs) + sumSize := sto.SumBlobSize() + t.Logf("Blob Size: %d raw, %d with meta (%.05fx)", fileSize, sumSize, float64(sumSize)/float64(fileSize)) + + const readSize = 2 << 10 + buf := make([]byte, readSize) + for off := int64(0); off < fileSize; off += readSize { + n, err := fr.ReadAt(buf, off) + if err != nil { + t.Fatalf("ReadAt at offset %d: %v", off, err) + } + if n != readSize { + t.Fatalf("Read %d bytes at offset %d; want %d", n, off, readSize) + } + got, want := buf, bigFile[off:off+readSize] + if !bytes.Equal(buf, want) { + t.Errorf("Incorrect read at offset %d:\n got: %s\n want: %s", off, summary(got), summary(want)) + off := 0 + for len(got) > 0 && len(want) > 0 && got[0] == want[0] { + off++ + got = got[1:] + want = want[1:] + } + t.Errorf(" differences start at offset %d:\n got: %s\n want: %s\n", off, summary(got), summary(want)) + break + } + } + fr.Close() + blobsFetched, bytesFetched := sto.Stats() + if blobsFetched != int64(numBlobs) { + t.Errorf("Fetched %d blobs; want %d", blobsFetched, numBlobs) + } + if bytesFetched != sumSize { + t.Errorf("Fetched %d bytes; want %d", bytesFetched, sumSize) + } +} + +func TestReaderForeachChunk(t *testing.T) { + fileSize := 4 << 20 + if testing.Short() { + fileSize = 1 << 20 + } + bigFile := make([]byte, fileSize) + rnd := rand.New(rand.NewSource(1)) + for i := range bigFile { + bigFile[i] = byte(rnd.Intn(256)) + } + sto := new(test.Fetcher) // in-memory blob storage + fileMap := NewFileMap("testfile") + fileref, err := WriteFileMap(sto, fileMap, bytes.NewReader(bigFile)) + if err != nil { + t.Fatalf("WriteFileMap: %v", err) + } + + fr, err := NewFileReader(sto, fileref) + if err != nil { + t.Fatal(err) + } + + var back bytes.Buffer + var totSize uint64 + err = fr.ForeachChunk(func(sref []blob.Ref, p BytesPart) error { + if len(sref) < 1 { + t.Fatal("expected at least one schemaPath blob") + } + for i, br := range sref { + if !br.Valid() { + t.Fatalf("invalid schema blob in path index %d", i) + } + } + if p.BytesRef.Valid() { + t.Fatal("should never see a valid BytesRef") + } + if !p.BlobRef.Valid() { + t.Fatal("saw part with invalid blobref") + } + rc, size, err := sto.Fetch(p.BlobRef) + if err != nil { + return fmt.Errorf("Error fetching blobref of chunk %+v: %v", p, err) + } + defer rc.Close() + totSize += p.Size + if uint64(size) != p.Size { + return fmt.Errorf("fetched size %d doesn't match expected for chunk %+v", size, p) + } + n, err := io.Copy(&back, rc) + if err != nil { + return err + } + if n != int64(size) { + return fmt.Errorf("Copied unexpected %d bytes of chunk %+v", n, p) + } + return nil + }) + if err != nil { + t.Fatalf("ForeachChunk = %v", err) + } + if back.Len() != fileSize { + t.Fatalf("Read file is %d bytes; want %d", back.Len(), fileSize) + } + if totSize != uint64(fileSize) { + t.Errorf("sum of parts = %d; want %d", totSize, fileSize) + } + if !bytes.Equal(back.Bytes(), bigFile) { + t.Errorf("file read mismatch") + } +} + +func TestForeachChunkAllSchemaBlobs(t *testing.T) { + sto := new(test.Fetcher) // in-memory blob storage + foo := &test.Blob{"foo"} + bar := &test.Blob{"bar"} + sto.AddBlob(foo) + sto.AddBlob(bar) + + // Make a "bytes" schema blob referencing the "foo" and "bar" chunks. + // Verify it works. + bytesBlob := &test.Blob{`{"camliVersion": 1, +"camliType": "bytes", +"parts": [ + {"blobRef": "` + foo.BlobRef().String() + `", "size": 3}, + {"blobRef": "` + bar.BlobRef().String() + `", "size": 3} +]}`} + sto.AddBlob(bytesBlob) + + var fr *FileReader + mustRead := func(name string, br blob.Ref, want string) { + var err error + fr, err = NewFileReader(sto, br) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + all, err := ioutil.ReadAll(fr) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + if string(all) != want { + t.Errorf("%s: read contents %q; want %q", name, all, want) + } + } + mustRead("bytesBlob", bytesBlob.BlobRef(), "foobar") + + // Now make another bytes schema blob embedding the previous one. + bytesBlob2 := &test.Blob{`{"camliVersion": 1, +"camliType": "bytes", +"parts": [ + {"bytesRef": "` + bytesBlob.BlobRef().String() + `", "size": 6} +]}`} + sto.AddBlob(bytesBlob2) + mustRead("bytesBlob2", bytesBlob2.BlobRef(), "foobar") + + sawSchema := map[blob.Ref]bool{} + sawData := map[blob.Ref]bool{} + if err := fr.ForeachChunk(func(path []blob.Ref, p BytesPart) error { + for _, sref := range path { + sawSchema[sref] = true + } + sawData[p.BlobRef] = true + return nil + }); err != nil { + t.Fatal(err) + } + want := []struct { + name string + tb *test.Blob + m map[blob.Ref]bool + }{ + {"bytesBlob", bytesBlob, sawSchema}, + {"bytesBlob2", bytesBlob2, sawSchema}, + {"foo", foo, sawData}, + {"bar", bar, sawData}, + } + for _, tt := range want { + if b := tt.tb.BlobRef(); !tt.m[b] { + t.Errorf("didn't see %s (%s)", tt.name, b) + } + } +} + +type summary []byte + +func (s summary) String() string { + const prefix = 10 + plen := prefix + if len(s) < plen { + plen = len(s) + } + return fmt.Sprintf("%d bytes, starting with %q", len(s), []byte(s[:plen])) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/filereader.go b/vendor/github.com/camlistore/camlistore/pkg/schema/filereader.go new file mode 100644 index 00000000..cb805de4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/filereader.go @@ -0,0 +1,395 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/env" + "camlistore.org/pkg/singleflight" + "camlistore.org/pkg/syncutil" + "camlistore.org/pkg/types" +) + +const closedIndex = -1 + +var errClosed = errors.New("filereader is closed") + +// A FileReader reads the bytes of "file" and "bytes" schema blobrefs. +type FileReader struct { + // Immutable stuff: + *io.SectionReader // provides Read, Seek, and Size. + parent *FileReader // or nil. for sub-region readers to find the top. + rootOff int64 // this FileReader's offset from the root + fetcher blob.Fetcher + ss *superset + size int64 // total number of bytes + + sfg singleflight.Group // for loading blobrefs for ssm + + blobmu sync.Mutex // guards lastBlob + lastBlob *blob.Blob // most recently fetched blob; cuts dup reads up to 85x + + ssmmu sync.Mutex // guards ssm + ssm map[blob.Ref]*superset // blobref -> superset +} + +var _ interface { + io.Seeker + io.ReaderAt + io.Reader + io.Closer + Size() int64 +} = (*FileReader)(nil) + +// NewFileReader returns a new FileReader reading the contents of fileBlobRef, +// fetching blobs from fetcher. The fileBlobRef must be of a "bytes" or "file" +// schema blob. +// +// The caller should call Close on the FileReader when done reading. +func NewFileReader(fetcher blob.Fetcher, fileBlobRef blob.Ref) (*FileReader, error) { + // TODO(bradfitz): rename this into bytes reader? but for now it's still + // named FileReader, but can also read a "bytes" schema. + if !fileBlobRef.Valid() { + return nil, errors.New("schema/filereader: NewFileReader blobref invalid") + } + rc, _, err := fetcher.Fetch(fileBlobRef) + if err != nil { + return nil, fmt.Errorf("schema/filereader: fetching file schema blob: %v", err) + } + defer rc.Close() + ss, err := parseSuperset(rc) + if err != nil { + return nil, fmt.Errorf("schema/filereader: decoding file schema blob: %v", err) + } + ss.BlobRef = fileBlobRef + if ss.Type != "file" && ss.Type != "bytes" { + return nil, fmt.Errorf("schema/filereader: expected \"file\" or \"bytes\" schema blob, got %q", ss.Type) + } + fr, err := ss.NewFileReader(fetcher) + if err != nil { + return nil, fmt.Errorf("schema/filereader: creating FileReader for %s: %v", fileBlobRef, err) + } + return fr, nil +} + +func (b *Blob) NewFileReader(fetcher blob.Fetcher) (*FileReader, error) { + return b.ss.NewFileReader(fetcher) +} + +// NewFileReader returns a new FileReader, reading bytes and blobs +// from the provided fetcher. +// +// NewFileReader does no fetch operation on the fetcher itself. The +// fetcher is only used in subsequent read operations. +// +// An error is only returned if the type of the superset is not either +// "file" or "bytes". +func (ss *superset) NewFileReader(fetcher blob.Fetcher) (*FileReader, error) { + if ss.Type != "file" && ss.Type != "bytes" { + return nil, fmt.Errorf("schema/filereader: Superset not of type \"file\" or \"bytes\"") + } + size := int64(ss.SumPartsSize()) + fr := &FileReader{ + fetcher: fetcher, + ss: ss, + size: size, + ssm: make(map[blob.Ref]*superset), + } + fr.SectionReader = io.NewSectionReader(fr, 0, size) + return fr, nil +} + +// LoadAllChunks starts a process of loading all chunks of this file +// as quickly as possible. The contents are immediately discarded, so +// it is assumed that the fetcher is a caching fetcher. +func (fr *FileReader) LoadAllChunks() { + // TODO: ask the underlying blobserver to do this if it would + // prefer. Some blobservers (like blobpacked) might not want + // to do this at all. + go fr.loadAllChunksSync() +} + +func (fr *FileReader) loadAllChunksSync() { + gate := syncutil.NewGate(20) // num readahead chunk loads at a time + fr.ForeachChunk(func(_ []blob.Ref, p BytesPart) error { + if !p.BlobRef.Valid() { + return nil + } + gate.Start() + go func(br blob.Ref) { + defer gate.Done() + rc, _, err := fr.fetcher.Fetch(br) + if err == nil { + defer rc.Close() + var b [1]byte + rc.Read(b[:]) // fault in the blob + } + }(p.BlobRef) + return nil + }) +} + +// UnixMtime returns the file schema's UnixMtime field, or the zero value. +func (fr *FileReader) UnixMtime() time.Time { + t, err := time.Parse(time.RFC3339, fr.ss.UnixMtime) + if err != nil { + return time.Time{} + } + return t +} + +// FileName returns the file schema's filename, if any. +func (fr *FileReader) FileName() string { return fr.ss.FileNameString() } + +func (fr *FileReader) ModTime() time.Time { return fr.ss.ModTime() } + +func (fr *FileReader) SchemaBlobRef() blob.Ref { return fr.ss.BlobRef } + +// Close currently does nothing. +func (fr *FileReader) Close() error { return nil } + +func (fr *FileReader) ReadAt(p []byte, offset int64) (n int, err error) { + if offset < 0 { + return 0, errors.New("schema/filereader: negative offset") + } + if offset >= fr.Size() { + return 0, io.EOF + } + want := len(p) + for len(p) > 0 && err == nil { + rc, err := fr.readerForOffset(offset) + if err != nil { + return n, err + } + var n1 int + n1, err = io.ReadFull(rc, p) + rc.Close() + if err == io.EOF || err == io.ErrUnexpectedEOF { + err = nil + } + if n1 == 0 { + break + } + p = p[n1:] + offset += int64(n1) + n += n1 + } + if n < want && err == nil { + err = io.ErrUnexpectedEOF + } + return n, err +} + +// ForeachChunk calls fn for each chunk of fr, in order. +// +// The schemaPath argument will be the path from the "file" or "bytes" +// schema blob down to possibly other "bytes" schema blobs, the final +// one of which references the given BytesPart. The BytesPart will be +// the actual chunk. The fn function will not be called with +// BytesParts referencing a "BytesRef"; those are followed recursively +// instead. The fn function must not retain or mutate schemaPath. +// +// If fn returns an error, iteration stops and that error is returned +// from ForeachChunk. Other errors may be returned from ForeachChunk +// if schema blob fetches fail. +func (fr *FileReader) ForeachChunk(fn func(schemaPath []blob.Ref, p BytesPart) error) error { + return fr.foreachChunk(fn, nil) +} + +func (fr *FileReader) foreachChunk(fn func([]blob.Ref, BytesPart) error, path []blob.Ref) error { + path = append(path, fr.ss.BlobRef) + for _, bp := range fr.ss.Parts { + if bp.BytesRef.Valid() && bp.BlobRef.Valid() { + return fmt.Errorf("part in %v illegally contained both a blobRef and bytesRef", fr.ss.BlobRef) + } + if bp.BytesRef.Valid() { + ss, err := fr.getSuperset(bp.BytesRef) + if err != nil { + return err + } + subfr, err := ss.NewFileReader(fr.fetcher) + if err != nil { + return err + } + subfr.parent = fr + if err := subfr.foreachChunk(fn, path); err != nil { + return err + } + } else { + if err := fn(path, *bp); err != nil { + return err + } + } + } + return nil +} + +func (fr *FileReader) rootReader() *FileReader { + if fr.parent != nil { + return fr.parent.rootReader() + } + return fr +} + +func (fr *FileReader) getBlob(br blob.Ref) (*blob.Blob, error) { + if root := fr.rootReader(); root != fr { + return root.getBlob(br) + } + fr.blobmu.Lock() + last := fr.lastBlob + fr.blobmu.Unlock() + if last != nil && last.Ref() == br { + return last, nil + } + blob, err := blob.FromFetcher(fr.fetcher, br) + if err != nil { + return nil, err + } + + fr.blobmu.Lock() + fr.lastBlob = blob + fr.blobmu.Unlock() + return blob, nil +} + +func (fr *FileReader) getSuperset(br blob.Ref) (*superset, error) { + if root := fr.rootReader(); root != fr { + return root.getSuperset(br) + } + brStr := br.String() + ssi, err := fr.sfg.Do(brStr, func() (interface{}, error) { + fr.ssmmu.Lock() + ss, ok := fr.ssm[br] + fr.ssmmu.Unlock() + if ok { + return ss, nil + } + rc, _, err := fr.fetcher.Fetch(br) + if err != nil { + return nil, fmt.Errorf("schema/filereader: fetching file schema blob: %v", err) + } + defer rc.Close() + ss, err = parseSuperset(rc) + if err != nil { + return nil, err + } + ss.BlobRef = br + fr.ssmmu.Lock() + defer fr.ssmmu.Unlock() + fr.ssm[br] = ss + return ss, nil + }) + if err != nil { + return nil, err + } + return ssi.(*superset), nil +} + +var debug = env.IsDebug() + +// readerForOffset returns a ReadCloser that reads some number of bytes and then EOF +// from the provided offset. Seeing EOF doesn't mean the end of the whole file; just the +// chunk at that offset. The caller must close the ReadCloser when done reading. +func (fr *FileReader) readerForOffset(off int64) (io.ReadCloser, error) { + if debug { + log.Printf("(%p) readerForOffset %d + %d = %d", fr, fr.rootOff, off, fr.rootOff+off) + } + if off < 0 { + panic("negative offset") + } + if off >= fr.size { + return types.EmptyBody, nil + } + offRemain := off + var skipped int64 + parts := fr.ss.Parts + for len(parts) > 0 && parts[0].Size <= uint64(offRemain) { + offRemain -= int64(parts[0].Size) + skipped += int64(parts[0].Size) + parts = parts[1:] + } + if len(parts) == 0 { + return types.EmptyBody, nil + } + p0 := parts[0] + var rsc types.ReadSeekCloser + var err error + switch { + case p0.BlobRef.Valid() && p0.BytesRef.Valid(): + return nil, fmt.Errorf("part illegally contained both a blobRef and bytesRef") + case !p0.BlobRef.Valid() && !p0.BytesRef.Valid(): + return ioutil.NopCloser( + io.LimitReader(zeroReader{}, + int64(p0.Size-uint64(offRemain)))), nil + case p0.BlobRef.Valid(): + blob, err := fr.getBlob(p0.BlobRef) + if err != nil { + return nil, err + } + rsc = blob.Open() + case p0.BytesRef.Valid(): + var ss *superset + ss, err = fr.getSuperset(p0.BytesRef) + if err != nil { + return nil, err + } + rsc, err = ss.NewFileReader(fr.fetcher) + if err == nil { + subFR := rsc.(*FileReader) + subFR.parent = fr.rootReader() + subFR.rootOff = fr.rootOff + skipped + } + } + if err != nil { + return nil, err + } + offRemain += int64(p0.Offset) + if offRemain > 0 { + newPos, err := rsc.Seek(offRemain, os.SEEK_SET) + if err != nil { + return nil, err + } + if newPos != offRemain { + panic("Seek didn't work") + } + } + return struct { + io.Reader + io.Closer + }{ + io.LimitReader(rsc, int64(p0.Size)), + rsc, + }, nil +} + +type zeroReader struct{} + +func (zeroReader) Read(p []byte) (n int, err error) { + for i := range p { + p[i] = 0 + } + return len(p), nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/filewriter.go b/vendor/github.com/camlistore/camlistore/pkg/schema/filewriter.go new file mode 100644 index 00000000..f0fea393 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/filewriter.go @@ -0,0 +1,469 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "strings" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/rollsum" + "camlistore.org/pkg/syncutil" +) + +const ( + // maxBlobSize is the largest blob we ever make when cutting up + // a file. + maxBlobSize = 1 << 20 + + // firstChunkSize is the ideal size of the first chunk of a + // file. It's kept smaller for the file(1) command, which + // likes to read 96 kB on Linux and 256 kB on OS X. Related + // are tools which extract the EXIF metadata from JPEGs, + // ID3 from mp3s, etc. Nautilus, OS X Finder, etc. + // The first chunk may be larger than this if cutting the file + // here would create a small subsequent chunk (e.g. a file one + // byte larger than firstChunkSize) + firstChunkSize = 256 << 10 + + // bufioReaderSize is an explicit size for our bufio.Reader, + // so we don't rely on NewReader's implicit size. + // We care about the buffer size because it affects how far + // in advance we can detect EOF from an io.Reader that doesn't + // know its size. Detecting an EOF bufioReaderSize bytes early + // means we can plan for the final chunk. + bufioReaderSize = 32 << 10 + + // tooSmallThreshold is the threshold at which rolling checksum + // boundaries are ignored if the current chunk being built is + // smaller than this. + tooSmallThreshold = 64 << 10 +) + +// WriteFileFromReaderWithModTime creates and uploads a "file" JSON schema +// composed of chunks of r, also uploading the chunks. The returned +// BlobRef is of the JSON file schema blob. +// Both filename and modTime are optional. +func WriteFileFromReaderWithModTime(bs blobserver.StatReceiver, filename string, modTime time.Time, r io.Reader) (blob.Ref, error) { + if strings.Contains(filename, "/") { + return blob.Ref{}, fmt.Errorf("schema.WriteFileFromReader: filename %q shouldn't contain a slash", filename) + } + + m := NewFileMap(filename) + if !modTime.IsZero() { + m.SetModTime(modTime) + } + return WriteFileMap(bs, m, r) +} + +// WriteFileFromReader creates and uploads a "file" JSON schema +// composed of chunks of r, also uploading the chunks. The returned +// BlobRef is of the JSON file schema blob. +// The filename is optional. +func WriteFileFromReader(bs blobserver.StatReceiver, filename string, r io.Reader) (blob.Ref, error) { + return WriteFileFromReaderWithModTime(bs, filename, time.Time{}, r) +} + +// WriteFileMap uploads chunks of r to bs while populating file and +// finally uploading file's Blob. The returned blobref is of file's +// JSON blob. +func WriteFileMap(bs blobserver.StatReceiver, file *Builder, r io.Reader) (blob.Ref, error) { + return writeFileMapRolling(bs, file, r) +} + +// This is the simple 1MB chunk version. The rolling checksum version is below. +func writeFileMapOld(bs blobserver.StatReceiver, file *Builder, r io.Reader) (blob.Ref, error) { + parts, size := []BytesPart{}, int64(0) + + var buf bytes.Buffer + for { + buf.Reset() + n, err := io.Copy(&buf, io.LimitReader(r, maxBlobSize)) + if err != nil { + return blob.Ref{}, err + } + if n == 0 { + break + } + + hash := blob.NewHash() + io.Copy(hash, bytes.NewReader(buf.Bytes())) + br := blob.RefFromHash(hash) + hasBlob, err := serverHasBlob(bs, br) + if err != nil { + return blob.Ref{}, err + } + if !hasBlob { + sb, err := bs.ReceiveBlob(br, &buf) + if err != nil { + return blob.Ref{}, err + } + if want := (blob.SizedRef{br, uint32(n)}); sb != want { + return blob.Ref{}, fmt.Errorf("schema/filewriter: wrote %s, expect %s", sb, want) + } + } + + size += n + parts = append(parts, BytesPart{ + BlobRef: br, + Size: uint64(n), + Offset: 0, // into BlobRef to read from (not of dest) + }) + } + + err := file.PopulateParts(size, parts) + if err != nil { + return blob.Ref{}, err + } + + json := file.Blob().JSON() + if err != nil { + return blob.Ref{}, err + } + br := blob.SHA1FromString(json) + sb, err := bs.ReceiveBlob(br, strings.NewReader(json)) + if err != nil { + return blob.Ref{}, err + } + if expect := (blob.SizedRef{br, uint32(len(json))}); expect != sb { + return blob.Ref{}, fmt.Errorf("schema/filewriter: wrote %s bytes, got %s ack'd", expect, sb) + } + + return br, nil +} + +func serverHasBlob(bs blobserver.BlobStatter, br blob.Ref) (have bool, err error) { + _, err = blobserver.StatBlob(bs, br) + if err == nil { + have = true + } else if err == os.ErrNotExist { + err = nil + } + return +} + +type span struct { + from, to int64 + bits int + br blob.Ref + children []span +} + +func (s *span) isSingleBlob() bool { + return len(s.children) == 0 +} + +func (s *span) size() int64 { + size := s.to - s.from + for _, cs := range s.children { + size += cs.size() + } + return size +} + +// noteEOFReader keeps track of when it's seen EOF, but otherwise +// delegates entirely to r. +type noteEOFReader struct { + r io.Reader + sawEOF bool +} + +func (r *noteEOFReader) Read(p []byte) (n int, err error) { + n, err = r.r.Read(p) + if err == io.EOF { + r.sawEOF = true + } + return +} + +func uploadString(bs blobserver.StatReceiver, br blob.Ref, s string) (blob.Ref, error) { + if !br.Valid() { + panic("invalid blobref") + } + hasIt, err := serverHasBlob(bs, br) + if err != nil { + return blob.Ref{}, err + } + if hasIt { + return br, nil + } + _, err = blobserver.ReceiveNoHash(bs, br, strings.NewReader(s)) + if err != nil { + return blob.Ref{}, err + } + return br, nil +} + +// uploadBytes populates bb (a builder of either type "bytes" or +// "file", which is a superset of "bytes"), sets it to the provided +// size, and populates with provided spans. The bytes or file schema +// blob is uploaded and its blobref is returned. +func uploadBytes(bs blobserver.StatReceiver, bb *Builder, size int64, s []span) *uploadBytesFuture { + future := newUploadBytesFuture() + parts := []BytesPart{} + addBytesParts(bs, &parts, s, future) + + if err := bb.PopulateParts(size, parts); err != nil { + future.errc <- err + return future + } + + // Hack until camlistore.org/issue/102 is fixed. If we happen to upload + // the "file" schema before any of its parts arrive, then the indexer + // can get confused. So wait on the parts before, and then upload + // the "file" blob afterwards. + if bb.Type() == "file" { + future.errc <- nil + _, err := future.Get() // may not be nil, if children parts failed + future = newUploadBytesFuture() + if err != nil { + future.errc <- err + return future + } + } + + json := bb.Blob().JSON() + br := blob.SHA1FromString(json) + future.br = br + go func() { + _, err := uploadString(bs, br, json) + future.errc <- err + }() + return future +} + +func newUploadBytesFuture() *uploadBytesFuture { + return &uploadBytesFuture{ + errc: make(chan error, 1), + } +} + +// An uploadBytesFuture is an eager result of a still-in-progress uploadBytes call. +// Call Get to wait and get its final result. +type uploadBytesFuture struct { + br blob.Ref + errc chan error + children []*uploadBytesFuture +} + +// BlobRef returns the optimistic blobref of this uploadBytes call without blocking. +func (f *uploadBytesFuture) BlobRef() blob.Ref { + return f.br +} + +// Get blocks for all children and returns any final error. +func (f *uploadBytesFuture) Get() (blob.Ref, error) { + for _, f := range f.children { + if _, err := f.Get(); err != nil { + return blob.Ref{}, err + } + } + return f.br, <-f.errc +} + +// addBytesParts uploads the provided spans to bs, appending elements to *dst. +func addBytesParts(bs blobserver.StatReceiver, dst *[]BytesPart, spans []span, parent *uploadBytesFuture) { + for _, sp := range spans { + if len(sp.children) == 1 && sp.children[0].isSingleBlob() { + // Remove an occasional useless indirection of + // what would become a bytes schema blob + // pointing to a single blobref. Just promote + // the blobref child instead. + child := sp.children[0] + *dst = append(*dst, BytesPart{ + BlobRef: child.br, + Size: uint64(child.size()), + }) + sp.children = nil + } + if len(sp.children) > 0 { + childrenSize := int64(0) + for _, cs := range sp.children { + childrenSize += cs.size() + } + future := uploadBytes(bs, newBytes(), childrenSize, sp.children) + parent.children = append(parent.children, future) + *dst = append(*dst, BytesPart{ + BytesRef: future.BlobRef(), + Size: uint64(childrenSize), + }) + } + if sp.from == sp.to { + panic("Shouldn't happen. " + fmt.Sprintf("weird span with same from & to: %#v", sp)) + } + *dst = append(*dst, BytesPart{ + BlobRef: sp.br, + Size: uint64(sp.to - sp.from), + }) + } +} + +// writeFileMap uploads chunks of r to bs while populating fileMap and +// finally uploading fileMap. The returned blobref is of fileMap's +// JSON blob. It uses rolling checksum for the chunks sizes. +func writeFileMapRolling(bs blobserver.StatReceiver, file *Builder, r io.Reader) (blob.Ref, error) { + n, spans, err := writeFileChunks(bs, file, r) + if err != nil { + return blob.Ref{}, err + } + // The top-level content parts + return uploadBytes(bs, file, n, spans).Get() +} + +// WriteFileChunks uploads chunks of r to bs while populating file. +// It does not upload file. +func WriteFileChunks(bs blobserver.StatReceiver, file *Builder, r io.Reader) error { + size, spans, err := writeFileChunks(bs, file, r) + if err != nil { + return err + } + parts := []BytesPart{} + future := newUploadBytesFuture() + addBytesParts(bs, &parts, spans, future) + future.errc <- nil // Get will still block on addBytesParts' children + if _, err := future.Get(); err != nil { + return err + } + return file.PopulateParts(size, parts) +} + +func writeFileChunks(bs blobserver.StatReceiver, file *Builder, r io.Reader) (n int64, spans []span, outerr error) { + src := ¬eEOFReader{r: r} + bufr := bufio.NewReaderSize(src, bufioReaderSize) + spans = []span{} // the tree of spans, cut on interesting rollsum boundaries + rs := rollsum.New() + var last int64 + var buf bytes.Buffer + blobSize := 0 // of the next blob being built, should be same as buf.Len() + + const chunksInFlight = 32 // at ~64 KB chunks, this is ~2MB memory per file + gatec := syncutil.NewGate(chunksInFlight) + firsterrc := make(chan error, 1) + + // uploadLastSpan runs in the same goroutine as the loop below and is responsible for + // starting uploading the contents of the buf. It returns false if there's been + // an error and the loop below should be stopped. + uploadLastSpan := func() bool { + chunk := buf.String() + buf.Reset() + br := blob.SHA1FromString(chunk) + spans[len(spans)-1].br = br + select { + case outerr = <-firsterrc: + return false + default: + // No error seen so far, continue. + } + gatec.Start() + go func() { + defer gatec.Done() + if _, err := uploadString(bs, br, chunk); err != nil { + select { + case firsterrc <- err: + default: + } + } + }() + return true + } + + for { + c, err := bufr.ReadByte() + if err == io.EOF { + if n != last { + spans = append(spans, span{from: last, to: n}) + if !uploadLastSpan() { + return + } + } + break + } + if err != nil { + return 0, nil, err + } + + buf.WriteByte(c) + n++ + blobSize++ + rs.Roll(c) + + var bits int + onRollSplit := rs.OnSplit() + switch { + case blobSize == maxBlobSize: + bits = 20 // arbitrary node weight; 1<<20 == 1MB + case src.sawEOF: + // Don't split. End is coming soon enough. + continue + case onRollSplit && n > firstChunkSize && blobSize > tooSmallThreshold: + bits = rs.Bits() + case n == firstChunkSize: + bits = 18 // 1 << 18 == 256KB + default: + // Don't split. + continue + } + blobSize = 0 + + // Take any spans from the end of the spans slice that + // have a smaller 'bits' score and make them children + // of this node. + var children []span + childrenFrom := len(spans) + for childrenFrom > 0 && spans[childrenFrom-1].bits < bits { + childrenFrom-- + } + if nCopy := len(spans) - childrenFrom; nCopy > 0 { + children = make([]span, nCopy) + copy(children, spans[childrenFrom:]) + spans = spans[:childrenFrom] + } + + spans = append(spans, span{from: last, to: n, bits: bits, children: children}) + last = n + if !uploadLastSpan() { + return + } + } + + // Loop was already hit earlier. + if outerr != nil { + return 0, nil, outerr + } + + // Wait for all uploads to finish, one way or another, and then + // see if any generated errors. + // Once this loop is done, we own all the tokens in gatec, so nobody + // else can have one outstanding. + for i := 0; i < chunksInFlight; i++ { + gatec.Start() + } + select { + case err := <-firsterrc: + return 0, nil, err + default: + } + + return n, spans, nil + +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/filewriter_test.go b/vendor/github.com/camlistore/camlistore/pkg/schema/filewriter_test.go new file mode 100644 index 00000000..dda6893d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/filewriter_test.go @@ -0,0 +1,169 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "bytes" + "errors" + "fmt" + "io" + "math/rand" + "sort" + "sync" + "testing" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver/stats" + "camlistore.org/pkg/test" +) + +func TestWriteFileMap(t *testing.T) { + m := NewFileMap("test-file") + r := &randReader{seed: 123, length: 5 << 20} + sr := new(stats.Receiver) + var buf bytes.Buffer + br, err := WriteFileMap(sr, m, io.TeeReader(r, &buf)) + if err != nil { + t.Fatal(err) + } + t.Logf("Got root file %v; %d blobs, %d bytes", br, sr.NumBlobs(), sr.SumBlobSize()) + sizes := sr.Sizes() + t.Logf("Sizes are %v", sizes) + + // TODO(bradfitz): these are fragile tests and mostly just a placeholder. + // Real tests to add: + // -- no "bytes" schema with a single "blobref" + // -- more seeds (including some that tickle the above) + // -- file reader reading back the root gets the same sha1 content back + // (will require keeping the full data in our stats receiver, not + // just the size) + // -- well-balanced tree + // -- nothing too big, nothing too small. + if g, w := br.String(), "sha1-95a5d2686b239e36dff3aeb5a45ed18153121835"; g != w { + t.Errorf("root blobref = %v; want %v", g, w) + } + if g, w := sr.NumBlobs(), 88; g != w { + t.Errorf("num blobs = %v; want %v", g, w) + } + if g, w := sr.SumBlobSize(), int64(5252655); g != w { + t.Errorf("sum blob size = %v; want %v", g, w) + } + if g, w := sizes[len(sizes)-1], 262144; g != w { + t.Errorf("biggest blob is %d; want %d", g, w) + } +} + +func TestWriteThenRead(t *testing.T) { + m := NewFileMap("test-file") + const size = 5 << 20 + r := &randReader{seed: 123, length: size} + sto := new(test.Fetcher) + var buf bytes.Buffer + br, err := WriteFileMap(sto, m, io.TeeReader(r, &buf)) + if err != nil { + t.Fatal(err) + } + + var got bytes.Buffer + fr, err := NewFileReader(sto, br) + if err != nil { + t.Fatal(err) + } + + n, err := io.Copy(&got, fr) + if err != nil { + t.Fatal(err) + } + if n != size { + t.Errorf("read back %d bytes; want %d", n, size) + } + if !bytes.Equal(buf.Bytes(), got.Bytes()) { + t.Error("bytes differ") + } + + var offs []int + + getOffsets := func() error { + offs = offs[:0] + var off int + return fr.ForeachChunk(func(_ []blob.Ref, p BytesPart) error { + offs = append(offs, off) + off += int(p.Size) + return err + }) + } + + if err := getOffsets(); err != nil { + t.Fatal(err) + } + sort.Ints(offs) + wantOffs := "[0 262144 358150 433428 525437 602690 675039 748088 816210 898743 980993 1053410 1120438 1188662 1265192 1332541 1398316 1463899 1530446 1596700 1668839 1738909 1817065 1891025 1961646 2031127 2099232 2170640 2238692 2304743 2374317 2440449 2514327 2582670 2653257 2753975 2827518 2905783 2975426 3053820 3134057 3204879 3271019 3346750 3421351 3487420 3557939 3624006 3701093 3768863 3842013 3918267 4001933 4069157 4139132 4208109 4281390 4348801 4422695 4490535 4568111 4642769 4709005 4785526 4866313 4933575 5005564 5071633 5152695 5227716]" + gotOffs := fmt.Sprintf("%v", offs) + if wantOffs != gotOffs { + t.Errorf("Got chunk offsets %v; want %v", gotOffs, wantOffs) + } + + // Now force a fetch failure on one of the filereader schema chunks, to + // force a failure of GetChunkOffsets + errFetch := errors.New("fake fetch error") + var fetches struct { + sync.Mutex + n int + } + sto.FetchErr = func() error { + fetches.Lock() + defer fetches.Unlock() + fetches.n++ + if fetches.n == 1 { + return nil + } + return errFetch + } + + fr, err = NewFileReader(sto, br) + if err != nil { + t.Fatal(err) + } + if err := getOffsets(); fmt.Sprint(err) != "schema/filereader: fetching file schema blob: fake fetch error" { + t.Errorf("expected second call of GetChunkOffsets to return wrapped errFetch; got %v", err) + } +} + +type randReader struct { + seed int64 + length int + rnd *rand.Rand // lazy init + remain int // lazy init +} + +func (r *randReader) Read(p []byte) (n int, err error) { + if r.rnd == nil { + r.rnd = rand.New(rand.NewSource(r.seed)) + r.remain = r.length + } + if r.remain == 0 { + return 0, io.EOF + } + if len(p) > r.remain { + p = p[:r.remain] + } + for i := range p { + p[i] = byte(r.rnd.Intn(256)) + } + r.remain -= len(p) + return len(p), nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/lookup.go b/vendor/github.com/camlistore/camlistore/pkg/schema/lookup.go new file mode 100644 index 00000000..5860b0cb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/lookup.go @@ -0,0 +1,171 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "bufio" + "os" + "os/user" + "strconv" + "strings" + "sync" +) + +type intBool struct { + int + bool +} + +var ( + lookupMu sync.RWMutex // guards rest + uidName = map[int]string{} + gidName = map[int]string{} + userUid = map[string]intBool{} + groupGid = map[string]intBool{} + + parsedGroups, parsedPasswd bool +) + +func getUserFromUid(id int) string { + return cachedName(id, uidName, lookupUserid) +} + +func getGroupFromGid(id int) string { + return cachedName(id, gidName, lookupGroupId) +} + +func getUidFromName(user string) (int, bool) { + return cachedId(user, userUid, lookupUserToId) +} + +func getGidFromName(group string) (int, bool) { + return cachedId(group, groupGid, lookupGroupToId) +} + +func cachedName(id int, m map[int]string, fn func(int) string) string { + // TODO: use singleflight library here, keyed by 'id', rather than this lookupMu lock, + // which is too coarse. + lookupMu.RLock() + name, ok := m[id] + lookupMu.RUnlock() + if ok { + return name + } + lookupMu.Lock() + defer lookupMu.Unlock() + name, ok = m[id] + if ok { + return name // lost race, already populated + } + m[id] = fn(id) + return m[id] +} + +func cachedId(name string, m map[string]intBool, fn func(string) (int, bool)) (int, bool) { + // TODO: use singleflight library here, keyed by 'name', rather than this lookupMu lock, + // which is too coarse. + lookupMu.RLock() + intb, ok := m[name] + lookupMu.RUnlock() + if ok { + return intb.int, intb.bool + } + lookupMu.Lock() + defer lookupMu.Unlock() + intb, ok = m[name] + if ok { + return intb.int, intb.bool // lost race, already populated + } + id, ok := fn(name) + m[name] = intBool{id, ok} + return id, ok +} + +func lookupUserToId(name string) (uid int, ok bool) { + u, err := user.Lookup(name) + if err == nil { + uid, err := strconv.Atoi(u.Uid) + if err == nil { + return uid, true + } + } + return +} + +func lookupGroupToId(group string) (gid int, ok bool) { + if !parsedGroups { + lookupGroupId(0) // force them to be loaded + } + intb := groupGid[group] + return intb.int, intb.bool +} + +// lookupMu is held +func lookupGroupId(id int) string { + if parsedGroups { + return "" + } + parsedGroups = true + populateMap(gidName, groupGid, "/etc/group") + return gidName[id] +} + +// lookupMu is held +func lookupUserid(id int) string { + u, err := user.LookupId(strconv.Itoa(id)) + if err == nil { + return u.Username + } + if _, ok := err.(user.UnknownUserIdError); ok { + return "" + } + if parsedPasswd { + return "" + } + parsedPasswd = true + populateMap(uidName, nil, "/etc/passwd") + return uidName[id] +} + +// Lame fallback parsing /etc/password for non-cgo systems where os/user doesn't work, +// and used for groups (which also happens to work on OS X, generally) +// nameMap may be nil. +func populateMap(m map[int]string, nameMap map[string]intBool, file string) { + f, err := os.Open(file) + if err != nil { + return + } + defer f.Close() + bufr := bufio.NewReader(f) + for { + line, err := bufr.ReadString('\n') + if err != nil { + return + } + parts := strings.SplitN(line, ":", 4) + if len(parts) >= 3 { + idstr := parts[2] + id, err := strconv.Atoi(idstr) + if err == nil { + m[id] = parts[0] + if nameMap != nil { + nameMap[parts[0]] = intBool{id, true} + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/nodeattr/nodeattr.go b/vendor/github.com/camlistore/camlistore/pkg/schema/nodeattr/nodeattr.go new file mode 100644 index 00000000..7277067e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/nodeattr/nodeattr.go @@ -0,0 +1,106 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package nodeattr contains constants for permanode attribute names. +// +// For all date values in RFC 3339 format, Camlistore additionally +// treats the special timezone offset -00:01 (one minute west of UTC) +// as meaning that the local time was known, but the location or +// timezone was not. Usually this is from EXIF files. +package nodeattr + +const ( + // Type is the Camlistore permanode type ("camliNodeType"). + // Importer-specific ones are of the form "domain.com:objecttype". + // Well-defined ones are documented in doc/schema/claims/attributes.txt. + Type = "camliNodeType" + + // CamliContent is "camliContent", the blobref of the permanode's content. + // For files or images, the camliContent is fileref (the blobref of + // the "file" schema blob). + CamliContent = "camliContent" + + // CamliContentImage is "camliContentImage", for when CamliContent is + // already set to the blobref of a non-image. + CamliContentImage = "camliContentImage" + + // DateCreated is http://schema.org/dateCreated in RFC 3339 + // format. + DateCreated = "dateCreated" + + // StartDate is http://schema.org/startDate, the start date + // and time of the event or item, in RFC 3339 format. + StartDate = "startDate" + + // DateModified is http://schema.org/dateModified, in RFC 3339 + // format. + DateModified = "dateModified" + + // DatePublished is http://schema.org/datePublished in RFC + // 3339 format. + DatePublished = "datePublished" + + // Title is http://schema.org/title + Title = "title" + + // Description is http://schema.org/description + // Value is plain text, no HTML, newlines are newlines. + Description = "description" + + // Content is "content", used e.g. for the content of a tweet. + // TODO: define this more + Content = "content" + + // URL is the item's original or origin URL. + URL = "url" + + // LocationText is free-flowing text definition of a location or place, such + // as a city name, or a full postal address. + LocationText = "locationText" + + Latitude = "latitude" + Longitude = "longitude" + + // StreetAddress is http://schema.org/streetAddress + StreetAddress = "streetAddress" + + // AddressLocality is http://schema.org/addressLocality + // City, town, village, etc. name, plus any additional locality + // information, such as suburb name. Not as restricted as + // the UK postal meaning. + AddressLocality = "addressLocality" + + // PostalCode is http://schema.org/postalCode + PostalCode = "postalCode" + + // AddressRegion is http://schema.org/addressRegion + // Region, or state name. + AddressRegion = "addressRegion" + + // AddressCountry is http://schema.org/addressCountry + AddressCountry = "addressCountry" + + // CamliPathOrderColon is the prefix "camliPathOrder:". + // The attribute key should be followed by a uint64. The attribute value + // is an existing value of a camliPath element. + // CamliPathOrder optionally sorts sets already using "camliPath:foo" keys. + // The integers do not need to be contiguous, nor 0- (or 1-) based. + CamliPathOrderColon = "camliPathOrder:" + + // DefaultVisibility is "camliDefVis", which affects the default + // visibility of the concerned permanode in the web UI. + DefaultVisibility = "camliDefVis" +) diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/schema.go b/vendor/github.com/camlistore/camlistore/pkg/schema/schema.go new file mode 100644 index 00000000..75e18fb1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/schema.go @@ -0,0 +1,1056 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package schema manipulates Camlistore schema blobs. +// +// A schema blob is a JSON-encoded blob that describes other blobs. +// See documentation in Camlistore's doc/schema/ directory. +package schema + +import ( + "bytes" + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "hash" + "io" + "log" + "os" + "reflect" + "regexp" + "strconv" + "strings" + "sync" + "time" + "unicode/utf8" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/strutil" + "camlistore.org/pkg/types" + "camlistore.org/third_party/github.com/bradfitz/latlong" + "camlistore.org/third_party/github.com/rwcarlsen/goexif/exif" + "camlistore.org/third_party/github.com/rwcarlsen/goexif/tiff" +) + +func init() { + // Intern common strings as used by schema blobs (camliType values), to reduce + // index memory usage, which uses strutil.StringFromBytes. + strutil.RegisterCommonString( + "bytes", + "claim", + "directory", + "file", + "permanode", + "share", + "static-set", + "symlink", + ) +} + +// MaxSchemaBlobSize represents the upper bound for how large +// a schema blob may be. +const MaxSchemaBlobSize = 1 << 20 + +var sha1Type = reflect.TypeOf(sha1.New()) + +var ( + ErrNoCamliVersion = errors.New("schema: no camliVersion key in map") +) + +var clockNow = time.Now + +type StatHasher interface { + Lstat(fileName string) (os.FileInfo, error) + Hash(fileName string) (blob.Ref, error) +} + +// File is the interface returned when opening a DirectoryEntry that +// is a regular file. +type File interface { + io.Closer + io.ReaderAt + io.Reader + Size() int64 +} + +// Directory is a read-only interface to a "directory" schema blob. +type Directory interface { + // Readdir reads the contents of the directory associated with dr + // and returns an array of up to n DirectoryEntries structures. + // Subsequent calls on the same file will yield further + // DirectoryEntries. + // If n > 0, Readdir returns at most n DirectoryEntry structures. In + // this case, if Readdir returns an empty slice, it will return + // a non-nil error explaining why. At the end of a directory, + // the error is os.EOF. + // If n <= 0, Readdir returns all the DirectoryEntries from the + // directory in a single slice. In this case, if Readdir succeeds + // (reads all the way to the end of the directory), it returns the + // slice and a nil os.Error. If it encounters an error before the + // end of the directory, Readdir returns the DirectoryEntry read + // until that point and a non-nil error. + Readdir(n int) ([]DirectoryEntry, error) +} + +type Symlink interface { + // .. TODO +} + +// FIFO is the read-only interface to a "fifo" schema blob. +type FIFO interface { + // .. TODO +} + +// Socket is the read-only interface to a "socket" schema blob. +type Socket interface { + // .. TODO +} + +// DirectoryEntry is a read-only interface to an entry in a (static) +// directory. +type DirectoryEntry interface { + // CamliType returns the schema blob's "camliType" field. + // This may be "file", "directory", "symlink", or other more + // obscure types added in the future. + CamliType() string + + FileName() string + BlobRef() blob.Ref + + File() (File, error) // if camliType is "file" + Directory() (Directory, error) // if camliType is "directory" + Symlink() (Symlink, error) // if camliType is "symlink" + FIFO() (FIFO, error) // if camliType is "fifo" + Socket() (Socket, error) // If camliType is "socket" +} + +// dirEntry is the default implementation of DirectoryEntry +type dirEntry struct { + ss superset + fetcher blob.Fetcher + fr *FileReader // or nil if not a file + dr *DirReader // or nil if not a directory +} + +// A SearchQuery must be of type *search.SearchQuery. +// This type breaks an otherwise-circular dependency. +type SearchQuery interface{} + +func (de *dirEntry) CamliType() string { + return de.ss.Type +} + +func (de *dirEntry) FileName() string { + return de.ss.FileNameString() +} + +func (de *dirEntry) BlobRef() blob.Ref { + return de.ss.BlobRef +} + +func (de *dirEntry) File() (File, error) { + if de.fr == nil { + if de.ss.Type != "file" { + return nil, fmt.Errorf("DirectoryEntry is camliType %q, not %q", de.ss.Type, "file") + } + fr, err := NewFileReader(de.fetcher, de.ss.BlobRef) + if err != nil { + return nil, err + } + de.fr = fr + } + return de.fr, nil +} + +func (de *dirEntry) Directory() (Directory, error) { + if de.dr == nil { + if de.ss.Type != "directory" { + return nil, fmt.Errorf("DirectoryEntry is camliType %q, not %q", de.ss.Type, "directory") + } + dr, err := NewDirReader(de.fetcher, de.ss.BlobRef) + if err != nil { + return nil, err + } + de.dr = dr + } + return de.dr, nil +} + +func (de *dirEntry) Symlink() (Symlink, error) { + return 0, errors.New("TODO: Symlink not implemented") +} + +func (de *dirEntry) FIFO() (FIFO, error) { + return 0, errors.New("TODO: FIFO not implemented") +} + +func (de *dirEntry) Socket() (Socket, error) { + return 0, errors.New("TODO: Socket not implemented") +} + +// newDirectoryEntry takes a superset and returns a DirectoryEntry if +// the Supserset is valid and represents an entry in a directory. It +// must by of type "file", "directory", "symlink" or "socket". +// TODO: "char", block", probably. later. +func newDirectoryEntry(fetcher blob.Fetcher, ss *superset) (DirectoryEntry, error) { + if ss == nil { + return nil, errors.New("ss was nil") + } + if !ss.BlobRef.Valid() { + return nil, errors.New("ss.BlobRef was invalid") + } + switch ss.Type { + case "file", "directory", "symlink", "fifo", "socket": + // Okay + default: + return nil, fmt.Errorf("invalid DirectoryEntry camliType of %q", ss.Type) + } + de := &dirEntry{ss: *ss, fetcher: fetcher} // defensive copy + return de, nil +} + +// NewDirectoryEntryFromBlobRef takes a BlobRef and returns a +// DirectoryEntry if the BlobRef contains a type "file", "directory", +// "symlink", "fifo" or "socket". +// TODO: ""char", "block", probably. later. +func NewDirectoryEntryFromBlobRef(fetcher blob.Fetcher, blobRef blob.Ref) (DirectoryEntry, error) { + ss := new(superset) + err := ss.setFromBlobRef(fetcher, blobRef) + if err != nil { + return nil, fmt.Errorf("schema/filereader: can't fill superset: %v\n", err) + } + return newDirectoryEntry(fetcher, ss) +} + +// superset represents the superset of common Camlistore JSON schema +// keys as a convenient json.Unmarshal target. +// TODO(bradfitz): unexport this type. Getting too gross. Move to schema.Blob +type superset struct { + // BlobRef isn't for a particular metadata blob field, but included + // for convenience. + BlobRef blob.Ref + + Version int `json:"camliVersion"` + Type string `json:"camliType"` + + Signer blob.Ref `json:"camliSigner"` + Sig string `json:"camliSig"` + + ClaimType string `json:"claimType"` + ClaimDate types.Time3339 `json:"claimDate"` + + Permanode blob.Ref `json:"permaNode"` + Attribute string `json:"attribute"` + Value string `json:"value"` + + // FileName and FileNameBytes represent one of the two + // representations of file names in schema blobs. They should + // not be accessed directly. Use the FileNameString accessor + // instead, which also sanitizes malicious values. + FileName string `json:"fileName"` + FileNameBytes []interface{} `json:"fileNameBytes"` + + SymlinkTarget string `json:"symlinkTarget"` + SymlinkTargetBytes []interface{} `json:"symlinkTargetBytes"` + + UnixPermission string `json:"unixPermission"` + UnixOwnerId int `json:"unixOwnerId"` + UnixOwner string `json:"unixOwner"` + UnixGroupId int `json:"unixGroupId"` + UnixGroup string `json:"unixGroup"` + UnixMtime string `json:"unixMtime"` + UnixCtime string `json:"unixCtime"` + UnixAtime string `json:"unixAtime"` + + // Parts are references to the data chunks of a regular file (or a "bytes" schema blob). + // See doc/schema/bytes.txt and doc/schema/files/file.txt. + Parts []*BytesPart `json:"parts"` + + Entries blob.Ref `json:"entries"` // for directories, a blobref to a static-set + Members []blob.Ref `json:"members"` // for static sets (for directory static-sets: blobrefs to child dirs/files) + + // Search allows a "share" blob to share an entire search. Contrast with "target". + Search SearchQuery `json:"search"` + // Target is a "share" blob's target (the thing being shared) + // Or it is the object being deleted in a DeleteClaim claim. + Target blob.Ref `json:"target"` + // Transitive is a property of a "share" blob. + Transitive bool `json:"transitive"` + // AuthType is a "share" blob's authentication type that is required. + // Currently (2013-01-02) just "haveref" (if you know the share's blobref, + // you get access: the secret URL model) + AuthType string `json:"authType"` + Expires types.Time3339 `json:"expires"` // or zero for no expiration +} + +func parseSuperset(r io.Reader) (*superset, error) { + var ss superset + if err := json.NewDecoder(io.LimitReader(r, MaxSchemaBlobSize)).Decode(&ss); err != nil { + return nil, err + } + return &ss, nil +} + +// BlobReader returns a new Blob from the provided Reader r, +// which should be the body of the provided blobref. +// Note: the hash checksum is not verified. +func BlobFromReader(ref blob.Ref, r io.Reader) (*Blob, error) { + if !ref.Valid() { + return nil, errors.New("schema.BlobFromReader: invalid blobref") + } + var buf bytes.Buffer + tee := io.TeeReader(r, &buf) + ss, err := parseSuperset(tee) + if err != nil { + return nil, err + } + var wb [16]byte + afterObj := 0 + for { + n, err := tee.Read(wb[:]) + afterObj += n + for i := 0; i < n; i++ { + if !isASCIIWhite(wb[i]) { + return nil, fmt.Errorf("invalid bytes after JSON schema blob in %v", ref) + } + } + if afterObj > MaxSchemaBlobSize { + break + } + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + } + json := buf.String() + if len(json) > MaxSchemaBlobSize { + return nil, fmt.Errorf("schema: metadata blob %v is over expected limit; size=%d", ref, len(json)) + } + return &Blob{ref, json, ss}, nil +} + +func isASCIIWhite(b byte) bool { + switch b { + case ' ', '\t', '\r', '\n': + return true + } + return false +} + +// BytesPart is the type representing one of the "parts" in a "file" +// or "bytes" JSON schema. +// +// See doc/schema/bytes.txt and doc/schema/files/file.txt. +type BytesPart struct { + // Size is the number of bytes that this part contributes to the overall segment. + Size uint64 `json:"size"` + + // At most one of BlobRef or BytesRef must be non-zero + // (Valid), but it's illegal for both. + // If neither are set, this BytesPart represents Size zero bytes. + // BlobRef refers to raw bytes. BytesRef references a "bytes" schema blob. + BlobRef blob.Ref `json:"blobRef,omitempty"` + BytesRef blob.Ref `json:"bytesRef,omitempty"` + + // Offset optionally specifies the offset into BlobRef to skip + // when reading Size bytes. + Offset uint64 `json:"offset,omitempty"` +} + +// stringFromMixedArray joins a slice of either strings or float64 +// values (as retrieved from JSON decoding) into a string. These are +// used for non-UTF8 filenames in "fileNameBytes" fields. The strings +// are UTF-8 segments and the float64s (actually uint8 values) are +// byte values. +func stringFromMixedArray(parts []interface{}) string { + var buf bytes.Buffer + for _, part := range parts { + if s, ok := part.(string); ok { + buf.WriteString(s) + continue + } + if num, ok := part.(float64); ok { + buf.WriteByte(byte(num)) + continue + } + } + return buf.String() +} + +// mixedArrayFromString is the inverse of stringFromMixedArray. It +// splits a string to a series of either UTF-8 strings and non-UTF-8 +// bytes. +func mixedArrayFromString(s string) (parts []interface{}) { + for len(s) > 0 { + if n := utf8StrLen(s); n > 0 { + parts = append(parts, s[:n]) + s = s[n:] + } else { + parts = append(parts, s[0]) + s = s[1:] + } + } + return parts +} + +// utf8StrLen returns how many prefix bytes of s are valid UTF-8. +func utf8StrLen(s string) int { + for i, r := range s { + for r == utf8.RuneError { + // The RuneError value can be an error + // sentinel value (if it's size 1) or the same + // value encoded properly. Decode it to see if + // it's the 1 byte sentinel value. + _, size := utf8.DecodeRuneInString(s[i:]) + if size == 1 { + return i + } + } + } + return len(s) +} + +func (ss *superset) SumPartsSize() (size uint64) { + for _, part := range ss.Parts { + size += uint64(part.Size) + } + return size +} + +func (ss *superset) SymlinkTargetString() string { + if ss.SymlinkTarget != "" { + return ss.SymlinkTarget + } + return stringFromMixedArray(ss.SymlinkTargetBytes) +} + +// FileNameString returns the schema blob's base filename. +// +// If the fileName field of the blob accidentally or maliciously +// contains a slash, this function returns an empty string instead. +func (ss *superset) FileNameString() string { + v := ss.FileName + if v == "" { + v = stringFromMixedArray(ss.FileNameBytes) + } + if v != "" { + if strings.Index(v, "/") != -1 { + // Bogus schema blob; ignore. + return "" + } + if strings.Index(v, "\\") != -1 { + // Bogus schema blob; ignore. + return "" + } + } + return v +} + +func (ss *superset) HasFilename(name string) bool { + return ss.FileNameString() == name +} + +func (b *Blob) FileMode() os.FileMode { + // TODO: move this to a different type, off *Blob + return b.ss.FileMode() +} + +func (ss *superset) FileMode() os.FileMode { + var mode os.FileMode + hasPerm := ss.UnixPermission != "" + if hasPerm { + m64, err := strconv.ParseUint(ss.UnixPermission, 8, 64) + if err == nil { + mode = mode | os.FileMode(m64) + } + } + + // TODO: add other types (block, char, etc) + switch ss.Type { + case "directory": + mode = mode | os.ModeDir + case "file": + // No extra bit. + case "symlink": + mode = mode | os.ModeSymlink + case "fifo": + mode = mode | os.ModeNamedPipe + case "socket": + mode = mode | os.ModeSocket + } + if !hasPerm { + switch ss.Type { + case "directory": + mode |= 0755 + default: + mode |= 0644 + } + } + return mode +} + +// MapUid returns the most appropriate mapping from this file's owner +// to the local machine's owner, trying first a match by name, +// followed by just mapping the number through directly. +func (b *Blob) MapUid() int { return b.ss.MapUid() } + +// MapGid returns the most appropriate mapping from this file's group +// to the local machine's group, trying first a match by name, +// followed by just mapping the number through directly. +func (b *Blob) MapGid() int { return b.ss.MapGid() } + +func (ss *superset) MapUid() int { + if ss.UnixOwner != "" { + uid, ok := getUidFromName(ss.UnixOwner) + if ok { + return uid + } + } + return ss.UnixOwnerId // TODO: will be 0 if unset, which isn't ideal +} + +func (ss *superset) MapGid() int { + if ss.UnixGroup != "" { + gid, ok := getGidFromName(ss.UnixGroup) + if ok { + return gid + } + } + return ss.UnixGroupId // TODO: will be 0 if unset, which isn't ideal +} + +func (ss *superset) ModTime() time.Time { + if ss.UnixMtime == "" { + return time.Time{} + } + t, err := time.Parse(time.RFC3339, ss.UnixMtime) + if err != nil { + return time.Time{} + } + return t +} + +var DefaultStatHasher = &defaultStatHasher{} + +type defaultStatHasher struct{} + +func (d *defaultStatHasher) Lstat(fileName string) (os.FileInfo, error) { + return os.Lstat(fileName) +} + +func (d *defaultStatHasher) Hash(fileName string) (blob.Ref, error) { + s1 := sha1.New() + file, err := os.Open(fileName) + if err != nil { + return blob.Ref{}, err + } + defer file.Close() + _, err = io.Copy(s1, file) + if err != nil { + return blob.Ref{}, err + } + return blob.RefFromHash(s1), nil +} + +type StaticSet struct { + l sync.Mutex + refs []blob.Ref +} + +func (ss *StaticSet) Add(ref blob.Ref) { + ss.l.Lock() + defer ss.l.Unlock() + ss.refs = append(ss.refs, ref) +} + +func base(version int, ctype string) *Builder { + return &Builder{map[string]interface{}{ + "camliVersion": version, + "camliType": ctype, + }} +} + +// NewUnsignedPermanode returns a new random permanode, not yet signed. +func NewUnsignedPermanode() *Builder { + bb := base(1, "permanode") + chars := make([]byte, 20) + _, err := io.ReadFull(rand.Reader, chars) + if err != nil { + panic("error reading random bytes: " + err.Error()) + } + bb.m["random"] = base64.StdEncoding.EncodeToString(chars) + return bb +} + +// NewPlannedPermanode returns a permanode with a fixed key. Like +// NewUnsignedPermanode, this builder is also not yet signed. Callers of +// NewPlannedPermanode must sign the map with a fixed claimDate and +// GPG date to create consistent JSON encodings of the Map (its +// blobref), between runs. +func NewPlannedPermanode(key string) *Builder { + bb := base(1, "permanode") + bb.m["key"] = key + return bb +} + +// NewHashPlannedPermanode returns a planned permanode with the sum +// of the hash, prefixed with "sha1-", as the key. +func NewHashPlannedPermanode(h hash.Hash) *Builder { + if reflect.TypeOf(h) != sha1Type { + panic("Hash not supported. Only sha1 for now.") + } + return NewPlannedPermanode(fmt.Sprintf("sha1-%x", h.Sum(nil))) +} + +// Map returns a Camli map of camliType "static-set" +// TODO: delete this method +func (ss *StaticSet) Blob() *Blob { + bb := base(1, "static-set") + ss.l.Lock() + defer ss.l.Unlock() + + members := make([]string, 0, len(ss.refs)) + if ss.refs != nil { + for _, ref := range ss.refs { + members = append(members, ref.String()) + } + } + bb.m["members"] = members + return bb.Blob() +} + +// JSON returns the map m encoded as JSON in its +// recommended canonical form. The canonical form is readable with newlines and indentation, +// and always starts with the header bytes: +// +// {"camliVersion": +// +func mapJSON(m map[string]interface{}) (string, error) { + version, hasVersion := m["camliVersion"] + if !hasVersion { + return "", ErrNoCamliVersion + } + delete(m, "camliVersion") + jsonBytes, err := json.MarshalIndent(m, "", " ") + if err != nil { + return "", err + } + m["camliVersion"] = version + var buf bytes.Buffer + fmt.Fprintf(&buf, "{\"camliVersion\": %v,\n", version) + buf.Write(jsonBytes[2:]) + return buf.String(), nil +} + +// NewFileMap returns a new builder of a type "file" schema for the provided fileName. +// The chunk parts of the file are not populated. +func NewFileMap(fileName string) *Builder { + return newCommonFilenameMap(fileName).SetType("file") +} + +// NewDirMap returns a new builder of a type "directory" schema for the provided fileName. +func NewDirMap(fileName string) *Builder { + return newCommonFilenameMap(fileName).SetType("directory") +} + +func newCommonFilenameMap(fileName string) *Builder { + bb := base(1, "" /* no type yet */) + if fileName != "" { + bb.SetFileName(fileName) + } + return bb +} + +var populateSchemaStat []func(schemaMap map[string]interface{}, fi os.FileInfo) + +func NewCommonFileMap(fileName string, fi os.FileInfo) *Builder { + bb := newCommonFilenameMap(fileName) + // Common elements (from file-common.txt) + if fi.Mode()&os.ModeSymlink == 0 { + bb.m["unixPermission"] = fmt.Sprintf("0%o", fi.Mode().Perm()) + } + + // OS-specific population; defined in schema_posix.go, etc. (not on App Engine) + for _, f := range populateSchemaStat { + f(bb.m, fi) + } + + if mtime := fi.ModTime(); !mtime.IsZero() { + bb.m["unixMtime"] = RFC3339FromTime(mtime) + } + return bb +} + +// PopulateParts sets the "parts" field of the blob with the provided +// parts. The sum of the sizes of parts must match the provided size +// or an error is returned. Also, each BytesPart may only contain either +// a BytesPart or a BlobRef, but not both. +func (bb *Builder) PopulateParts(size int64, parts []BytesPart) error { + return populateParts(bb.m, size, parts) +} + +func populateParts(m map[string]interface{}, size int64, parts []BytesPart) error { + sumSize := int64(0) + mparts := make([]map[string]interface{}, len(parts)) + for idx, part := range parts { + mpart := make(map[string]interface{}) + mparts[idx] = mpart + switch { + case part.BlobRef.Valid() && part.BytesRef.Valid(): + return errors.New("schema: part contains both BlobRef and BytesRef") + case part.BlobRef.Valid(): + mpart["blobRef"] = part.BlobRef.String() + case part.BytesRef.Valid(): + mpart["bytesRef"] = part.BytesRef.String() + default: + return errors.New("schema: part must contain either a BlobRef or BytesRef") + } + mpart["size"] = part.Size + sumSize += int64(part.Size) + if part.Offset != 0 { + mpart["offset"] = part.Offset + } + } + if sumSize != size { + return fmt.Errorf("schema: declared size %d doesn't match sum of parts size %d", size, sumSize) + } + m["parts"] = mparts + return nil +} + +func newBytes() *Builder { + return base(1, "bytes") +} + +// ClaimType is one of the valid "claimType" fields in a "claim" schema blob. See doc/schema/claims/. +type ClaimType string + +const ( + SetAttributeClaim ClaimType = "set-attribute" + AddAttributeClaim ClaimType = "add-attribute" + DelAttributeClaim ClaimType = "del-attribute" + ShareClaim ClaimType = "share" + // DeleteClaim deletes a permanode or another claim. + // A delete claim can itself be deleted, and so on. + DeleteClaim ClaimType = "delete" +) + +// claimParam is used to populate a claim map when building a new claim +type claimParam struct { + claimType ClaimType + + // Params specific to *Attribute claims: + permanode blob.Ref // modified permanode + attribute string // required + value string // optional if Type == DelAttributeClaim + + // Params specific to ShareClaim claims: + authType string + transitive bool + shareExpires time.Time // Zero means no expiration + + // Params specific to ShareClaim and DeleteClaim claims. + target blob.Ref +} + +func newClaim(claims ...*claimParam) *Builder { + bb := base(1, "claim") + bb.SetClaimDate(clockNow()) + if len(claims) == 1 { + cp := claims[0] + populateClaimMap(bb.m, cp) + return bb + } + var claimList []interface{} + for _, cp := range claims { + m := map[string]interface{}{} + populateClaimMap(m, cp) + claimList = append(claimList, m) + } + bb.m["claimType"] = "multi" + bb.m["claims"] = claimList + return bb +} + +func populateClaimMap(m map[string]interface{}, cp *claimParam) { + m["claimType"] = string(cp.claimType) + switch cp.claimType { + case ShareClaim: + m["authType"] = cp.authType + m["transitive"] = cp.transitive + case DeleteClaim: + m["target"] = cp.target.String() + default: + m["permaNode"] = cp.permanode.String() + m["attribute"] = cp.attribute + if !(cp.claimType == DelAttributeClaim && cp.value == "") { + m["value"] = cp.value + } + } +} + +// NewShareRef creates a *Builder for a "share" claim. +func NewShareRef(authType string, transitive bool) *Builder { + return newClaim(&claimParam{ + claimType: ShareClaim, + authType: authType, + transitive: transitive, + }) +} + +func NewSetAttributeClaim(permaNode blob.Ref, attr, value string) *Builder { + return newClaim(&claimParam{ + permanode: permaNode, + claimType: SetAttributeClaim, + attribute: attr, + value: value, + }) +} + +func NewAddAttributeClaim(permaNode blob.Ref, attr, value string) *Builder { + return newClaim(&claimParam{ + permanode: permaNode, + claimType: AddAttributeClaim, + attribute: attr, + value: value, + }) +} + +// NewDelAttributeClaim creates a new claim to remove value from the +// values set for the attribute attr of permaNode. If value is empty then +// all the values for attribute are cleared. +func NewDelAttributeClaim(permaNode blob.Ref, attr, value string) *Builder { + return newClaim(&claimParam{ + permanode: permaNode, + claimType: DelAttributeClaim, + attribute: attr, + value: value, + }) +} + +// NewDeleteClaim creates a new claim to delete a target claim or permanode. +func NewDeleteClaim(target blob.Ref) *Builder { + return newClaim(&claimParam{ + target: target, + claimType: DeleteClaim, + }) +} + +// ShareHaveRef is the auth type specifying that if you "have the +// reference" (know the blobref to the haveref share blob), then you +// have access to the referenced object from that share blob. +// This is the "send a link to a friend" access model. +const ShareHaveRef = "haveref" + +// UnknownLocation is a magic timezone value used when the actual location +// of a time is unknown. For instance, EXIF files commonly have a time without +// a corresponding location or timezone offset. +var UnknownLocation = time.FixedZone("Unknown", -60) // 1 minute west + +// IsZoneKnown reports whether t is in a known timezone. +// Camlistore uses the magic timezone offset of 1 minute west of UTC +// to mean that the timezone wasn't known. +func IsZoneKnown(t time.Time) bool { + if t.Location() == UnknownLocation { + return false + } + if _, off := t.Zone(); off == -60 { + return false + } + return true +} + +// RFC3339FromTime returns an RFC3339-formatted time. +// +// If the timezone is known, the time will be converted to UTC and +// returned with a "Z" suffix. For unknown zones, the timezone will be +// "-00:01" (1 minute west of UTC). +// +// Fractional seconds are only included if the time has fractional +// seconds. +func RFC3339FromTime(t time.Time) string { + if IsZoneKnown(t) { + t = t.UTC() + } + if t.UnixNano()%1e9 == 0 { + return t.Format(time.RFC3339) + } + return t.Format(time.RFC3339Nano) +} + +var bytesCamliVersion = []byte("camliVersion") + +// LikelySchemaBlob returns quickly whether buf likely contains (or is +// the prefix of) a schema blob. +func LikelySchemaBlob(buf []byte) bool { + if len(buf) == 0 || buf[0] != '{' { + return false + } + return bytes.Contains(buf, bytesCamliVersion) +} + +// findSize checks if v is an *os.File or if it has +// a Size() int64 method, to find its size. +// It returns 0, false otherwise. +func findSize(v interface{}) (size int64, ok bool) { + if fi, ok := v.(*os.File); ok { + v, _ = fi.Stat() + } + if sz, ok := v.(interface { + Size() int64 + }); ok { + return sz.Size(), true + } + // For bytes.Reader, strings.Reader, etc: + if li, ok := v.(interface { + Len() int + }); ok { + ln := int64(li.Len()) // unread portion, typically + // If it's also a seeker, remove add any seek offset: + if sk, ok := v.(io.Seeker); ok { + if cur, err := sk.Seek(0, 1); err == nil { + ln += cur + } + } + return ln, true + } + return 0, false +} + +// FileTime returns the best guess of the file's creation time (or modtime). +// If the file doesn't have its own metadata indication the creation time (such as in EXIF), +// FileTime uses the modification time from the file system. +// It there was a valid EXIF but an error while trying to get a date from it, +// it logs the error and tries the other methods. +func FileTime(f io.ReaderAt) (time.Time, error) { + var ct time.Time + defaultTime := func() (time.Time, error) { + if osf, ok := f.(*os.File); ok { + fi, err := osf.Stat() + if err != nil { + return ct, fmt.Errorf("Failed to find a modtime: stat: %v", err) + } + return fi.ModTime(), nil + } + return ct, errors.New("All methods failed to find a creation time or modtime.") + } + + size, ok := findSize(f) + if !ok { + size = 256 << 10 // enough to get the EXIF + } + r := io.NewSectionReader(f, 0, size) + var tiffErr error + ex, err := exif.Decode(r) + if err != nil { + tiffErr = err + if exif.IsShortReadTagValueError(err) { + return ct, io.ErrUnexpectedEOF + } + if exif.IsCriticalError(err) || exif.IsExifError(err) { + return defaultTime() + } + } + ct, err = ex.DateTime() + if err != nil { + return defaultTime() + } + // If the EXIF file only had local timezone, but it did have + // GPS, then lookup the timezone and correct the time. + if ct.Location() == time.Local { + if exif.IsGPSError(tiffErr) { + log.Printf("Invalid EXIF GPS data: %v", tiffErr) + return ct, nil + } + if lat, long, err := ex.LatLong(); err == nil { + if loc := lookupLocation(latlong.LookupZoneName(lat, long)); loc != nil { + if t, err := exifDateTimeInLocation(ex, loc); err == nil { + return t, nil + } + } + } else if !exif.IsTagNotPresentError(err) { + log.Printf("Invalid EXIF GPS data: %v", err) + } + } + return ct, nil +} + +// This is basically a copy of the exif.Exif.DateTime() method, except: +// * it takes a *time.Location to assume +// * the caller already assumes there's no timezone offset or GPS time +// in the EXIF, so any of that code can be ignored. +func exifDateTimeInLocation(x *exif.Exif, loc *time.Location) (time.Time, error) { + tag, err := x.Get(exif.DateTimeOriginal) + if err != nil { + tag, err = x.Get(exif.DateTime) + if err != nil { + return time.Time{}, err + } + } + if tag.Format() != tiff.StringVal { + return time.Time{}, errors.New("DateTime[Original] not in string format") + } + const exifTimeLayout = "2006:01:02 15:04:05" + dateStr := strings.TrimRight(string(tag.Val), "\x00") + return time.ParseInLocation(exifTimeLayout, dateStr, loc) +} + +var zoneCache struct { + sync.RWMutex + m map[string]*time.Location +} + +func lookupLocation(zone string) *time.Location { + if zone == "" { + return nil + } + zoneCache.RLock() + l, ok := zoneCache.m[zone] + zoneCache.RUnlock() + if ok { + return l + } + // could use singleflight here, but doesn't really + // matter if two callers both do this. + loc, err := time.LoadLocation(zone) + + zoneCache.Lock() + if zoneCache.m == nil { + zoneCache.m = make(map[string]*time.Location) + } + zoneCache.m[zone] = loc // even if nil + zoneCache.Unlock() + + if err != nil { + log.Printf("failed to lookup timezone %q: %v", zone, err) + return nil + } + return loc +} + +var boringTitlePattern = regexp.MustCompile(`^(?:IMG_|DSC|PANO_|ESR_).*$`) + +// IsInterestingTitle returns whether title would be interesting information as +// a title for a permanode. For example, filenames automatically created by +// cameras, such as IMG_XXXX.JPG, do not add any interesting value. +func IsInterestingTitle(title string) bool { + return !boringTitlePattern.MatchString(title) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/schema_darwin.go b/vendor/github.com/camlistore/camlistore/pkg/schema/schema_darwin.go new file mode 100644 index 00000000..a236caad --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/schema_darwin.go @@ -0,0 +1,28 @@ +//+build darwin +//+build !appengine + +package schema + +import ( + "os" + "syscall" + "time" +) + +func init() { + populateSchemaStat = append(populateSchemaStat, populateSchemaCtime) +} + +func populateSchemaCtime(m map[string]interface{}, fi os.FileInfo) { + st, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + return + } + + // Include the ctime too, if it differs. + sec, nsec := st.Ctimespec.Unix() + ctime := time.Unix(sec, nsec) + if sec != 0 && !ctime.Equal(fi.ModTime()) { + m["unixCtime"] = RFC3339FromTime(ctime) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/schema_linux.go b/vendor/github.com/camlistore/camlistore/pkg/schema/schema_linux.go new file mode 100644 index 00000000..cd78e060 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/schema_linux.go @@ -0,0 +1,28 @@ +//+build linux +//+build !appengine + +package schema + +import ( + "os" + "syscall" + "time" +) + +func init() { + populateSchemaStat = append(populateSchemaStat, populateSchemaCtime) +} + +func populateSchemaCtime(m map[string]interface{}, fi os.FileInfo) { + st, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + return + } + + // Include the ctime too, if it differs. + sec, nsec := st.Ctim.Unix() + ctime := time.Unix(sec, nsec) + if sec != 0 && !ctime.Equal(fi.ModTime()) { + m["unixCtime"] = RFC3339FromTime(ctime) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/schema_posix.go b/vendor/github.com/camlistore/camlistore/pkg/schema/schema_posix.go new file mode 100644 index 00000000..ceb32cc0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/schema_posix.go @@ -0,0 +1,28 @@ +//+build linux darwin netbsd freebsd openbsd +//+build !appengine + +package schema + +import ( + "os" + "syscall" +) + +func init() { + populateSchemaStat = append(populateSchemaStat, populateSchemaUnix) +} + +func populateSchemaUnix(m map[string]interface{}, fi os.FileInfo) { + st, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + return + } + m["unixOwnerId"] = st.Uid + if user := getUserFromUid(int(st.Uid)); user != "" { + m["unixOwner"] = user + } + m["unixGroupId"] = st.Gid + if group := getGroupFromGid(int(st.Gid)); group != "" { + m["unixGroup"] = group + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/schema_public_test.go b/vendor/github.com/camlistore/camlistore/pkg/schema/schema_public_test.go new file mode 100644 index 00000000..5a47b0cc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/schema_public_test.go @@ -0,0 +1,57 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema_test + +import ( + "testing" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/search" +) + +func TestShareSearchSerialization(t *testing.T) { + signer := blob.MustParse("yyy-5678") + + q := &search.SearchQuery{ + Expression: "is:image", + Limit: 42, + } + bb := schema.NewShareRef(schema.ShareHaveRef, true) + bb.SetShareSearch(q) + bb = bb.SetSigner(signer) + bb = bb.SetClaimDate(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)) + s := bb.Blob().JSON() + + want := `{"camliVersion": 1, + "authType": "haveref", + "camliSigner": "yyy-5678", + "camliType": "claim", + "claimDate": "2009-11-10T23:00:00Z", + "claimType": "share", + "search": { + "expression": "is:image", + "limit": 42, + "around": null + }, + "transitive": true +}` + if want != s { + t.Errorf("Incorrect serialization of shared search. Wanted:\n %s\nGot:\n%s\n", want, s) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/schema_test.go b/vendor/github.com/camlistore/camlistore/pkg/schema/schema_test.go new file mode 100644 index 00000000..e741fedc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/schema_test.go @@ -0,0 +1,705 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "encoding/json" + "io" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/osutil" + . "camlistore.org/pkg/test/asserts" +) + +const kExpectedHeader = `{"camliVersion"` + +func TestJSON(t *testing.T) { + fileName := "schema_test.go" + fi, _ := os.Lstat(fileName) + m := NewCommonFileMap(fileName, fi) + json, err := m.JSON() + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + t.Logf("Got json: [%s]\n", json) + // TODO: test it parses back + + if !strings.HasPrefix(json, kExpectedHeader) { + t.Errorf("JSON does't start with expected header.") + } + +} + +func TestRegularFile(t *testing.T) { + fileName := "schema_test.go" + fi, err := os.Lstat(fileName) + AssertNil(t, err, "schema_test.go stat") + m := NewCommonFileMap("schema_test.go", fi) + json, err := m.JSON() + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + t.Logf("Got json for regular file: [%s]\n", json) +} + +func TestSymlink(t *testing.T) { + td, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(td) + + symFile := filepath.Join(td, "test-symlink") + if err := os.Symlink("test-target", symFile); err != nil { + t.Fatal(err) + } + + // Shouldn't be accessed: + if err := ioutil.WriteFile(filepath.Join(td, "test-target"), []byte("foo bar"), 0644); err != nil { + t.Fatal(err) + } + + fi, err := os.Lstat(symFile) + if err != nil { + t.Fatal(err) + } + m := NewCommonFileMap(symFile, fi) + json, err := m.JSON() + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if strings.Contains(string(json), "unixPermission") { + t.Errorf("JSON unexpectedly contains unixPermission: [%s]\n", json) + } +} + +func TestUtf8StrLen(t *testing.T) { + tests := []struct { + in string + want int + }{ + {"", 0}, + {"a", 1}, + {"foo", 3}, + {"Здравствуйте!", 25}, + {"foo\x80", 3}, + {"\x80foo", 0}, + } + for _, tt := range tests { + got := utf8StrLen(tt.in) + if got != tt.want { + t.Errorf("utf8StrLen(%q) = %v; want %v", tt.in, got, tt.want) + } + } +} + +func TestMixedArrayFromString(t *testing.T) { + b80 := byte('\x80') + tests := []struct { + in string + want []interface{} + }{ + {"foo", []interface{}{"foo"}}, + {"\x80foo", []interface{}{b80, "foo"}}, + {"foo\x80foo", []interface{}{"foo", b80, "foo"}}, + {"foo\x80", []interface{}{"foo", b80}}, + {"\x80", []interface{}{b80}}, + {"\x80\x80", []interface{}{b80, b80}}, + } + for _, tt := range tests { + got := mixedArrayFromString(tt.in) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("mixedArrayFromString(%q) = %#v; want %#v", tt.in, got, tt.want) + } + } +} + +type mixPartsTest struct { + json, expected string +} + +func TestStringFromMixedArray(t *testing.T) { + tests := []mixPartsTest{ + {`["brad"]`, "brad"}, + {`["brad", 32, 70]`, "brad F"}, + {`["brad", "fitz"]`, "bradfitz"}, + {`["Am", 233, "lie.jpg"]`, "Am\xe9lie.jpg"}, + } + for idx, test := range tests { + var v []interface{} + if err := json.Unmarshal([]byte(test.json), &v); err != nil { + t.Fatalf("invalid JSON in test %d", idx) + } + got := stringFromMixedArray(v) + if got != test.expected { + t.Errorf("test %d got %q; expected %q", idx, got, test.expected) + } + } +} + +func TestParseInLocation_UnknownLocation(t *testing.T) { + // Example of parsing a time from an API (e.g. Flickr) that + // doesn't know its timezone. + const format = "2006-01-02 15:04:05" + const when = "2010-11-12 13:14:15" + tm, err := time.ParseInLocation(format, when, UnknownLocation) + if err != nil { + t.Fatal(err) + } + got, want := RFC3339FromTime(tm), "2010-11-12T13:14:15-00:01" + if got != want { + t.Errorf("parsed %v to %s; want %s", tm, got, want) + } +} + +func TestIsZoneKnown(t *testing.T) { + if !IsZoneKnown(time.Now()) { + t.Errorf("should know Now's zone") + } + if !IsZoneKnown(time.Now().UTC()) { + t.Errorf("UTC should be known") + } + if IsZoneKnown(time.Now().In(UnknownLocation)) { + t.Errorf("with explicit unknown location, should be false") + } + if IsZoneKnown(time.Now().In(time.FixedZone("xx", -60))) { + t.Errorf("with other fixed zone at -60, should be false") + } +} + +func TestRFC3339(t *testing.T) { + tests := []string{ + "2012-05-13T15:02:47Z", + "2012-05-13T15:02:47.1234Z", + "2012-05-13T15:02:47.123456789Z", + "2012-05-13T15:02:47-00:01", + } + for _, in := range tests { + tm, err := time.Parse(time.RFC3339, in) + if err != nil { + t.Errorf("error parsing %q", in) + continue + } + knownZone := IsZoneKnown(tm) + out := RFC3339FromTime(tm) + if in != out { + t.Errorf("RFC3339FromTime(%q) = %q; want %q", in, out, in) + } + + sub := "Z" + if !knownZone { + sub = "-00:01" + } + if !strings.Contains(out, sub) { + t.Errorf("expected substring %q in %q", sub, out) + } + } +} + +func TestBlobFromReader(t *testing.T) { + br := blob.MustParse("sha1-f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") + blob, err := BlobFromReader(br, strings.NewReader(`{"camliVersion": 1, "camliType": "foo"} `)) + if err != nil { + t.Error(err) + } else if blob.Type() != "foo" { + t.Errorf("got type %q; want foo", blob.Type()) + } + + blob, err = BlobFromReader(br, strings.NewReader(`{"camliVersion": 1, "camliType": "foo"} X `)) + if err == nil { + // TODO(bradfitz): fix this somehow. Currently encoding/json's + // decoder over-reads. + // See: https://code.google.com/p/go/issues/detail?id=1955 , + // which was "fixed", but not really. + t.Logf("TODO(bradfitz): make sure bogus non-whitespace after the JSON object causes an error.") + } +} + +func TestAttribute(t *testing.T) { + tm := time.Unix(123, 456) + br := blob.MustParse("xxx-1234") + tests := []struct { + bb *Builder + want string + }{ + { + bb: NewSetAttributeClaim(br, "attr1", "val1"), + want: `{"camliVersion": 1, + "attribute": "attr1", + "camliType": "claim", + "claimDate": "1970-01-01T00:02:03.000000456Z", + "claimType": "set-attribute", + "permaNode": "xxx-1234", + "value": "val1" +}`, + }, + { + bb: NewAddAttributeClaim(br, "tag", "funny"), + want: `{"camliVersion": 1, + "attribute": "tag", + "camliType": "claim", + "claimDate": "1970-01-01T00:02:03.000000456Z", + "claimType": "add-attribute", + "permaNode": "xxx-1234", + "value": "funny" +}`, + }, + { + bb: NewDelAttributeClaim(br, "attr1", "val1"), + want: `{"camliVersion": 1, + "attribute": "attr1", + "camliType": "claim", + "claimDate": "1970-01-01T00:02:03.000000456Z", + "claimType": "del-attribute", + "permaNode": "xxx-1234", + "value": "val1" +}`, + }, + { + bb: NewDelAttributeClaim(br, "attr2", ""), + want: `{"camliVersion": 1, + "attribute": "attr2", + "camliType": "claim", + "claimDate": "1970-01-01T00:02:03.000000456Z", + "claimType": "del-attribute", + "permaNode": "xxx-1234" +}`, + }, + { + bb: newClaim(&claimParam{ + permanode: br, + claimType: SetAttributeClaim, + attribute: "foo", + value: "bar", + }, &claimParam{ + permanode: br, + claimType: DelAttributeClaim, + attribute: "foo", + value: "specific-del", + }, &claimParam{ + permanode: br, + claimType: DelAttributeClaim, + attribute: "foo", + }), + want: `{"camliVersion": 1, + "camliType": "claim", + "claimDate": "1970-01-01T00:02:03.000000456Z", + "claimType": "multi", + "claims": [ + { + "attribute": "foo", + "claimType": "set-attribute", + "permaNode": "xxx-1234", + "value": "bar" + }, + { + "attribute": "foo", + "claimType": "del-attribute", + "permaNode": "xxx-1234", + "value": "specific-del" + }, + { + "attribute": "foo", + "claimType": "del-attribute", + "permaNode": "xxx-1234" + } + ] +}`, + }, + } + for i, tt := range tests { + tt.bb.SetClaimDate(tm) + got, err := tt.bb.JSON() + if err != nil { + t.Errorf("%d. JSON error = %v", i, err) + continue + } + if got != tt.want { + t.Errorf("%d.\t got:\n%s\n\twant:q\n%s", i, got, tt.want) + } + } +} + +func TestDeleteClaim(t *testing.T) { + tm := time.Unix(123, 456) + br := blob.MustParse("xxx-1234") + delTest := struct { + bb *Builder + want string + }{ + bb: NewDeleteClaim(br), + want: `{"camliVersion": 1, + "camliType": "claim", + "claimDate": "1970-01-01T00:02:03.000000456Z", + "claimType": "delete", + "target": "xxx-1234" +}`, + } + delTest.bb.SetClaimDate(tm) + got, err := delTest.bb.JSON() + if err != nil { + t.Fatalf("JSON error = %v", err) + } + if got != delTest.want { + t.Fatalf("got:\n%s\n\twant:q\n%s", got, delTest.want) + } +} + +func TestAsClaimAndAsShare(t *testing.T) { + br := blob.MustParse("xxx-1234") + signer := blob.MustParse("yyy-5678") + + bb := NewSetAttributeClaim(br, "title", "Test Title") + getBlob := func() *Blob { + var c *Blob + c = bb.Blob() + c.ss.Sig = "non-null-sig" // required by AsShare + return c + } + + bb = bb.SetSigner(signer) + bb = bb.SetClaimDate(time.Now()) + c1 := getBlob() + + bb = NewShareRef(ShareHaveRef, true) + bb = bb.SetSigner(signer) + bb = bb.SetClaimDate(time.Now()) + c2 := getBlob() + + if !br.Valid() { + t.Error("Blobref not valid") + } + + _, ok := c1.AsClaim() + if !ok { + t.Error("Claim 1 not returned as claim") + } + + _, ok = c2.AsClaim() + if !ok { + t.Error("Claim 2 not returned as claim") + } + + s, ok := c1.AsShare() + if ok { + t.Error("Title claim returned share", s) + } + + _, ok = c2.AsShare() + if ok { + t.Error("Share claim returned share without target or search") + } + + bb.SetShareTarget(br) + s, ok = getBlob().AsShare() + if !ok { + t.Error("Share claim failed to return share with target") + } + + bb = NewShareRef(ShareHaveRef, true) + bb = bb.SetSigner(signer) + bb = bb.SetClaimDate(time.Now()) + // Would be better to use search.SearchQuery but we can't reference it here. + bb.SetShareSearch(&struct{}{}) + s, ok = getBlob().AsShare() + if !ok { + t.Error("Share claim failed to return share with search") + } +} + +func TestShareExpiration(t *testing.T) { + defer func() { clockNow = time.Now }() + b, err := BlobFromReader( + blob.MustParse("sha1-64ffa72fa9bcb2f825e7ed40b9451e5cadca4c2c"), + strings.NewReader(`{"camliVersion": 1, + "authType": "haveref", + "camliSigner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "camliType": "claim", + "claimDate": "2013-09-08T23:58:53.656549677Z", + "claimType": "share", + "expires": "2013-09-09T23:58:53.65658012Z", + "target": "sha1-f1d2d2f924e986ac86fdf7b36c94bcdf32beec15", + "transitive": false +,"camliSig":"wsBcBAABCAAQBQJSLQ89CRApMaZ8JvWr2gAAcuEIABRQolhn+yKksfaBx6oLo18NWvWQ+aYweF+5Gu0TH0Ixur7t1o5HFtFSSfFISyggSZDJSjsxoxaawhWrvCe9dZuU2s/zgRpgUtd2xmBt82tLOn9JidnUavsNGFXbfCwdUBSkzN0vDYLmgXW0VtiybB354uIKfOInZor2j8Mq0p6pkWzK3qq9W0dku7iE96YFaTb4W7eOikqoSC6VpjC1/4MQWOYRHLcPcIEY6xJ8es2sYMMSNXuVaR9nMupz8ZcTygP4jh+lPR1OH61q/FSjpRp7GKt4wZ1PknYjMbnpIzVjiSz0MkYd65bpZwuPOwZh/h2kHW7wvHNQZfWUJHEsOAI==J2ID"}`), + ) + if err != nil { + t.Fatal(err) + } + s, ok := b.AsShare() + if !ok { + t.Fatal("expected share") + } + clockNow = func() time.Time { return time.Unix(100, 0) } + if s.IsExpired() { + t.Error("expected not expired") + } + clockNow = func() time.Time { return time.Unix(1378687181+2*86400, 0) } + if !s.IsExpired() { + t.Error("expected expired") + } + + // And without an expiration time: + b, err = BlobFromReader( + blob.MustParse("sha1-931875ec6b8d917b7aae9f672f4f92de1ffaeeb1"), + strings.NewReader(`{"camliVersion": 1, + "authType": "haveref", + "camliSigner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "camliType": "claim", + "claimDate": "2013-09-09T01:01:09.907842963Z", + "claimType": "share", + "target": "sha1-64ffa72fa9bcb2f825e7ed40b9451e5cadca4c2c", + "transitive": false +,"camliSig":"wsBcBAABCAAQBQJSLR3VCRApMaZ8JvWr2gAA14kIAKmi5rCI5JTBvHbBuAu7wPVA87BLXm/BaD6zjqOENB4U8B+6KxyuT6KXe9P591IDXdZmJTP5tesbLtKw0iAWiRf2ea0Y7Ms3K77nLnSZM5QIOzb4aQKd1668p/5KqU3VfNayoHt69YkXyKBkqyEPjHINzC03QuLz5NIEBMYJaNqKKtEtSgh4gG8BBYq5qQzdKFg/Hx7VhkhW1y/1wwGSFJjaiPFMIJsF4d/gaO01Ip7XLro63ccyCy81tqKHnVjv0uULmZdbpgd3RHGGSnW3c9BfqkGvc3Wl11UQKzqc9OT+WTAWp8TXg6bLES9sQNzerx2wUfjKB9J4Yrk14iBfjl8==AynO"}`), + ) + if err != nil { + t.Fatal(err) + } + s, ok = b.AsShare() + if !ok { + t.Fatal("expected share") + } + clockNow = func() time.Time { return time.Unix(100, 0) } + if s.IsExpired() { + t.Error("expected not expired") + } + clockNow = func() time.Time { return time.Unix(1378687181+2*86400, 0) } + if s.IsExpired() { + t.Error("expected not expired") + } +} + +// camlistore.org/issue/305 +func TestIssue305(t *testing.T) { + var in = `{"camliVersion": 1, + "camliType": "file", + "fileName": "2012-03-10 15.03.18.m4v", + "parts": [ + { + "bytesRef": "sha1-c76d8b17b887c207875e61a77b7eccc60289e61c", + "size": 20032564 + } + ] +}` + var ss superset + if err := json.NewDecoder(strings.NewReader(in)).Decode(&ss); err != nil { + t.Fatal(err) + } + inref := blob.SHA1FromString(in) + blob, err := BlobFromReader(inref, strings.NewReader(in)) + if err != nil { + t.Fatal(err) + } + if blob.BlobRef() != inref { + t.Errorf("original ref = %s; want %s", blob.BlobRef(), inref) + } + bb := blob.Builder() + jback, err := bb.JSON() + if err != nil { + t.Fatal(err) + } + if jback != in { + t.Errorf("JSON doesn't match:\n got: %q\nwant: %q\n", jback, in) + } + out := bb.Blob() + if got := out.BlobRef(); got != inref { + t.Errorf("cloned ref = %v; want %v", got, inref) + } +} + +func TestStaticFileAndStaticSymlink(t *testing.T) { + // TODO (marete): Split this into two test functions. + fd, err := ioutil.TempFile("", "schema-test-") + if err != nil { + t.Fatalf("io.TempFile(): %v", err) + } + defer os.Remove(fd.Name()) + defer fd.Close() + + fi, err := os.Lstat(fd.Name()) + if err != nil { + t.Fatalf("os.Lstat(): %v", err) + } + + bb := NewCommonFileMap(fd.Name(), fi) + bb.SetType("file") + bb.SetFileName(fd.Name()) + blob := bb.Blob() + + sf, ok := blob.AsStaticFile() + if !ok { + t.Fatalf("Blob.AsStaticFile(): Unexpected return value: false") + } + if want, got := filepath.Base(fd.Name()), sf.FileName(); want != got { + t.Fatalf("StaticFile.FileName(): Expected %s, got %s", + want, got) + } + + _, ok = sf.AsStaticSymlink() + if ok { + t.Fatalf("StaticFile.AsStaticSymlink(): Unexpected return value: true") + } + + dir, err := ioutil.TempDir("", "schema-test-") + if err != nil { + t.Fatalf("ioutil.TempDir(): %v", err) + } + defer os.RemoveAll(dir) + + target := "bar" + src := filepath.Join(dir, "foo") + err = os.Symlink(target, src) + fi, err = os.Lstat(src) + if err != nil { + t.Fatalf("os.Lstat(): %v", err) + } + + bb = NewCommonFileMap(src, fi) + bb.SetType("symlink") + bb.SetFileName(src) + bb.SetSymlinkTarget(target) + blob = bb.Blob() + + sf, ok = blob.AsStaticFile() + if !ok { + t.Fatalf("Blob.AsStaticFile(): Unexpected return value: false") + } + sl, ok := sf.AsStaticSymlink() + if !ok { + t.Fatalf("StaticFile.AsStaticSymlink(): Unexpected return value: false") + } + + if want, got := filepath.Base(src), sl.FileName(); want != got { + t.Fatalf("StaticSymlink.FileName(): Expected %s, got %s", + want, got) + } + + if want, got := target, sl.SymlinkTargetString(); got != want { + t.Fatalf("StaticSymlink.SymlinkTargetString(): Expected %s, got %s", want, got) + } +} + +func TestStaticFIFO(t *testing.T) { + tdir, err := ioutil.TempDir("", "schema-test-") + if err != nil { + t.Fatalf("ioutil.TempDir(): %v", err) + } + defer os.RemoveAll(tdir) + + fifoPath := filepath.Join(tdir, "fifo") + err = osutil.Mkfifo(fifoPath, 0660) + if err == osutil.ErrNotSupported { + t.SkipNow() + } + if err != nil { + t.Fatalf("osutil.Mkfifo(): %v", err) + } + + fi, err := os.Lstat(fifoPath) + if err != nil { + t.Fatalf("os.Lstat(): %v", err) + } + + bb := NewCommonFileMap(fifoPath, fi) + bb.SetType("fifo") + bb.SetFileName(fifoPath) + blob := bb.Blob() + t.Logf("Got JSON for fifo: %s\n", blob.JSON()) + + sf, ok := blob.AsStaticFile() + if !ok { + t.Fatalf("Blob.AsStaticFile(): Expected true, got false") + } + _, ok = sf.AsStaticFIFO() + if !ok { + t.Fatalf("StaticFile.AsStaticFIFO(): Expected true, got false") + } +} + +func TestStaticSocket(t *testing.T) { + tdir, err := ioutil.TempDir("", "schema-test-") + if err != nil { + t.Fatalf("ioutil.TempDir(): %v", err) + } + defer os.RemoveAll(tdir) + + sockPath := filepath.Join(tdir, "socket") + err = osutil.Mksocket(sockPath) + if err == osutil.ErrNotSupported { + t.SkipNow() + } + if err != nil { + t.Fatalf("osutil.Mksocket(): %v", err) + } + + fi, err := os.Lstat(sockPath) + if err != nil { + t.Fatalf("os.Lstat(): %v", err) + } + + bb := NewCommonFileMap(sockPath, fi) + bb.SetType("socket") + bb.SetFileName(sockPath) + blob := bb.Blob() + t.Logf("Got JSON for socket: %s\n", blob.JSON()) + + sf, ok := blob.AsStaticFile() + if !ok { + t.Fatalf("Blob.AsStaticFile(): Expected true, got false") + } + _, ok = sf.AsStaticSocket() + if !ok { + t.Fatalf("StaticFile.AsStaticSocket(): Expected true, got false") + } +} + +func TestTimezoneEXIFCorrection(t *testing.T) { + // Test that we get UTC times for photos taken in two + // different timezones. + // Both only have local time + GPS in the exif. + tests := []struct { + file, want, wantUTC string + }{ + {"coffee-sf.jpg", "2014-07-11 08:44:34 -0700 PDT", "2014-07-11 15:44:34 +0000 UTC"}, + {"gocon-tokyo.jpg", "2014-05-31 13:34:04 +0900 JST", "2014-05-31 04:34:04 +0000 UTC"}, + } + for _, tt := range tests { + f, err := os.Open("testdata/" + tt.file) + if err != nil { + t.Fatal(err) + } + // Hide *os.File type from FileTime, so it can't use modtime: + tm, err := FileTime(struct{ io.ReaderAt }{f}) + f.Close() + if err != nil { + t.Errorf("%s: %v", tt.file, err) + continue + } + if got := tm.String(); got != tt.want { + t.Errorf("%s: time = %q; want %q", tt.file, got, tt.want) + } + if got := tm.UTC().String(); got != tt.wantUTC { + t.Errorf("%s: utc time = %q; want %q", tt.file, got, tt.wantUTC) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/sign.go b/vendor/github.com/camlistore/camlistore/pkg/schema/sign.go new file mode 100644 index 00000000..801ed624 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/sign.go @@ -0,0 +1,130 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/jsonsign" + "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp" +) + +// A Signer signs the JSON schema blobs that require signing, such as claims +// and permanodes. +type Signer struct { + keyId string // short one; 8 capital hex digits + pubref blob.Ref + privEntity *openpgp.Entity + + // baseSigReq is the prototype signing request used with the jsonsig + // package. + baseSigReq jsonsign.SignRequest +} + +func (s *Signer) String() string { + return fmt.Sprintf("[*schema.Signer for key=%s pubkey=%s]", s.keyId, s.pubref) +} + +// KeyID returns the short 8 capital hex digit GPG key ID +func (s *Signer) KeyID() string { + return s.keyId +} + +// NewSigner returns an Signer given an armored public key's blobref, +// its armored content, and its associated private key entity. +// The privateKeySource must be either an *openpgp.Entity or a string filename to a secret key. +func NewSigner(pubKeyRef blob.Ref, armoredPubKey io.Reader, privateKeySource interface{}) (*Signer, error) { + hash := pubKeyRef.Hash() + keyId, armoredPubKeyString, err := jsonsign.ParseArmoredPublicKey(io.TeeReader(armoredPubKey, hash)) + if err != nil { + return nil, err + } + if !pubKeyRef.HashMatches(hash) { + return nil, fmt.Errorf("pubkey ref of %v doesn't match provided armored public key", pubKeyRef) + } + + var privateKey *openpgp.Entity + switch v := privateKeySource.(type) { + case *openpgp.Entity: + privateKey = v + case string: + privateKey, err = jsonsign.EntityFromSecring(keyId, v) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("invalid privateKeySource type %T", v) + } + if privateKey == nil { + return nil, errors.New("nil privateKey") + } + + return &Signer{ + keyId: keyId, + pubref: pubKeyRef, + privEntity: privateKey, + baseSigReq: jsonsign.SignRequest{ + ServerMode: true, // shouldn't matter, since we're supplying the rest of the fields + Fetcher: memoryBlobFetcher{ + pubKeyRef: func() (uint32, io.ReadCloser) { + return uint32(len(armoredPubKeyString)), ioutil.NopCloser(strings.NewReader(armoredPubKeyString)) + }, + }, + EntityFetcher: entityFetcherFunc(func(wantKeyId string) (*openpgp.Entity, error) { + if privateKey.PrivateKey.KeyIdString() != wantKeyId && + privateKey.PrivateKey.KeyIdShortString() != wantKeyId { + return nil, fmt.Errorf("jsonsign code unexpectedly requested keyId %q; only have %q", + wantKeyId, keyId) + } + return privateKey, nil + }), + }, + }, nil +} + +// SignJSON signs the provided json at the optional time t. +// If t is the zero Time, the current time is used. +func (s *Signer) SignJSON(json string, t time.Time) (string, error) { + sr := s.baseSigReq + sr.UnsignedJSON = json + sr.SignatureTime = t + return sr.Sign() +} + +type memoryBlobFetcher map[blob.Ref]func() (size uint32, rc io.ReadCloser) + +func (m memoryBlobFetcher) Fetch(br blob.Ref) (file io.ReadCloser, size uint32, err error) { + fn, ok := m[br] + if !ok { + return nil, 0, os.ErrNotExist + } + size, file = fn() + return +} + +type entityFetcherFunc func(keyId string) (*openpgp.Entity, error) + +func (f entityFetcherFunc) FetchEntity(keyId string) (*openpgp.Entity, error) { + return f(keyId) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/sign_test.go b/vendor/github.com/camlistore/camlistore/pkg/schema/sign_test.go new file mode 100644 index 00000000..92de1f49 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/schema/sign_test.go @@ -0,0 +1,51 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "strings" + "testing" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/jsonsign" +) + +func TestSigner(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + ent, err := jsonsign.NewEntity() + if err != nil { + t.Fatal(err) + } + armorPub, err := jsonsign.ArmoredPublicKey(ent) + if err != nil { + t.Fatal(err) + } + pubRef := blob.SHA1FromString(armorPub) + sig, err := NewSigner(pubRef, strings.NewReader(armorPub), ent) + if err != nil { + t.Fatalf("NewSigner: %v", err) + } + pn, err := NewUnsignedPermanode().Sign(sig) + if err != nil { + t.Fatalf("NewPermanode: %v", err) + } + if !strings.Contains(pn, `,"camliSig":"`) { + t.Errorf("Permanode doesn't look signed: %v", pn) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/testdata/coffee-sf.jpg b/vendor/github.com/camlistore/camlistore/pkg/schema/testdata/coffee-sf.jpg new file mode 100644 index 0000000000000000000000000000000000000000..10c800fb177450c9884d007f06bfc4e219189122 GIT binary patch literal 28083 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
    s(ZxPm?|TzN~rZE?r3LYieeTzb~zZ!NEsp^ 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 Q?DZD+_YF)j{jp#F+27dnGXMYp literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/schema/testdata/gocon-tokyo.jpg b/vendor/github.com/camlistore/camlistore/pkg/schema/testdata/gocon-tokyo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..34f58fa2845171c9ce47ca9378f6cc596d7e9c82 GIT binary patch literal 27493 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/describe.go b/vendor/github.com/camlistore/camlistore/pkg/search/describe.go new file mode 100644 index 00000000..0d100808 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/describe.go @@ -0,0 +1,884 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "os" + "sort" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/syncutil" + "camlistore.org/pkg/types" + "camlistore.org/pkg/types/camtypes" +) + +func (sh *Handler) serveDescribe(rw http.ResponseWriter, req *http.Request) { + defer httputil.RecoverJSON(rw, req) + var dr DescribeRequest + dr.fromHTTP(req) + + res, err := sh.Describe(&dr) + if err != nil { + httputil.ServeJSONError(rw, err) + return + } + httputil.ReturnJSON(rw, res) +} + +const verboseDescribe = false + +func (sh *Handler) Describe(dr *DescribeRequest) (dres *DescribeResponse, err error) { + if verboseDescribe { + t0 := time.Now() + defer func() { + td := time.Since(t0) + var num int + if dres != nil { + num = len(dres.Meta) + } + log.Printf("Described %d blobs in %v", num, td) + }() + } + sh.initDescribeRequest(dr) + if dr.BlobRef.Valid() { + dr.Describe(dr.BlobRef, dr.depth()) + } + for _, br := range dr.BlobRefs { + dr.Describe(br, dr.depth()) + } + if err := dr.expandRules(); err != nil { + return nil, err + } + metaMap, err := dr.metaMap() + if err != nil { + return nil, err + } + return &DescribeResponse{metaMap}, nil +} + +type DescribeRequest struct { + // BlobRefs are the blobs to describe. If length zero, BlobRef + // is used. + BlobRefs []blob.Ref `json:"blobrefs,omitempty"` + + // BlobRef is the blob to describe. + BlobRef blob.Ref `json:"blobref,omitempty"` + + // Depth is the optional traversal depth to describe from the + // root BlobRef. If zero, a default is used. + // Depth is deprecated and will be removed. Use Rules instead. + Depth int `json:"depth,omitempty"` + + // MaxDirChildren is the requested optional limit to the number + // of children that should be fetched when describing a static + // directory. If zero, a default is used. + MaxDirChildren int `json:"maxDirChildren,omitempty"` + + // At specifies the time which we wish to see the state of + // this blob. If zero (unspecified), all claims will be + // considered, otherwise, any claims after this date will not + // be considered. + At types.Time3339 `json:"at"` + + // Rules specifies a set of rules to instruct how to keep + // expanding the described set. All rules are tested and + // matching rules grow the the response set until all rules no + // longer match or internal limits are hit. + Rules []*DescribeRule `json:"rules,omitempty"` + + // Internal details, used while loading. + // Initialized by sh.initDescribeRequest. + sh *Handler + mu sync.Mutex // protects following: + m MetaMap + done map[blobrefAndDepth]bool // blobref -> true + errs map[string]error // blobref -> error + resFromRule map[*DescribeRule]map[blob.Ref]bool + flatRuleCache []*DescribeRule // flattened once, by flatRules + + wg *sync.WaitGroup // for load requests +} + +type blobrefAndDepth struct { + br blob.Ref + depth int +} + +// Requires dr.mu is held +func (dr *DescribeRequest) flatRules() []*DescribeRule { + if dr.flatRuleCache == nil { + dr.flatRuleCache = make([]*DescribeRule, 0) + for _, rule := range dr.Rules { + rule.appendToFlatCache(dr) + } + } + return dr.flatRuleCache +} + +func (r *DescribeRule) appendToFlatCache(dr *DescribeRequest) { + dr.flatRuleCache = append(dr.flatRuleCache, r) + for _, rchild := range r.Rules { + rchild.parentRule = r + rchild.appendToFlatCache(dr) + } +} + +// Requires dr.mu is held. +func (dr *DescribeRequest) foreachResultBlob(fn func(blob.Ref)) { + if dr.BlobRef.Valid() { + fn(dr.BlobRef) + } + for _, br := range dr.BlobRefs { + fn(br) + } + for brStr := range dr.m { + if br, ok := blob.Parse(brStr); ok { + fn(br) + } + } +} + +// Requires dr.mu is held. +func (dr *DescribeRequest) blobInitiallyRequested(br blob.Ref) bool { + if dr.BlobRef.Valid() && dr.BlobRef == br { + return true + } + for _, br1 := range dr.BlobRefs { + if br == br1 { + return true + } + } + return false +} + +type DescribeRule struct { + // All non-zero 'If*' fields in the following set must match + // for the rule to match: + + // IsResultRoot, if true, only matches if the blob was part of + // the original search results, not a blob expanded later. + IfResultRoot bool `json:"ifResultRoot,omitempty"` + + // IfCamliNodeType matches if the "camliNodeType" attribute + // equals this value. + IfCamliNodeType string `json:"ifCamliNodeType,omitempty"` + + // Attrs lists attributes to describe. A special case + // is if the value ends in "*", which matches prefixes + // (e.g. "camliPath:*" or "*"). + Attrs []string `json:"attrs,omitempty"` + + // Additional rules to run on the described results of Attrs. + Rules []*DescribeRule `json:"rules,omitempty"` + + parentRule *DescribeRule +} + +// DescribeResponse is the JSON response from $searchRoot/camli/search/describe. +type DescribeResponse struct { + Meta MetaMap `json:"meta"` +} + +// A MetaMap is a map from blobref to a DescribedBlob. +type MetaMap map[string]*DescribedBlob + +type DescribedBlob struct { + Request *DescribeRequest `json:"-"` + + BlobRef blob.Ref `json:"blobRef"` + CamliType string `json:"camliType,omitempty"` + Size int64 `json:"size,"` + + // if camliType "permanode" + Permanode *DescribedPermanode `json:"permanode,omitempty"` + + // if camliType "file" + File *camtypes.FileInfo `json:"file,omitempty"` + // if camliType "directory" + Dir *camtypes.FileInfo `json:"dir,omitempty"` + // if camliType "file", and File.IsImage() + Image *camtypes.ImageInfo `json:"image,omitempty"` + // if camliType "file" and media file + MediaTags map[string]string `json:"mediaTags,omitempty"` + + // if camliType "directory" + DirChildren []blob.Ref `json:"dirChildren,omitempty"` + + // Stub is set if this is not loaded, but referenced. + Stub bool `json:"-"` +} + +func (m MetaMap) Get(br blob.Ref) *DescribedBlob { + if !br.Valid() { + return nil + } + return m[br.String()] +} + +// URLSuffixPost returns the URL suffix for POST requests. +func (r *DescribeRequest) URLSuffixPost() string { + return "camli/search/describe" +} + +// URLSuffix returns the URL suffix for GET requests. +// This is deprecated. +func (r *DescribeRequest) URLSuffix() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "camli/search/describe?depth=%d&maxdirchildren=%d", + r.depth(), r.maxDirChildren()) + for _, br := range r.BlobRefs { + buf.WriteString("&blobref=") + buf.WriteString(br.String()) + } + if len(r.BlobRefs) == 0 && r.BlobRef.Valid() { + buf.WriteString("&blobref=") + buf.WriteString(r.BlobRef.String()) + } + if !r.At.IsZero() { + buf.WriteString("&at=") + buf.WriteString(r.At.String()) + } + return buf.String() +} + +// fromHTTP panics with an httputil value on failure +func (r *DescribeRequest) fromHTTP(req *http.Request) { + switch { + case httputil.IsGet(req): + r.fromHTTPGet(req) + case req.Method == "POST": + r.fromHTTPPost(req) + default: + panic("Unsupported method") + } +} + +func (r *DescribeRequest) fromHTTPPost(req *http.Request) { + err := json.NewDecoder(req.Body).Decode(r) + if err != nil { + panic(err) + } +} + +func (r *DescribeRequest) fromHTTPGet(req *http.Request) { + req.ParseForm() + if vv := req.Form["blobref"]; len(vv) > 1 { + for _, brs := range vv { + if br, ok := blob.Parse(brs); ok { + r.BlobRefs = append(r.BlobRefs, br) + } else { + panic(httputil.InvalidParameterError("blobref")) + } + } + } else { + r.BlobRef = httputil.MustGetBlobRef(req, "blobref") + } + r.Depth = httputil.OptionalInt(req, "depth") + r.MaxDirChildren = httputil.OptionalInt(req, "maxdirchildren") + r.At = types.ParseTime3339OrZero(req.FormValue("at")) +} + +// PermanodeFile returns in path the blobref of the described permanode +// and the blobref of its File camliContent. +// If b isn't a permanode, or doesn't have a camliContent that +// is a file blob, ok is false. +func (b *DescribedBlob) PermanodeFile() (path []blob.Ref, fi *camtypes.FileInfo, ok bool) { + if b == nil || b.Permanode == nil { + return + } + if contentRef := b.Permanode.Attr.Get("camliContent"); contentRef != "" { + if cdes := b.Request.DescribedBlobStr(contentRef); cdes != nil && cdes.File != nil { + return []blob.Ref{b.BlobRef, cdes.BlobRef}, cdes.File, true + } + } + return +} + +// PermanodeDir returns in path the blobref of the described permanode +// and the blobref of its Directory camliContent. +// If b isn't a permanode, or doesn't have a camliContent that +// is a directory blob, ok is false. +func (b *DescribedBlob) PermanodeDir() (path []blob.Ref, fi *camtypes.FileInfo, ok bool) { + if b == nil || b.Permanode == nil { + return + } + if contentRef := b.Permanode.Attr.Get("camliContent"); contentRef != "" { + if cdes := b.Request.DescribedBlobStr(contentRef); cdes != nil && cdes.Dir != nil { + return []blob.Ref{b.BlobRef, cdes.BlobRef}, cdes.Dir, true + } + } + return +} + +func (b *DescribedBlob) DomID() string { + if b == nil { + return "" + } + return b.BlobRef.DomID() +} + +func (b *DescribedBlob) Title() string { + if b == nil { + return "" + } + if b.Permanode != nil { + if t := b.Permanode.Attr.Get("title"); t != "" { + return t + } + if contentRef := b.Permanode.Attr.Get("camliContent"); contentRef != "" { + return b.Request.DescribedBlobStr(contentRef).Title() + } + } + if b.File != nil { + return b.File.FileName + } + if b.Dir != nil { + return b.Dir.FileName + } + return "" +} + +func (b *DescribedBlob) Description() string { + if b == nil { + return "" + } + if b.Permanode != nil { + return b.Permanode.Attr.Get("description") + } + return "" +} + +// Members returns all of b's children, as given by b's camliMember and camliPath:* +// attributes. Only the first entry for a given camliPath attribute is used. +func (b *DescribedBlob) Members() []*DescribedBlob { + if b == nil { + return nil + } + m := make([]*DescribedBlob, 0) + if b.Permanode != nil { + for _, bstr := range b.Permanode.Attr["camliMember"] { + if br, ok := blob.Parse(bstr); ok { + m = append(m, b.PeerBlob(br)) + } + } + for k, bstrs := range b.Permanode.Attr { + if strings.HasPrefix(k, "camliPath:") && len(bstrs) > 0 { + if br, ok := blob.Parse(bstrs[0]); ok { + m = append(m, b.PeerBlob(br)) + } + } + } + } + return m +} + +func (b *DescribedBlob) DirMembers() []*DescribedBlob { + if b == nil || b.Dir == nil || len(b.DirChildren) == 0 { + return nil + } + + m := make([]*DescribedBlob, 0) + for _, br := range b.DirChildren { + m = append(m, b.PeerBlob(br)) + } + return m +} + +func (b *DescribedBlob) ContentRef() (br blob.Ref, ok bool) { + if b != nil && b.Permanode != nil { + if cref := b.Permanode.Attr.Get("camliContent"); cref != "" { + return blob.Parse(cref) + } + } + return +} + +// Given a blobref string returns a Description or nil. +// dr may be nil itself. +func (dr *DescribeRequest) DescribedBlobStr(blobstr string) *DescribedBlob { + if dr == nil { + return nil + } + dr.mu.Lock() + defer dr.mu.Unlock() + return dr.m[blobstr] +} + +// PeerBlob returns a DescribedBlob for the provided blobref. +// +// Unlike DescribedBlobStr, the returned DescribedBlob is never nil. +// +// If the blob was never loaded along with the the receiver (or if the +// receiver is nil), a stub DescribedBlob is returned with its Stub +// field set true. +func (b *DescribedBlob) PeerBlob(br blob.Ref) *DescribedBlob { + if b.Request == nil { + return &DescribedBlob{BlobRef: br, Stub: true} + } + b.Request.mu.Lock() + defer b.Request.mu.Unlock() + return b.peerBlob(br) +} + +// version of PeerBlob when b.Request.mu is already held. +func (b *DescribedBlob) peerBlob(br blob.Ref) *DescribedBlob { + if peer, ok := b.Request.m[br.String()]; ok { + return peer + } + return &DescribedBlob{Request: b.Request, BlobRef: br, Stub: true} +} + +func (b *DescribedBlob) isPermanode() bool { + return b.Permanode != nil +} + +type DescribedPermanode struct { + Attr url.Values `json:"attr"` // a map[string][]string + ModTime time.Time `json:"modtime,omitempty"` +} + +// IsContainer returns whether the permanode has either named ("camliPath:"-prefixed) or unnamed +// ("camliMember") member attributes. +func (dp *DescribedPermanode) IsContainer() bool { + if members := dp.Attr["camliMember"]; len(members) > 0 { + return true + } + for k := range dp.Attr { + if strings.HasPrefix(k, "camliPath:") { + return true + } + } + return false +} + +func (dp *DescribedPermanode) jsonMap() map[string]interface{} { + m := jsonMap() + + am := jsonMap() + m["attr"] = am + for k, vv := range dp.Attr { + if len(vv) > 0 { + vl := make([]string, len(vv)) + copy(vl[:], vv[:]) + am[k] = vl + } + } + return m +} + +// NewDescribeRequest returns a new DescribeRequest holding the state +// of blobs and their summarized descriptions. Use DescribeBlob +// one or more times before calling Result. +func (sh *Handler) NewDescribeRequest() *DescribeRequest { + dr := new(DescribeRequest) + sh.initDescribeRequest(dr) + return dr +} + +func (sh *Handler) initDescribeRequest(req *DescribeRequest) { + if req.sh != nil { + panic("already initialized") + } + req.sh = sh + req.m = make(MetaMap) + req.errs = make(map[string]error) + req.wg = new(sync.WaitGroup) +} + +type DescribeError map[string]error + +func (de DescribeError) Error() string { + var buf bytes.Buffer + for b, err := range de { + fmt.Fprintf(&buf, "%s: %v; ", b, err) + } + return fmt.Sprintf("Errors (%d) describing blobs: %s", len(de), buf.String()) +} + +// Result waits for all outstanding lookups to complete and +// returns the map of blobref (strings) to their described +// results. The returned error is non-nil if any errors +// occured, and will be of type DescribeError. +func (dr *DescribeRequest) Result() (desmap map[string]*DescribedBlob, err error) { + dr.wg.Wait() + // TODO: set "done" / locked flag, so no more DescribeBlob can + // be called. + if len(dr.errs) > 0 { + return dr.m, DescribeError(dr.errs) + } + return dr.m, nil +} + +func (dr *DescribeRequest) depth() int { + if dr.Depth > 0 { + return dr.Depth + } + return 1 +} + +func (dr *DescribeRequest) maxDirChildren() int { + return sanitizeNumResults(dr.MaxDirChildren) +} + +func (dr *DescribeRequest) metaMap() (map[string]*DescribedBlob, error) { + dr.wg.Wait() + dr.mu.Lock() + defer dr.mu.Unlock() + for k, err := range dr.errs { + // TODO: include all? + return nil, fmt.Errorf("error populating %s: %v", k, err) + } + m := make(map[string]*DescribedBlob) + for k, desb := range dr.m { + m[k] = desb + } + return m, nil +} + +func (dr *DescribeRequest) describedBlob(b blob.Ref) *DescribedBlob { + dr.mu.Lock() + defer dr.mu.Unlock() + bs := b.String() + if des, ok := dr.m[bs]; ok { + return des + } + des := &DescribedBlob{Request: dr, BlobRef: b} + dr.m[bs] = des + return des +} + +func (dr *DescribeRequest) DescribeSync(br blob.Ref) (*DescribedBlob, error) { + dr.Describe(br, 1) + res, err := dr.Result() + if err != nil { + return nil, err + } + return res[br.String()], nil +} + +// Describe starts a lookup of br, down to the provided depth. +// It returns immediately. +func (dr *DescribeRequest) Describe(br blob.Ref, depth int) { + if depth <= 0 { + return + } + dr.mu.Lock() + defer dr.mu.Unlock() + if dr.done == nil { + dr.done = make(map[blobrefAndDepth]bool) + } + doneKey := blobrefAndDepth{br, depth} + if dr.done[doneKey] { + return + } + dr.done[doneKey] = true + dr.wg.Add(1) + go func() { + defer dr.wg.Done() + dr.describeReally(br, depth) + }() +} + +// requires dr.mu is held +func (dr *DescribeRequest) isDescribedOrError(br blob.Ref) bool { + brs := br.String() + if _, ok := dr.m[brs]; ok { + return true + } + if _, ok := dr.errs[brs]; ok { + return true + } + return false +} + +// requires dr.mu be held. +func (r *DescribeRule) newMatches(br blob.Ref, dr *DescribeRequest) (brs []blob.Ref) { + if r.IfResultRoot { + if !dr.blobInitiallyRequested(br) { + return nil + } + } + if r.parentRule != nil { + if _, ok := dr.resFromRule[r.parentRule][br]; !ok { + return nil + } + } + db, ok := dr.m[br.String()] + if !ok || db.Permanode == nil { + return nil + } + if t := r.IfCamliNodeType; t != "" { + gotType := db.Permanode.Attr.Get("camliNodeType") + if gotType != t { + return nil + } + } + for attr, vv := range db.Permanode.Attr { + matches := false + for _, matchAttr := range r.Attrs { + if attr == matchAttr { + matches = true + break + } + if strings.HasSuffix(matchAttr, "*") && strings.HasPrefix(attr, strings.TrimSuffix(matchAttr, "*")) { + matches = true + break + } + } + if !matches { + continue + } + for _, v := range vv { + if br, ok := blob.Parse(v); ok { + brs = append(brs, br) + } + } + } + return brs +} + +// dr.mu just be locked. +func (dr *DescribeRequest) noteResultFromRule(rule *DescribeRule, br blob.Ref) { + if dr.resFromRule == nil { + dr.resFromRule = make(map[*DescribeRule]map[blob.Ref]bool) + } + m, ok := dr.resFromRule[rule] + if !ok { + m = make(map[blob.Ref]bool) + dr.resFromRule[rule] = m + } + m[br] = true +} + +func (dr *DescribeRequest) expandRules() error { + loop := true + + for loop { + loop = false + dr.wg.Wait() + dr.mu.Lock() + len0 := len(dr.m) + var new []blob.Ref + for _, rule := range dr.flatRules() { + dr.foreachResultBlob(func(br blob.Ref) { + for _, nbr := range rule.newMatches(br, dr) { + new = append(new, nbr) + dr.noteResultFromRule(rule, nbr) + } + }) + } + dr.mu.Unlock() + for _, br := range new { + dr.Describe(br, 1) + } + dr.wg.Wait() + dr.mu.Lock() + len1 := len(dr.m) + dr.mu.Unlock() + loop = len0 != len1 + } + return nil +} + +func (dr *DescribeRequest) addError(br blob.Ref, err error) { + if err == nil { + return + } + dr.mu.Lock() + defer dr.mu.Unlock() + // TODO: append? meh. + dr.errs[br.String()] = err +} + +func (dr *DescribeRequest) describeReally(br blob.Ref, depth int) { + meta, err := dr.sh.index.GetBlobMeta(br) + if err == os.ErrNotExist { + return + } + if err != nil { + dr.addError(br, err) + return + } + + // TODO: convert all this in terms of + // DescribedBlob/DescribedPermanode/DescribedFile, not json + // maps. Then add JSON marhsallers to those types. Add tests. + des := dr.describedBlob(br) + if meta.CamliType != "" { + des.setMIMEType("application/json; camliType=" + meta.CamliType) + } + des.Size = int64(meta.Size) + + switch des.CamliType { + case "permanode": + des.Permanode = new(DescribedPermanode) + dr.populatePermanodeFields(des.Permanode, br, dr.sh.owner, depth) + case "file": + fi, err := dr.sh.index.GetFileInfo(br) + if err != nil { + if os.IsNotExist(err) { + log.Printf("index.GetFileInfo(file %s) failed; index stale?", br) + } else { + dr.addError(br, err) + } + return + } + des.File = &fi + if des.File.IsImage() { + imgInfo, err := dr.sh.index.GetImageInfo(br) + if err != nil { + if !os.IsNotExist(err) { + dr.addError(br, err) + } + } else { + des.Image = &imgInfo + } + } + if mediaTags, err := dr.sh.index.GetMediaTags(br); err == nil { + des.MediaTags = mediaTags + } + case "directory": + var g syncutil.Group + g.Go(func() (err error) { + fi, err := dr.sh.index.GetFileInfo(br) + if os.IsNotExist(err) { + log.Printf("index.GetFileInfo(directory %s) failed; index stale?", br) + } + if err == nil { + des.Dir = &fi + } + return + }) + g.Go(func() (err error) { + des.DirChildren, err = dr.getDirMembers(br, depth) + return + }) + if err := g.Err(); err != nil { + dr.addError(br, err) + } + } +} + +func (dr *DescribeRequest) populatePermanodeFields(pi *DescribedPermanode, pn, signer blob.Ref, depth int) { + pi.Attr = make(url.Values) + attr := pi.Attr + + claims, err := dr.sh.index.AppendClaims(nil, pn, signer, "") + if err != nil { + log.Printf("Error getting claims of %s: %v", pn.String(), err) + dr.addError(pn, fmt.Errorf("Error getting claims of %s: %v", pn.String(), err)) + return + } + + sort.Sort(camtypes.ClaimsByDate(claims)) +claimLoop: + for _, cl := range claims { + if !dr.At.IsZero() { + if cl.Date.After(dr.At.Time()) { + continue + } + } + switch cl.Type { + default: + continue + case "del-attribute": + if cl.Value == "" { + delete(attr, cl.Attr) + } else { + sl := attr[cl.Attr] + filtered := make([]string, 0, len(sl)) + for _, val := range sl { + if val != cl.Value { + filtered = append(filtered, val) + } + } + attr[cl.Attr] = filtered + } + case "set-attribute": + delete(attr, cl.Attr) + fallthrough + case "add-attribute": + if cl.Value == "" { + continue + } + sl, ok := attr[cl.Attr] + if ok { + for _, exist := range sl { + if exist == cl.Value { + continue claimLoop + } + } + } else { + sl = make([]string, 0, 1) + attr[cl.Attr] = sl + } + attr[cl.Attr] = append(sl, cl.Value) + } + pi.ModTime = cl.Date + } + + // Descend into any references in current attributes. + for key, vals := range attr { + dr.describeRefs(key, depth) + for _, v := range vals { + dr.describeRefs(v, depth) + } + } +} + +func (dr *DescribeRequest) getDirMembers(br blob.Ref, depth int) ([]blob.Ref, error) { + limit := dr.maxDirChildren() + ch := make(chan blob.Ref) + errch := make(chan error) + go func() { + errch <- dr.sh.index.GetDirMembers(br, ch, limit) + }() + + var members []blob.Ref + for child := range ch { + dr.Describe(child, depth) + members = append(members, child) + } + if err := <-errch; err != nil { + return nil, err + } + return members, nil +} + +func (dr *DescribeRequest) describeRefs(str string, depth int) { + for _, match := range blobRefPattern.FindAllString(str, -1) { + if ref, ok := blob.ParseKnown(match); ok { + dr.Describe(ref, depth-1) + } + } +} + +func (d *DescribedBlob) setMIMEType(mime string) { + if strings.HasPrefix(mime, camliTypePrefix) { + d.CamliType = strings.TrimPrefix(mime, camliTypePrefix) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/describe_test.go b/vendor/github.com/camlistore/camlistore/pkg/search/describe_test.go new file mode 100644 index 00000000..51346eab --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/describe_test.go @@ -0,0 +1,253 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search_test + +import ( + "testing" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/index" + "camlistore.org/pkg/search" + "camlistore.org/pkg/test" +) + +func addPermanode(fi *test.FakeIndex, pnStr string, attrs ...string) { + pn := blob.MustParse(pnStr) + fi.AddMeta(pn, "permanode", 123) + for len(attrs) > 0 { + k, v := attrs[0], attrs[1] + attrs = attrs[2:] + fi.AddClaim(owner, pn, "add-attribute", k, v) + } +} + +func searchDescribeSetup(fi *test.FakeIndex) index.Interface { + addPermanode(fi, "abc-123", + "camliContent", "abc-123c", + "camliImageContent", "abc-888", + ) + addPermanode(fi, "abc-123c", + "camliContent", "abc-123cc", + "camliImageContent", "abc-123c1", + ) + addPermanode(fi, "abc-123c1", + "some", "image", + ) + addPermanode(fi, "abc-123cc", + "name", "leaf", + ) + addPermanode(fi, "abc-888", + "camliContent", "abc-8881", + ) + addPermanode(fi, "abc-8881", + "name", "leaf8881", + ) + + addPermanode(fi, "fourcheckin-0", + "camliNodeType", "foursquare.com:checkin", + "foursquareVenuePermanode", "fourvenue-123", + ) + addPermanode(fi, "fourvenue-123", + "camliNodeType", "foursquare.com:venue", + "camliPath:photos", "venuepicset-123", + ) + addPermanode(fi, "venuepicset-123", + "camliPath:1.jpg", "venuepic-1", + ) + addPermanode(fi, "venuepic-1", + "camliContent", "somevenuepic-0", + ) + addPermanode(fi, "somevenuepic-0", + "foo", "bar", + ) + addPermanode(fi, "venuepic-2", + "camliContent", "somevenuepic-2", + ) + addPermanode(fi, "somevenuepic-2", + "foo", "baz", + ) + + addPermanode(fi, "homedir-0", + "camliPath:subdir.1", "homedir-1", + ) + addPermanode(fi, "homedir-1", + "camliPath:subdir.2", "homedir-2", + ) + addPermanode(fi, "homedir-2", + "foo", "bar", + ) + + addPermanode(fi, "set-0", + "camliMember", "venuepic-1", + "camliMember", "venuepic-2", + ) + + return fi +} + +var searchDescribeTests = []handlerTest{ + { + name: "null", + postBody: marshalJSON(&search.DescribeRequest{}), + want: jmap(&search.DescribeResponse{ + Meta: search.MetaMap{}, + }), + }, + + { + name: "single", + postBody: marshalJSON(&search.DescribeRequest{ + BlobRef: blob.MustParse("abc-123"), + }), + wantDescribed: []string{"abc-123"}, + }, + + { + name: "follow all camliContent", + postBody: marshalJSON(&search.DescribeRequest{ + BlobRef: blob.MustParse("abc-123"), + Rules: []*search.DescribeRule{ + { + Attrs: []string{"camliContent"}, + }, + }, + }), + wantDescribed: []string{"abc-123", "abc-123c", "abc-123cc"}, + }, + + { + name: "follow only root camliContent", + postBody: marshalJSON(&search.DescribeRequest{ + BlobRef: blob.MustParse("abc-123"), + Rules: []*search.DescribeRule{ + { + IfResultRoot: true, + Attrs: []string{"camliContent"}, + }, + }, + }), + wantDescribed: []string{"abc-123", "abc-123c"}, + }, + + { + name: "follow all root, substring", + postBody: marshalJSON(&search.DescribeRequest{ + BlobRef: blob.MustParse("abc-123"), + Rules: []*search.DescribeRule{ + { + IfResultRoot: true, + Attrs: []string{"camli*"}, + }, + }, + }), + wantDescribed: []string{"abc-123", "abc-123c", "abc-888"}, + }, + + { + name: "two rules, two attrs", + postBody: marshalJSON(&search.DescribeRequest{ + BlobRef: blob.MustParse("abc-123"), + Rules: []*search.DescribeRule{ + { + IfResultRoot: true, + Attrs: []string{"camliContent", "camliImageContent"}, + }, + { + Attrs: []string{"camliContent"}, + }, + }, + }), + wantDescribed: []string{"abc-123", "abc-123c", "abc-123cc", "abc-888", "abc-8881"}, + }, + + { + name: "foursquare venue photos, but not recursive camliPath explosion", + postBody: marshalJSON(&search.DescribeRequest{ + BlobRefs: []blob.Ref{ + blob.MustParse("homedir-0"), + blob.MustParse("fourcheckin-0"), + }, + Rules: []*search.DescribeRule{ + { + Attrs: []string{"camliContent", "camliContentImage"}, + }, + { + IfCamliNodeType: "foursquare.com:checkin", + Attrs: []string{"foursquareVenuePermanode"}, + }, + { + IfCamliNodeType: "foursquare.com:venue", + Attrs: []string{"camliPath:photos"}, + Rules: []*search.DescribeRule{ + { + Attrs: []string{"camliPath:*"}, + }, + }, + }, + }, + }), + wantDescribed: []string{"homedir-0", "fourcheckin-0", "fourvenue-123", "venuepicset-123", "venuepic-1", "somevenuepic-0"}, + }, + + { + name: "home dirs forever", + postBody: marshalJSON(&search.DescribeRequest{ + BlobRefs: []blob.Ref{ + blob.MustParse("homedir-0"), + }, + Rules: []*search.DescribeRule{ + { + Attrs: []string{"camliPath:*"}, + }, + }, + }), + wantDescribed: []string{"homedir-0", "homedir-1", "homedir-2"}, + }, + + { + name: "find members", + postBody: marshalJSON(&search.DescribeRequest{ + BlobRef: blob.MustParse("set-0"), + Rules: []*search.DescribeRule{ + { + IfResultRoot: true, + Attrs: []string{"camliMember"}, + Rules: []*search.DescribeRule{ + {Attrs: []string{"camliContent"}}, + }, + }, + }, + }), + wantDescribed: []string{"set-0", "venuepic-1", "venuepic-2", "somevenuepic-0", "somevenuepic-2"}, + }, +} + +func init() { + checkNoDups("searchDescribeTests", searchDescribeTests) +} + +func TestSearchDescribe(t *testing.T) { + for _, ht := range searchDescribeTests { + if ht.setup == nil { + ht.setup = searchDescribeSetup + } + if ht.query == "" { + ht.query = "describe" + } + ht.test(t) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/export_test.go b/vendor/github.com/camlistore/camlistore/pkg/search/export_test.go new file mode 100644 index 00000000..2ea0d8cb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/export_test.go @@ -0,0 +1,31 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search + +func SetTestHookBug121(hook func()) { + testHookBug121 = hook +} + +func ExportSetCandidateSourceHook(fn func(string)) { candSourceHook = fn } + +func ExportBufferedConst() int { return buffered } + +func (s *SearchQuery) ExportPlannedQuery() *SearchQuery { + return s.plannedQuery(nil) +} + +var SortName = sortName diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/expr.go b/vendor/github.com/camlistore/camlistore/pkg/search/expr.go new file mode 100644 index 00000000..49d6765d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/expr.go @@ -0,0 +1,362 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search + +import ( + "fmt" + "log" + "strconv" + "strings" + + "camlistore.org/pkg/context" +) + +const seeDocs = "\nSee: https://camlistore.googlesource.com/camlistore/+/master/doc/search-ui.txt" + +var ( + noMatchingOpening = "No matching opening parenthesis" + noMatchingClosing = "No matching closing parenthesis" + noLiteralSupport = "No support for literals yet" + noQuotedLiteralSupport = "No support for quoted literals yet" + expectedAtom = "Expected an atom" + predicateError = "Predicates do not start with a colon" + trailingTokens = "After parsing finished there is still input left" +) + +type parseExpError struct { + mesg string + t token +} + +func (e parseExpError) Error() string { + return fmt.Sprintf("%s at position %d, token: %q %s", e.mesg, e.t.start, e.t.val, seeDocs) +} + +func newParseExpError(mesg string, t token) error { + return parseExpError{mesg: mesg, t: t} +} + +func andConst(a, b *Constraint) *Constraint { + return &Constraint{ + Logical: &LogicalConstraint{ + Op: "and", + A: a, + B: b, + }, + } +} + +func orConst(a, b *Constraint) *Constraint { + return &Constraint{ + Logical: &LogicalConstraint{ + Op: "or", + A: a, + B: b, + }, + } +} + +func notConst(a *Constraint) *Constraint { + return &Constraint{ + Logical: &LogicalConstraint{ + Op: "not", + A: a, + }, + } +} + +type parser struct { + tokens chan token + peeked *token + ctx *context.Context +} + +func newParser(exp string, ctx *context.Context) parser { + _, tokens := lex(exp) + return parser{tokens: tokens, ctx: ctx} +} + +func (p *parser) next() *token { + if p.peeked != nil { + t := p.peeked + p.peeked = nil + return t + } + return p.readInternal() +} + +func (p *parser) peek() *token { + if p.peeked == nil { + p.peeked = p.readInternal() + } + return p.peeked +} + +// ReadInternal should not be called directly, use 'next' or 'peek' +func (p *parser) readInternal() *token { + for t := range p.tokens { + return &t + } + return &token{tokenEOF, "", -1} +} + +func (p *parser) stripNot() (negated bool) { + for { + switch p.peek().typ { + case tokenNot: + p.next() + negated = !negated + continue + } + return negated + } +} + +func (p *parser) parseExp() (c *Constraint, err error) { + if p.peek().typ == tokenEOF { + return + } + c, err = p.parseOperand() + if err != nil { + return + } + for { + switch p.peek().typ { + case tokenAnd: + p.next() + case tokenOr: + p.next() + return p.parseOrRHS(c) + case tokenClose, tokenEOF: + return + } + c, err = p.parseAndRHS(c) + if err != nil { + return + } + } +} + +func (p *parser) parseGroup() (c *Constraint, err error) { + i := p.next() + switch i.typ { + case tokenOpen: + c, err = p.parseExp() + if err != nil { + return + } + if p.peek().typ == tokenClose { + p.next() + return + } else { + err = newParseExpError(noMatchingClosing, *i) + return + } + } + err = newParseExpError("internal: do not call parseGroup when not on a '('", *i) + return +} + +func (p *parser) parseOrRHS(lhs *Constraint) (c *Constraint, err error) { + var rhs *Constraint + c = lhs + for { + rhs, err = p.parseAnd() + if err != nil { + return + } + c = orConst(c, rhs) + switch p.peek().typ { + case tokenOr: + p.next() + case tokenAnd, tokenClose, tokenEOF: + return + } + } +} + +func (p *parser) parseAnd() (c *Constraint, err error) { + for { + c, err = p.parseOperand() + if err != nil { + return + } + switch p.peek().typ { + case tokenAnd: + p.next() + case tokenOr, tokenClose, tokenEOF: + return + } + return p.parseAndRHS(c) + } +} + +func (p *parser) parseAndRHS(lhs *Constraint) (c *Constraint, err error) { + var rhs *Constraint + c = lhs + for { + rhs, err = p.parseOperand() + if err != nil { + return + } + c = andConst(c, rhs) + switch p.peek().typ { + case tokenOr, tokenClose, tokenEOF: + return + case tokenAnd: + p.next() + continue + } + return + } +} + +func (p *parser) parseOperand() (c *Constraint, err error) { + negated := p.stripNot() + i := p.peek() + switch i.typ { + case tokenError: + err = newParseExpError(i.val, *i) + return + case tokenEOF: + err = newParseExpError(expectedAtom, *i) + return + case tokenClose: + err = newParseExpError(noMatchingOpening, *i) + return + case tokenLiteral, tokenQuotedLiteral, tokenPredicate, tokenColon, tokenArg: + c, err = p.parseAtom() + case tokenOpen: + c, err = p.parseGroup() + } + if err != nil { + return + } + if negated { + c = notConst(c) + } + return +} + +// AtomWords returns the parsed atom, the starting position of this +// atom and an error. +func (p *parser) atomWords() (a atom, start int, err error) { + i := p.peek() + start = i.start + a = atom{} + switch i.typ { + case tokenLiteral: + err = newParseExpError(noLiteralSupport, *i) + return + case tokenQuotedLiteral: + err = newParseExpError(noQuotedLiteralSupport, *i) + return + case tokenColon: + err = newParseExpError(predicateError, *i) + return + case tokenPredicate: + i := p.next() + a.predicate = i.val + } + for { + switch p.peek().typ { + case tokenColon: + p.next() + continue + case tokenArg: + i := p.next() + a.args = append(a.args, i.val) + continue + case tokenQuotedArg: + i := p.next() + var uq string + uq, err = strconv.Unquote(i.val) + if err != nil { + return + } + a.args = append(a.args, uq) + continue + } + return + } +} + +func (p *parser) parseAtom() (*Constraint, error) { + a, start, err := p.atomWords() + if err != nil { + return nil, err + } + faultToken := func() token { + return token{ + typ: tokenError, + val: a.String(), + start: start, + } + } + var c *Constraint + for _, k := range keywords { + matched, err := k.Match(a) + if err != nil { + return nil, newParseExpError(err.Error(), faultToken()) + } + if matched { + c, err = k.Predicate(p.ctx, a.args) + if err != nil { + return nil, newParseExpError(err.Error(), faultToken()) + } + return c, nil + } + } + t := faultToken() + err = newParseExpError(fmt.Sprintf("Unknown search predicate: %q", t.val), t) + log.Printf(err.Error()) + return nil, err +} + +func parseExpression(ctx *context.Context, exp string) (*SearchQuery, error) { + base := &Constraint{ + Permanode: &PermanodeConstraint{ + SkipHidden: true, + }, + } + sq := &SearchQuery{ + Constraint: base, + } + + exp = strings.TrimSpace(exp) + if exp == "" { + return sq, nil + } + p := newParser(exp, ctx) + + c, err := p.parseExp() + if err != nil { + return nil, err + } + lastToken := p.next() + if lastToken.typ != tokenEOF { + switch lastToken.typ { + case tokenClose: + return nil, newParseExpError(noMatchingOpening, *lastToken) + } + return nil, newParseExpError(trailingTokens, *lastToken) + } + if c != nil { + sq.Constraint = andConst(base, c) + } + return sq, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/expr_test.go b/vendor/github.com/camlistore/camlistore/pkg/search/expr_test.go new file mode 100644 index 00000000..f3b71d65 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/expr_test.go @@ -0,0 +1,988 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search + +import ( + "encoding/json" + "reflect" + "strings" + "testing" + + "camlistore.org/pkg/context" +) + +var skiphiddenC = &Constraint{ + Permanode: &PermanodeConstraint{ + SkipHidden: true, + }, +} + +var ispanoC = &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + WHRatio: &FloatConstraint{ + Min: 2.0, + }, + }, + }, + }, +} + +var attrfoobarC = &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "foo", + Value: "bar", + SkipHidden: true, + }, +} + +var attrgorunC = &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "go", + Value: "run", + SkipHidden: true, + }, +} + +var hasLocationC = orConst(&Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Location: &LocationConstraint{Any: true}, + }, + }, + }, +}, &Constraint{ + Permanode: &PermanodeConstraint{ + Location: &LocationConstraint{Any: true}, + }, +}) + +var parseExpressionTests = []struct { + name string + in string + inList []string + want *SearchQuery + errContains string + ctx *context.Context +}{ + { + name: "empty search", + inList: []string{"", " ", "\n"}, + want: &SearchQuery{ + Constraint: skiphiddenC, + }, + }, + + { + in: "is:pano", + want: &SearchQuery{ + Constraint: andConst(skiphiddenC, ispanoC), + }, + }, + + { + in: "is:pano)", + errContains: "No matching opening", + }, + + { + in: "width:0-640", + want: &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "and", + A: skiphiddenC, + B: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Width: &IntConstraint{ + ZeroMin: true, + Max: 640, + }, + }, + }, + }, + }, + }, + }, + }, + }, + + { + name: "tag with spaces", + in: `tag:"Foo Bar"`, + want: &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "and", + A: skiphiddenC, + B: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "tag", + Value: "Foo Bar", + SkipHidden: true, + }, + }, + }, + }, + }, + }, + + { + name: "attribute search", + in: "attr:foo:bar", + want: &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "and", + A: skiphiddenC, + B: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "foo", + Value: "bar", + SkipHidden: true, + }, + }, + }, + }, + }, + }, + + { + name: "attribute search with space in value", + in: `attr:foo:"fun bar"`, + want: &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "and", + A: skiphiddenC, + B: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "foo", + Value: "fun bar", + SkipHidden: true, + }, + }, + }, + }, + }, + }, + + { + in: "tag:funny", + want: &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "and", + A: skiphiddenC, + B: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "tag", + Value: "funny", + SkipHidden: true, + }, + }, + }, + }, + }, + }, + + { + in: "title:Doggies", + want: &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "and", + A: skiphiddenC, + B: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "title", + ValueMatches: &StringConstraint{ + Contains: "Doggies", + CaseInsensitive: true, + }, + SkipHidden: true, + }, + }, + }, + }, + }, + }, + + { + in: "childrenof:sha1-f00ba4", + want: &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "and", + A: skiphiddenC, + B: &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "parent", + Any: &Constraint{ + BlobRefPrefix: "sha1-f00ba4", + }, + }, + }, + }, + }, + }, + }, + }, + // Location predicates + { + in: "loc:Uitdam", // Small dutch town + want: &SearchQuery{ + Constraint: andConst(skiphiddenC, orConst(&Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Location: uitdamLC, + }, + }, + }, + }, &Constraint{ + Permanode: &PermanodeConstraint{ + Location: uitdamLC, + }, + })), + }, + ctx: newGeocodeContext(), + }, + + { + in: "has:location", + want: &SearchQuery{ + Constraint: andConst(skiphiddenC, hasLocationC), + }, + }, + + // TODO: at least 'x' will go away eventually. + /* + { + inList: []string{"x", "bogus:operator"}, + errContains: "unknown expression", + }, + */ +} + +func TestParseExpression(t *testing.T) { + qj := func(sq *SearchQuery) []byte { + v, err := json.MarshalIndent(sq, "", " ") + if err != nil { + t.Fatal(err) + } + return v + } + for _, tt := range parseExpressionTests { + ins := tt.inList + if len(ins) == 0 { + ins = []string{tt.in} + } + for _, in := range ins { + ctx := tt.ctx + if ctx == nil { + ctx = context.TODO() + } + got, err := parseExpression(ctx, in) + if err != nil { + if tt.errContains != "" && strings.Contains(err.Error(), tt.errContains) { + continue + } + t.Errorf("%s: parseExpression(%q) error: %v", tt.name, in, err) + continue + } + if tt.errContains != "" { + t.Errorf("%s: parseExpression(%q) succeeded; want error containing %q", tt.name, in, tt.errContains) + continue + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("%s: parseExpression(%q) got:\n%s\n\nwant:%s\n", tt.name, in, qj(got), qj(tt.want)) + } + } + } +} + +func doSticherChecking(name string, t *testing.T, tt sticherTestCase, got *Constraint, err error, p parser) { + ntt := parserTestCase{ + name: tt.name, + in: tt.in, + want: tt.want, + remCount: tt.remCount, + errContains: tt.errContains, + } + doChecking(name, t, ntt, got, err, p) +} + +func doChecking(name string, t *testing.T, tt parserTestCase, got *Constraint, err error, p parser) { + cj := func(c *Constraint) []byte { + v, err := json.MarshalIndent(c, "", " ") + if err != nil { + panic(err) + } + return v + } + remain := func() []token { + var remainder []token + var i int + for i = 0; true; i++ { + token := p.next() + if token.typ == tokenEOF { + break + } else { + remainder = append(remainder, *token) + } + } + return remainder + } + + if err != nil { + if tt.errContains != "" && strings.Contains(err.Error(), tt.errContains) { + return + } + if tt.errContains != "" { + t.Errorf("%s: %s(%q) error: %v, but wanted an error with: %v", tt.name, name, tt.in, err, tt.errContains) + } else { + t.Errorf("%s: %s(%q) unexpected error: %v", tt.name, name, tt.in, err) + } + return + } + if tt.errContains != "" { + t.Errorf("%s: %s(%q) succeeded; want error containing %q got: %s", tt.name, name, tt.in, tt.errContains, cj(got)) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("%s: %s(%q) got:\n%s\n\nwant:%s\n", tt.name, name, tt.in, cj(got), cj(tt.want)) + } + remainder := remain() + if len(remainder) != tt.remCount { + t.Errorf("%s: %s(%s): Expected remainder of %d got %d\nRemaining tokens: %#v", tt.name, name, tt.in, tt.remCount, len(remainder), remainder) + } +} + +type parserTestCase struct { + name string + in string + want *Constraint + remCount int + errContains string +} + +type sticherTestCase struct { + name string + in string + want *Constraint + remCount int + errContains string + lhs *Constraint +} + +var parseOrRHSTests = []sticherTestCase{ + { + name: "stop on )", + in: "is:pano )", + want: orConst(nil, ispanoC), + remCount: 1, + }, + + { + in: "is:pano and attr:foo:bar", + want: orConst(nil, andConst(ispanoC, attrfoobarC)), + remCount: 0, + }, + + { + name: "add atom", + in: "is:pano", + want: orConst(nil, ispanoC), + remCount: 0, + }, +} + +func TestParseOrRhs(t *testing.T) { + for _, tt := range parseOrRHSTests { + p := newParser(tt.in, context.TODO()) + + got, err := p.parseOrRHS(tt.lhs) + + doSticherChecking("parseOrRHS", t, tt, got, err, p) + } +} + +var parseAndRHSTests = []sticherTestCase{ + { + name: "stop on )", + in: "is:pano )", + want: andConst(nil, ispanoC), + remCount: 1, + }, + + { + name: "stop on or", + in: "is:pano or", + want: andConst(nil, ispanoC), + remCount: 1, + }, + + { + name: "add atom", + in: "is:pano", + want: andConst(nil, ispanoC), + remCount: 0, + }, +} + +func TestParseConjuction(t *testing.T) { + for _, tt := range parseAndRHSTests { + p := newParser(tt.in, context.TODO()) + + got, err := p.parseAndRHS(tt.lhs) + + doSticherChecking("parseAndRHS", t, tt, got, err, p) + } +} + +var parseGroupTests = []struct { + name string + in string + want *Constraint + remCount int + errContains string +}{ + { + name: "simple grouped atom", + in: "( is:pano )", + want: ispanoC, + remCount: 0, + }, + + { + name: "simple grouped or with remainder", + in: "( attr:foo:bar or is:pano ) attr:foo:bar", + want: orConst(attrfoobarC, ispanoC), + remCount: 5, + }, + + { + name: "simple grouped and with remainder", + in: "( attr:foo:bar is:pano ) attr:foo:bar", + want: andConst(attrfoobarC, ispanoC), + remCount: 5, + }, + + { + name: "simple grouped atom with remainder", + in: "( is:pano ) attr:foo:bar", + want: ispanoC, + remCount: 5, + }, +} + +func TestParseGroup(t *testing.T) { + for _, tt := range parseGroupTests { + p := newParser(tt.in, context.TODO()) + + got, err := p.parseGroup() + + doChecking("parseGroup", t, tt, got, err, p) + } +} + +var parseOperandTests = []struct { + name string + in string + want *Constraint + remCount int + errContains string +}{ + { + name: "group of one atom", + in: "( is:pano )", + want: ispanoC, + remCount: 0, + }, + + { + name: "one atom", + in: "is:pano", + want: ispanoC, + remCount: 0, + }, + + { + name: "two atoms", + in: "is:pano attr:foo:bar", + want: ispanoC, + remCount: 5, + }, + + { + name: "grouped atom and atom", + in: "( is:pano ) attr:foo:bar", + want: ispanoC, + remCount: 5, + }, + + { + name: "atom and )", + in: "is:pano )", + want: ispanoC, + remCount: 1, + }, +} + +func TestParseOperand(t *testing.T) { + for _, tt := range parseOperandTests { + p := newParser(tt.in, context.TODO()) + + got, err := p.parseOperand() + + doChecking("parseOperand", t, tt, got, err, p) + } +} + +var parseExpTests = []parserTestCase{ + { + in: "attr:foo:", + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "foo", + ValueMatches: &StringConstraint{Empty: true}, + SkipHidden: true, + }, + }, + }, + + { + in: "after:foo", + errContains: "as \"2006\" at position 0", + }, + + { + in: "after:foo:bar", + errContains: `Wrong number of arguments for "after", given 2, expected 1 at position 0, token: "after:foo:bar"`, + }, + + { + in: " attr:foo", + errContains: `Wrong number of arguments for "attr", given 1, expected 2 at position 5, token: "attr:foo"`, + }, + + { + in: "has:location", + want: hasLocationC, + }, + + { + in: "is:pano", + want: ispanoC, + }, + + { + in: "height:0-640", + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Height: &IntConstraint{ + ZeroMin: true, + Max: 640, + }, + }, + }, + }, + }, + }, + + { + in: "width:0-640", + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Width: &IntConstraint{ + ZeroMin: true, + Max: 640, + }, + }, + }, + }, + }, + }, + + { + in: "height:++0", + errContains: "Unable to parse \"++0\" as range, wanted something like 480-1024, 480-, -1024 or 1024 at position 0", + }, + + { + in: "height:480", + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Height: &IntConstraint{ + Min: 480, + Max: 480, + }, + }, + }, + }, + }, + }, + + { + in: "width:++0", + errContains: "Unable to parse \"++0\" as range, wanted something like 480-1024, 480-, -1024 or 1024 at position 0", + }, + + { + in: "width:640", + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Width: &IntConstraint{ + Min: 640, + Max: 640, + }, + }, + }, + }, + }, + }, + { + name: "tag with spaces", + in: `tag:"Foo Bar"`, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "tag", + Value: "Foo Bar", + SkipHidden: true, + }, + }, + }, + + { + name: "attribute search", + in: "attr:foo:bar", + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "foo", + Value: "bar", + SkipHidden: true, + }, + }, + }, + + { + name: "attribute search with space in value", + in: `attr:foo:"fun bar"`, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "foo", + Value: "fun bar", + SkipHidden: true, + }, + }, + }, + + { + in: "tag:funny", + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "tag", + Value: "funny", + SkipHidden: true, + }, + }, + }, + + { + in: "title:Doggies", + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "title", + ValueMatches: &StringConstraint{ + Contains: "Doggies", + CaseInsensitive: true, + }, + SkipHidden: true, + }, + }, + }, + + { + in: "childrenof:sha1-f00ba4", + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "parent", + Any: &Constraint{ + BlobRefPrefix: "sha1-f00ba4", + }, + }, + }, + }, + }, + + { + name: "Unmatched quote", + in: `is:pano and "foo`, + errContains: "Unclosed quote at position 12", + }, + + { + name: "Unmatched quote", + in: `"foo`, + errContains: "Unclosed quote at position 0", + }, + + { + name: "Unmatched (", + in: "(", + errContains: "No matching closing parenthesis at position 0", + }, + + { + name: "Unmatched )", + in: ")", + errContains: "No matching opening parenthesis", + }, + + { + name: "Unmatched ) at the end ", + in: "is:pano or attr:foo:bar )", + want: orConst(ispanoC, attrfoobarC), + remCount: 1, + }, + + { + name: "empty search", + in: "", + want: nil, + }, + + { + name: "faulty negation in 'or'", + in: "is:pano - or - is:pano", + errContains: "at position 10", + }, + + { + name: "faulty negation in 'or'", + in: "is:pano or -", + errContains: "an atom", + }, + + { + name: "faulty disjunction, empty right", + in: "is:pano or", + errContains: "at position 8", + }, + + { + name: "faulty disjunction", + in: "or is:pano", + errContains: "at position 0", + }, + + { + name: "faulty conjunction", + in: "and is:pano", + errContains: "at position 0", + }, + + { + name: "one atom", + in: "is:pano", + want: ispanoC, + }, + + { + name: "negated atom", + in: "- is:pano", + want: notConst(ispanoC), + }, + + { + name: "double negated atom", + in: "- - is:pano", + want: ispanoC, + }, + + { + name: "parenthesized atom with implicit 'and' and other atom", + in: "( is:pano ) attr:foo:bar", + want: andConst(ispanoC, attrfoobarC), + }, + + { + name: "negated implicit 'and'", + in: "- ( is:pano attr:foo:bar )", + want: notConst(andConst(ispanoC, attrfoobarC)), + }, + + { + name: "negated implicit 'and' with trailing attr:go:run", + in: "- ( is:pano attr:foo:bar ) attr:go:run", + want: andConst(notConst(andConst(ispanoC, attrfoobarC)), attrgorunC), + }, + + { + name: "parenthesized implicit 'and'", + in: "( is:pano attr:foo:bar )", + want: andConst(ispanoC, attrfoobarC), + }, + + { + name: "simple 'or' of two atoms", + in: "is:pano or attr:foo:bar", + want: orConst(ispanoC, attrfoobarC), + }, + + { + name: "left associativity of implicit 'and'", + in: "is:pano attr:go:run attr:foo:bar", + want: andConst(andConst(ispanoC, attrgorunC), attrfoobarC), + }, + + { + name: "left associativity of explicit 'and'", + in: "is:pano and attr:go:run and attr:foo:bar", + want: andConst(andConst(ispanoC, attrgorunC), attrfoobarC), + }, + + { + name: "left associativity of 'or'", + in: "is:pano or attr:go:run or attr:foo:bar", + want: orConst(orConst(ispanoC, attrgorunC), attrfoobarC)}, + + { + name: "left associativity of 'or' with negated atom", + in: "is:pano or - attr:go:run or attr:foo:bar", + want: orConst(orConst(ispanoC, notConst(attrgorunC)), attrfoobarC), + }, + + { + name: "left associativity of 'or' with double negated atom", + in: "is:pano or - - attr:go:run or attr:foo:bar", + want: orConst(orConst(ispanoC, attrgorunC), attrfoobarC), + }, + + { + name: "left associativity of 'or' with parenthesized subexpression", + in: "is:pano or ( - attr:go:run ) or attr:foo:bar", + want: orConst(orConst(ispanoC, notConst(attrgorunC)), attrfoobarC), + }, + + { + name: "explicit 'and' of two atoms", + in: "is:pano and attr:foo:bar", + want: andConst(ispanoC, attrfoobarC), + }, + + { + name: "implicit 'and' of two atom", + in: "is:pano attr:foo:bar", + want: andConst(ispanoC, attrfoobarC), + }, + + { + name: "grouping an 'and' in an 'or'", + in: "is:pano or ( attr:foo:bar attr:go:run )", + want: orConst(ispanoC, andConst(attrfoobarC, attrgorunC)), + }, + + { + name: "precedence of 'and' over 'or'", + in: "is:pano or attr:foo:bar and attr:go:run", + want: orConst(ispanoC, andConst(attrfoobarC, attrgorunC)), + }, + + { + name: "precedence of 'and' over 'or' with 'and' on the left", + in: "is:pano and attr:foo:bar or attr:go:run", + want: orConst(andConst(ispanoC, attrfoobarC), attrgorunC), + }, + + { + name: "precedence of 'and' over 'or' with 'and' on the left and right", + in: "is:pano and attr:foo:bar or attr:go:run is:pano", + want: orConst(andConst(ispanoC, attrfoobarC), andConst(attrgorunC, ispanoC)), + }, + + { + name: "precedence of 'and' over 'or' with 'and' on the left and right with a negation", + in: "is:pano and attr:foo:bar or - attr:go:run is:pano", + want: orConst(andConst(ispanoC, attrfoobarC), andConst(notConst(attrgorunC), ispanoC)), + }, + + { + name: "precedence of 'and' over 'or' with 'and' on the left and right with a negation of group and trailing 'and'", + in: "is:pano and attr:foo:bar or - ( attr:go:run is:pano ) is:pano", + want: orConst(andConst(ispanoC, attrfoobarC), andConst(notConst(andConst(attrgorunC, ispanoC)), ispanoC)), + }, + + { + name: "complicated", + in: "- ( is:pano and attr:foo:bar ) or - ( attr:go:run is:pano ) is:pano", + want: orConst(notConst(andConst(ispanoC, attrfoobarC)), andConst(notConst(andConst(attrgorunC, ispanoC)), ispanoC)), + }, + + { + name: "complicated", + in: "is:pano or attr:foo:bar attr:go:run or - attr:go:run or is:pano is:pano", + want: orConst(orConst(orConst(ispanoC, andConst(attrfoobarC, attrgorunC)), notConst(attrgorunC)), andConst(ispanoC, ispanoC)), + }, + + { + name: "complicated", + in: "is:pano or attr:foo:bar attr:go:run or - attr:go:run or is:pano is:pano or attr:foo:bar", + want: orConst(orConst(orConst(orConst(ispanoC, andConst(attrfoobarC, attrgorunC)), notConst(attrgorunC)), andConst(ispanoC, ispanoC)), attrfoobarC), + }, +} + +func TestParseExp(t *testing.T) { + for _, tt := range parseExpTests { + p := newParser(tt.in, context.TODO()) + + got, err := p.parseExp() + + doChecking("parseExp", t, tt, got, err, p) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/handler.go b/vendor/github.com/camlistore/camlistore/pkg/search/handler.go new file mode 100644 index 00000000..6db46bde --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/handler.go @@ -0,0 +1,811 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "regexp" + "sort" + "strconv" + "strings" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/index" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/types" + "camlistore.org/pkg/types/camtypes" +) + +const buffered = 32 // arbitrary channel buffer size +const maxResults = 1000 // arbitrary limit on the number of search results returned +const defaultNumResults = 50 + +// MaxImageSize is the maximum width or height in pixels that we will serve image +// thumbnails at. It is used in the search result UI. +const MaxImageSize = 2000 + +var blobRefPattern = regexp.MustCompile(blob.Pattern) + +func init() { + blobserver.RegisterHandlerConstructor("search", newHandlerFromConfig) +} + +// Handler handles search queries. +type Handler struct { + index index.Interface + owner blob.Ref + + // Corpus optionally specifies the full in-memory metadata corpus + // to use. + // TODO: this may be required in the future, or folded into the index + // interface. + corpus *index.Corpus + + // WebSocket hub + wsHub *wsHub +} + +// GetRecentPermanoder is the interface containing the GetRecentPermanodes method. +type GetRecentPermanoder interface { + // GetRecentPermanodes returns recently-modified permanodes. + // This is a higher-level query returning more metadata than the index.GetRecentPermanodes, + // which only scans the blobrefs but doesn't return anything about the permanodes. + GetRecentPermanodes(*RecentRequest) (*RecentResponse, error) +} + +var _ GetRecentPermanoder = (*Handler)(nil) + +func NewHandler(index index.Interface, owner blob.Ref) *Handler { + sh := &Handler{ + index: index, + owner: owner, + } + sh.wsHub = newWebsocketHub(sh) + go sh.wsHub.run() + sh.subscribeToNewBlobs() + return sh +} + +func (sh *Handler) subscribeToNewBlobs() { + ch := make(chan blob.Ref, buffered) + blobserver.GetHub(sh.index).RegisterListener(ch) + go func() { + for br := range ch { + bm, err := sh.index.GetBlobMeta(br) + if err == nil { + sh.wsHub.newBlobRecv <- bm.CamliType + } + } + }() +} + +func (h *Handler) SetCorpus(c *index.Corpus) { + h.corpus = c +} + +// SendStatusUpdate sends a JSON status map to any connected WebSocket clients. +func (h *Handler) SendStatusUpdate(status json.RawMessage) { + h.wsHub.statusUpdate <- status +} + +func newHandlerFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Handler, error) { + indexPrefix := conf.RequiredString("index") // TODO: add optional help tips here? + ownerBlobStr := conf.RequiredString("owner") + devBlockStartupPrefix := conf.OptionalString("devBlockStartupOn", "") + slurpToMemory := conf.OptionalBool("slurpToMemory", false) + if err := conf.Validate(); err != nil { + return nil, err + } + + if devBlockStartupPrefix != "" { + _, err := ld.GetHandler(devBlockStartupPrefix) + if err != nil { + return nil, fmt.Errorf("search handler references bogus devBlockStartupOn handler %s: %v", devBlockStartupPrefix, err) + } + } + + indexHandler, err := ld.GetHandler(indexPrefix) + if err != nil { + return nil, fmt.Errorf("search config references unknown handler %q", indexPrefix) + } + indexer, ok := indexHandler.(index.Interface) + if !ok { + return nil, fmt.Errorf("search config references invalid indexer %q (actually a %T)", indexPrefix, indexHandler) + } + ownerBlobRef, ok := blob.Parse(ownerBlobStr) + if !ok { + return nil, fmt.Errorf("search 'owner' has malformed blobref %q; expecting e.g. sha1-xxxxxxxxxxxx", + ownerBlobStr) + } + h := NewHandler(indexer, ownerBlobRef) + if slurpToMemory { + ii := indexer.(*index.Index) + corpus, err := ii.KeepInMemory() + if err != nil { + return nil, fmt.Errorf("error slurping index to memory: %v", err) + } + h.corpus = corpus + } + return h, nil +} + +// Owner returns Handler owner's public key blobref. +func (h *Handler) Owner() blob.Ref { + // TODO: figure out a plan for an owner having multiple active public keys, or public + // key rotation + return h.owner +} + +func (h *Handler) Index() index.Interface { + return h.index +} + +func jsonMap() map[string]interface{} { + return make(map[string]interface{}) +} + +var getHandler = map[string]func(*Handler, http.ResponseWriter, *http.Request){ + "ws": (*Handler).serveWebSocket, + "recent": (*Handler).serveRecentPermanodes, + "permanodeattr": (*Handler).servePermanodesWithAttr, + "describe": (*Handler).serveDescribe, + "claims": (*Handler).serveClaims, + "files": (*Handler).serveFiles, + "signerattrvalue": (*Handler).serveSignerAttrValue, + "signerpaths": (*Handler).serveSignerPaths, + "edgesto": (*Handler).serveEdgesTo, +} + +var postHandler = map[string]func(*Handler, http.ResponseWriter, *http.Request){ + "describe": (*Handler).serveDescribe, + "query": (*Handler).serveQuery, +} + +func (sh *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + suffix := httputil.PathSuffix(req) + + handlers := getHandler + switch { + case httputil.IsGet(req): + // use default from above + case req.Method == "POST": + handlers = postHandler + default: + handlers = nil + } + fn := handlers[strings.TrimPrefix(suffix, "camli/search/")] + if fn != nil { + fn(sh, rw, req) + return + } + + // TODO: discovery for the endpoints & better error message with link to discovery info + ret := camtypes.SearchErrorResponse{ + Error: "Unsupported search path or method", + ErrorType: "input", + } + httputil.ReturnJSON(rw, &ret) +} + +// sanitizeNumResults takes n as a requested number of search results and sanitizes it. +func sanitizeNumResults(n int) int { + if n <= 0 || n > maxResults { + return defaultNumResults + } + return n +} + +// RecentRequest is a request to get a RecentResponse. +type RecentRequest struct { + N int // if zero, default number of results + Before time.Time // if zero, now +} + +func (r *RecentRequest) URLSuffix() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "camli/search/recent?n=%d", r.n()) + if !r.Before.IsZero() { + fmt.Fprintf(&buf, "&before=%s", types.Time3339(r.Before)) + } + return buf.String() +} + +// fromHTTP panics with an httputil value on failure +func (r *RecentRequest) fromHTTP(req *http.Request) { + r.N, _ = strconv.Atoi(req.FormValue("n")) + if before := req.FormValue("before"); before != "" { + r.Before = time.Time(types.ParseTime3339OrZero(before)) + } +} + +// n returns the sanitized maximum number of search results. +func (r *RecentRequest) n() int { + return sanitizeNumResults(r.N) +} + +// WithAttrRequest is a request to get a WithAttrResponse. +type WithAttrRequest struct { + N int // max number of results + Signer blob.Ref // if nil, will use the server's default owner (if configured) + // Requested attribute. If blank, all attributes are searched (for Value) + // as fulltext. + Attr string + // Value of the requested attribute. If blank, permanodes which have + // request.Attr as an attribute are searched. + Value string + Fuzzy bool // fulltext search (if supported). +} + +func (r *WithAttrRequest) URLSuffix() string { + return fmt.Sprintf("camli/search/permanodeattr?signer=%v&value=%v&fuzzy=%v&attr=%v&max=%v", + r.Signer, url.QueryEscape(r.Value), r.Fuzzy, r.Attr, r.N) +} + +// fromHTTP panics with an httputil value on failure +func (r *WithAttrRequest) fromHTTP(req *http.Request) { + r.Signer = blob.ParseOrZero(req.FormValue("signer")) + r.Value = req.FormValue("value") + fuzzy := req.FormValue("fuzzy") // exact match if empty + fuzzyMatch := false + if fuzzy != "" { + lowered := strings.ToLower(fuzzy) + if lowered == "true" || lowered == "t" { + fuzzyMatch = true + } + } + r.Attr = req.FormValue("attr") // all attributes if empty + if r.Attr == "" { // and force fuzzy in that case. + fuzzyMatch = true + } + r.Fuzzy = fuzzyMatch + max := req.FormValue("max") + if max != "" { + maxR, err := strconv.Atoi(max) + if err != nil { + panic(httputil.InvalidParameterError("max")) + } + r.N = maxR + } + r.N = r.n() +} + +// n returns the sanitized maximum number of search results. +func (r *WithAttrRequest) n() int { + return sanitizeNumResults(r.N) +} + +// ClaimsRequest is a request to get a ClaimsResponse. +type ClaimsRequest struct { + Permanode blob.Ref + + // AttrFilter optionally filters claims about the given attribute. + // If empty, all claims for the given Permanode are returned. + AttrFilter string +} + +func (r *ClaimsRequest) URLSuffix() string { + return fmt.Sprintf("camli/search/claims?permanode=%v&attrFilter=%s", + r.Permanode, url.QueryEscape(r.AttrFilter)) +} + +// fromHTTP panics with an httputil value on failure +func (r *ClaimsRequest) fromHTTP(req *http.Request) { + r.Permanode = httputil.MustGetBlobRef(req, "permanode") + r.AttrFilter = req.FormValue("attrFilter") +} + +// SignerPathsRequest is a request to get a SignerPathsResponse. +type SignerPathsRequest struct { + Signer blob.Ref + Target blob.Ref +} + +// fromHTTP panics with an httputil value on failure +func (r *SignerPathsRequest) fromHTTP(req *http.Request) { + r.Signer = httputil.MustGetBlobRef(req, "signer") + r.Target = httputil.MustGetBlobRef(req, "target") +} + +// EdgesRequest is a request to get an EdgesResponse. +type EdgesRequest struct { + // The blob we want to find as a reference. + ToRef blob.Ref +} + +// fromHTTP panics with an httputil value on failure +func (r *EdgesRequest) fromHTTP(req *http.Request) { + r.ToRef = httputil.MustGetBlobRef(req, "blobref") +} + +// TODO(mpl): it looks like we never populate RecentResponse.Error*, shouldn't we remove them? +// Same for WithAttrResponse. I suppose it doesn't matter much if we end up removing GetRecentPermanodes anyway... + +// RecentResponse is the JSON response from $searchRoot/camli/search/recent. +type RecentResponse struct { + Recent []*RecentItem `json:"recent"` + Meta MetaMap `json:"meta"` + + Error string `json:"error,omitempty"` + ErrorType string `json:"errorType,omitempty"` +} + +func (r *RecentResponse) Err() error { + if r.Error != "" || r.ErrorType != "" { + if r.ErrorType != "" { + return fmt.Errorf("%s: %s", r.ErrorType, r.Error) + } + return errors.New(r.Error) + } + return nil +} + +// WithAttrResponse is the JSON response from $searchRoot/camli/search/permanodeattr. +type WithAttrResponse struct { + WithAttr []*WithAttrItem `json:"withAttr"` + Meta MetaMap `json:"meta"` + + Error string `json:"error,omitempty"` + ErrorType string `json:"errorType,omitempty"` +} + +func (r *WithAttrResponse) Err() error { + if r.Error != "" || r.ErrorType != "" { + if r.ErrorType != "" { + return fmt.Errorf("%s: %s", r.ErrorType, r.Error) + } + return errors.New(r.Error) + } + return nil +} + +// ClaimsResponse is the JSON response from $searchRoot/camli/search/claims. +type ClaimsResponse struct { + Claims []*ClaimsItem `json:"claims"` +} + +// SignerPathsResponse is the JSON response from $searchRoot/camli/search/signerpaths. +type SignerPathsResponse struct { + Paths []*SignerPathsItem `json:"paths"` + Meta MetaMap `json:"meta"` +} + +// A RecentItem is an item returned from $searchRoot/camli/search/recent in the "recent" list. +type RecentItem struct { + BlobRef blob.Ref `json:"blobref"` + ModTime types.Time3339 `json:"modtime"` + Owner blob.Ref `json:"owner"` +} + +// A WithAttrItem is an item returned from $searchRoot/camli/search/permanodeattr. +type WithAttrItem struct { + Permanode blob.Ref `json:"permanode"` +} + +// A ClaimsItem is an item returned from $searchRoot/camli/search/claims. +type ClaimsItem struct { + BlobRef blob.Ref `json:"blobref"` + Signer blob.Ref `json:"signer"` + Permanode blob.Ref `json:"permanode"` + Date types.Time3339 `json:"date"` + Type string `json:"type"` + Attr string `json:"attr,omitempty"` + Value string `json:"value,omitempty"` +} + +// A SignerPathsItem is an item returned from $searchRoot/camli/search/signerpaths. +type SignerPathsItem struct { + ClaimRef blob.Ref `json:"claimRef"` + BaseRef blob.Ref `json:"baseRef"` + Suffix string `json:"suffix"` +} + +// EdgesResponse is the JSON response from $searchRoot/camli/search/edgesto. +type EdgesResponse struct { + ToRef blob.Ref `json:"toRef"` + EdgesTo []*EdgeItem `json:"edgesTo"` +} + +// An EdgeItem is an item returned from $searchRoot/camli/search/edgesto. +type EdgeItem struct { + From blob.Ref `json:"from"` + FromType string `json:"fromType"` +} + +var testHookBug121 = func() {} + +// GetRecentPermanodes returns recently-modified permanodes. +func (sh *Handler) GetRecentPermanodes(req *RecentRequest) (*RecentResponse, error) { + ch := make(chan camtypes.RecentPermanode) + errch := make(chan error, 1) + before := time.Now() + if !req.Before.IsZero() { + before = req.Before + } + go func() { + errch <- sh.index.GetRecentPermanodes(ch, sh.owner, req.n(), before) + }() + + dr := sh.NewDescribeRequest() + + var recent []*RecentItem + for res := range ch { + dr.Describe(res.Permanode, 2) + recent = append(recent, &RecentItem{ + BlobRef: res.Permanode, + Owner: res.Signer, + ModTime: types.Time3339(res.LastModTime), + }) + testHookBug121() // http://camlistore.org/issue/121 + } + + if err := <-errch; err != nil { + return nil, err + } + + metaMap, err := dr.metaMap() + if err != nil { + return nil, err + } + + res := &RecentResponse{ + Recent: recent, + Meta: metaMap, + } + return res, nil +} + +func (sh *Handler) serveRecentPermanodes(rw http.ResponseWriter, req *http.Request) { + defer httputil.RecoverJSON(rw, req) + var rr RecentRequest + rr.fromHTTP(req) + res, err := sh.GetRecentPermanodes(&rr) + if err != nil { + httputil.ServeJSONError(rw, err) + return + } + httputil.ReturnJSON(rw, res) +} + +// GetPermanodesWithAttr returns permanodes with attribute req.Attr +// having the req.Value as a value. +// See WithAttrRequest for more details about the query. +func (sh *Handler) GetPermanodesWithAttr(req *WithAttrRequest) (*WithAttrResponse, error) { + ch := make(chan blob.Ref, buffered) + errch := make(chan error, 1) + go func() { + signer := req.Signer + if !signer.Valid() { + signer = sh.owner + } + errch <- sh.index.SearchPermanodesWithAttr(ch, + &camtypes.PermanodeByAttrRequest{ + Attribute: req.Attr, + Query: req.Value, + Signer: signer, + FuzzyMatch: req.Fuzzy, + MaxResults: req.N, + }) + }() + + dr := sh.NewDescribeRequest() + + var withAttr []*WithAttrItem + for res := range ch { + dr.Describe(res, 2) + withAttr = append(withAttr, &WithAttrItem{ + Permanode: res, + }) + } + + metaMap, err := dr.metaMap() + if err != nil { + return nil, err + } + + if err := <-errch; err != nil { + return nil, err + } + + res := &WithAttrResponse{ + WithAttr: withAttr, + Meta: metaMap, + } + return res, nil +} + +// servePermanodesWithAttr uses the indexer to search for the permanodes matching +// the request. +// The valid values for the "attr" key in the request (i.e the only attributes +// for a permanode which are actually indexed as such) are "tag" and "title". +func (sh *Handler) servePermanodesWithAttr(rw http.ResponseWriter, req *http.Request) { + defer httputil.RecoverJSON(rw, req) + var wr WithAttrRequest + wr.fromHTTP(req) + res, err := sh.GetPermanodesWithAttr(&wr) + if err != nil { + httputil.ServeJSONError(rw, err) + return + } + httputil.ReturnJSON(rw, res) +} + +// GetClaims returns the claims on req.Permanode signed by sh.owner. +func (sh *Handler) GetClaims(req *ClaimsRequest) (*ClaimsResponse, error) { + if !req.Permanode.Valid() { + return nil, errors.New("Error getting claims: nil permanode.") + } + var claims []camtypes.Claim + claims, err := sh.index.AppendClaims(claims, req.Permanode, sh.owner, req.AttrFilter) + if err != nil { + return nil, fmt.Errorf("Error getting claims of %s: %v", req.Permanode.String(), err) + } + sort.Sort(camtypes.ClaimsByDate(claims)) + var jclaims []*ClaimsItem + for _, claim := range claims { + jclaim := &ClaimsItem{ + BlobRef: claim.BlobRef, + Signer: claim.Signer, + Permanode: claim.Permanode, + Date: types.Time3339(claim.Date), + Type: claim.Type, + Attr: claim.Attr, + Value: claim.Value, + } + jclaims = append(jclaims, jclaim) + } + + res := &ClaimsResponse{ + Claims: jclaims, + } + return res, nil +} + +func (sh *Handler) serveClaims(rw http.ResponseWriter, req *http.Request) { + defer httputil.RecoverJSON(rw, req) + var cr ClaimsRequest + cr.fromHTTP(req) + res, err := sh.GetClaims(&cr) + if err != nil { + httputil.ServeJSONError(rw, err) + return + } + httputil.ReturnJSON(rw, res) +} + +func (sh *Handler) serveFiles(rw http.ResponseWriter, req *http.Request) { + var ret camtypes.FileSearchResponse + defer httputil.ReturnJSON(rw, &ret) + + br, ok := blob.Parse(req.FormValue("wholedigest")) + if !ok { + ret.Error = "Missing or invalid 'wholedigest' param" + ret.ErrorType = "input" + return + } + + files, err := sh.index.ExistingFileSchemas(br) + if err != nil { + ret.Error = err.Error() + ret.ErrorType = "server" + return + } + + // the ui code expects an object + if files == nil { + files = []blob.Ref{} + } + + ret.Files = files + return +} + +// SignerAttrValueResponse is the JSON response to $search/camli/search/signerattrvalue +type SignerAttrValueResponse struct { + Permanode blob.Ref `json:"permanode"` + Meta MetaMap `json:"meta"` +} + +func (sh *Handler) serveSignerAttrValue(rw http.ResponseWriter, req *http.Request) { + defer httputil.RecoverJSON(rw, req) + signer := httputil.MustGetBlobRef(req, "signer") + attr := httputil.MustGet(req, "attr") + value := httputil.MustGet(req, "value") + + pn, err := sh.index.PermanodeOfSignerAttrValue(signer, attr, value) + if err != nil { + httputil.ServeJSONError(rw, err) + return + } + + dr := sh.NewDescribeRequest() + dr.Describe(pn, 2) + metaMap, err := dr.metaMap() + if err != nil { + httputil.ServeJSONError(rw, err) + return + } + + httputil.ReturnJSON(rw, &SignerAttrValueResponse{ + Permanode: pn, + Meta: metaMap, + }) +} + +// EdgesTo returns edges that reference req.RefTo. +// It filters out since-deleted permanode edges. +func (sh *Handler) EdgesTo(req *EdgesRequest) (*EdgesResponse, error) { + toRef := req.ToRef + toRefStr := toRef.String() + var edgeItems []*EdgeItem + + edges, err := sh.index.EdgesTo(toRef, nil) + if err != nil { + panic(err) + } + + type edgeOrError struct { + edge *EdgeItem // or nil + err error + } + resc := make(chan edgeOrError) + verify := func(edge *camtypes.Edge) { + db, err := sh.NewDescribeRequest().DescribeSync(edge.From) + if err != nil { + resc <- edgeOrError{err: err} + return + } + found := false + if db.Permanode != nil { + for attr, vv := range db.Permanode.Attr { + if index.IsBlobReferenceAttribute(attr) { + for _, v := range vv { + if v == toRefStr { + found = true + } + } + } + } + } + var ei *EdgeItem + if found { + ei = &EdgeItem{ + From: edge.From, + FromType: "permanode", + } + } + resc <- edgeOrError{edge: ei} + } + verifying := 0 + for _, edge := range edges { + if edge.FromType == "permanode" { + verifying++ + go verify(edge) + continue + } + ei := &EdgeItem{ + From: edge.From, + FromType: edge.FromType, + } + edgeItems = append(edgeItems, ei) + } + for i := 0; i < verifying; i++ { + res := <-resc + if res.err != nil { + return nil, res.err + } + if res.edge != nil { + edgeItems = append(edgeItems, res.edge) + } + } + + return &EdgesResponse{ + ToRef: toRef, + EdgesTo: edgeItems, + }, nil +} + +// Unlike the index interface's EdgesTo method, the "edgesto" Handler +// here additionally filters out since-deleted permanode edges. +func (sh *Handler) serveEdgesTo(rw http.ResponseWriter, req *http.Request) { + defer httputil.RecoverJSON(rw, req) + var er EdgesRequest + er.fromHTTP(req) + res, err := sh.EdgesTo(&er) + if err != nil { + httputil.ServeJSONError(rw, err) + return + } + httputil.ReturnJSON(rw, res) +} + +func (sh *Handler) serveQuery(rw http.ResponseWriter, req *http.Request) { + defer httputil.RecoverJSON(rw, req) + + var sq SearchQuery + if err := sq.fromHTTP(req); err != nil { + httputil.ServeJSONError(rw, err) + return + } + + sr, err := sh.Query(&sq) + if err != nil { + httputil.ServeJSONError(rw, err) + return + } + + httputil.ReturnJSON(rw, sr) +} + +// GetSignerPaths returns paths with a target of req.Target. +func (sh *Handler) GetSignerPaths(req *SignerPathsRequest) (*SignerPathsResponse, error) { + if !req.Signer.Valid() { + return nil, errors.New("Error getting signer paths: nil signer.") + } + if !req.Target.Valid() { + return nil, errors.New("Error getting signer paths: nil target.") + } + paths, err := sh.index.PathsOfSignerTarget(req.Signer, req.Target) + if err != nil { + return nil, fmt.Errorf("Error getting paths of %s: %v", req.Target.String(), err) + } + var jpaths []*SignerPathsItem + for _, path := range paths { + jpaths = append(jpaths, &SignerPathsItem{ + ClaimRef: path.Claim, + BaseRef: path.Base, + Suffix: path.Suffix, + }) + } + + dr := sh.NewDescribeRequest() + for _, path := range paths { + dr.Describe(path.Base, 2) + } + metaMap, err := dr.metaMap() + if err != nil { + return nil, err + } + + res := &SignerPathsResponse{ + Paths: jpaths, + Meta: metaMap, + } + return res, nil +} + +func (sh *Handler) serveSignerPaths(rw http.ResponseWriter, req *http.Request) { + defer httputil.RecoverJSON(rw, req) + var sr SignerPathsRequest + sr.fromHTTP(req) + + res, err := sh.GetSignerPaths(&sr) + if err != nil { + httputil.ServeJSONError(rw, err) + return + } + httputil.ReturnJSON(rw, res) +} + +const camliTypePrefix = "application/json; camliType=" diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/handler_test.go b/vendor/github.com/camlistore/camlistore/pkg/search/handler_test.go new file mode 100644 index 00000000..0663ac0a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/handler_test.go @@ -0,0 +1,760 @@ +/* +Copyright 2011 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "path/filepath" + "reflect" + "sort" + "strings" + "testing" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/index" + "camlistore.org/pkg/index/indextest" + "camlistore.org/pkg/osutil" + . "camlistore.org/pkg/search" + "camlistore.org/pkg/test" +) + +// An indexOwnerer is something that knows who owns the index. +// It is implemented by indexAndOwner for use by TestHandler. +type indexOwnerer interface { + IndexOwner() blob.Ref +} + +type indexAndOwner struct { + index.Interface + owner blob.Ref +} + +func (io indexAndOwner) IndexOwner() blob.Ref { + return io.owner +} + +type handlerTest struct { + // setup is responsible for populating the index before the + // handler is invoked. + // + // A FakeIndex is constructed and provided to setup and is + // generally then returned as the Index to use, but an + // alternate Index may be returned instead, in which case the + // FakeIndex is not used. + setup func(fi *test.FakeIndex) index.Interface + + name string // test name + query string // the HTTP path + optional query suffix after "camli/search/" + postBody string // if non-nil, a POST request + + want map[string]interface{} + // wantDescribed is a list of blobref strings that should've been + // described in meta. If want is nil and this is non-zero length, + // want is ignored. + wantDescribed []string +} + +var owner = blob.MustParse("abcown-123") + +func parseJSON(s string) map[string]interface{} { + m := make(map[string]interface{}) + err := json.Unmarshal([]byte(s), &m) + if err != nil { + panic(err) + } + return m +} + +// addToClockOrigin returns the given Duration added +// to test.ClockOrigin, in UTC, and RFC3339Nano formatted. +func addToClockOrigin(d time.Duration) string { + return test.ClockOrigin.Add(d).UTC().Format(time.RFC3339Nano) +} + +func handlerDescribeTestSetup(fi *test.FakeIndex) index.Interface { + pn := blob.MustParse("perma-123") + fi.AddMeta(pn, "permanode", 123) + fi.AddClaim(owner, pn, "set-attribute", "camliContent", "fakeref-232") + fi.AddMeta(blob.MustParse("fakeref-232"), "", 878) + + // Test deleting all attributes + fi.AddClaim(owner, pn, "add-attribute", "wont-be-present", "x") + fi.AddClaim(owner, pn, "add-attribute", "wont-be-present", "y") + fi.AddClaim(owner, pn, "del-attribute", "wont-be-present", "") + + // Test deleting a specific attribute. + fi.AddClaim(owner, pn, "add-attribute", "only-delete-b", "a") + fi.AddClaim(owner, pn, "add-attribute", "only-delete-b", "b") + fi.AddClaim(owner, pn, "add-attribute", "only-delete-b", "c") + fi.AddClaim(owner, pn, "del-attribute", "only-delete-b", "b") + return fi +} + +// extends handlerDescribeTestSetup but adds a camliContentImage to pn. +func handlerDescribeTestSetupWithImage(fi *test.FakeIndex) index.Interface { + handlerDescribeTestSetup(fi) + pn := blob.MustParse("perma-123") + imageRef := blob.MustParse("fakeref-789") + fi.AddMeta(imageRef, "", 789) + fi.AddClaim(owner, pn, "set-attribute", "camliContentImage", imageRef.String()) + return fi +} + +// extends handlerDescribeTestSetup but adds various embedded references to other nodes. +func handlerDescribeTestSetupWithEmbeddedRefs(fi *test.FakeIndex) index.Interface { + handlerDescribeTestSetup(fi) + pn := blob.MustParse("perma-123") + c1 := blob.MustParse("fakeref-01") + c2 := blob.MustParse("fakeref-02") + c3 := blob.MustParse("fakeref-03") + c4 := blob.MustParse("fakeref-04") + c5 := blob.MustParse("fakeref-05") + c6 := blob.MustParse("fakeref-06") + fi.AddMeta(c1, "", 1) + fi.AddMeta(c2, "", 2) + fi.AddMeta(c3, "", 3) + fi.AddMeta(c4, "", 4) + fi.AddMeta(c5, "", 5) + fi.AddMeta(c6, "", 6) + fi.AddClaim(owner, pn, "set-attribute", c1.String(), "foo") + fi.AddClaim(owner, pn, "set-attribute", "foo,"+c2.String()+"=bar", "foo") + fi.AddClaim(owner, pn, "set-attribute", "foo:"+c3.String()+"?bar,"+c4.String(), "foo") + fi.AddClaim(owner, pn, "set-attribute", "foo", c5.String()) + fi.AddClaim(owner, pn, "add-attribute", "bar", "baz") + fi.AddClaim(owner, pn, "add-attribute", "bar", "monkey\n"+c6.String()) + return fi +} + +var handlerTests = []handlerTest{ + { + name: "describe-missing", + setup: func(fi *test.FakeIndex) index.Interface { return fi }, + query: "describe?blobref=eabfakeref-0555", + want: parseJSON(`{ + "meta": { + } + }`), + }, + + { + name: "describe-jpeg-blob", + setup: func(fi *test.FakeIndex) index.Interface { + fi.AddMeta(blob.MustParse("abfakeref-0555"), "", 999) + return fi + }, + query: "describe?blobref=abfakeref-0555", + want: parseJSON(`{ + "meta": { + "abfakeref-0555": { + "blobRef": "abfakeref-0555", + "size": 999 + } + } + }`), + }, + + { + name: "describe-permanode", + setup: handlerDescribeTestSetup, + query: "describe", + postBody: `{ + "blobref": "perma-123", + "rules": [ + {"attrs": ["camliContent"]} + ] +}`, + want: parseJSON(`{ + "meta": { + "fakeref-232": { + "blobRef": "fakeref-232", + "size": 878 + }, + "perma-123": { + "blobRef": "perma-123", + "camliType": "permanode", + "size": 123, + "permanode": { + "attr": { + "camliContent": [ "fakeref-232" ], + "only-delete-b": [ "a", "c" ] + }, + "modtime": "` + addToClockOrigin(8*time.Second) + `" + } + } + } + }`), + }, + + { + name: "describe-permanode-image", + setup: handlerDescribeTestSetupWithImage, + query: "describe", + postBody: `{ + "blobref": "perma-123", + "rules": [ + {"attrs": ["camliContent", "camliContentImage"]} + ] +}`, + want: parseJSON(`{ + "meta": { + "fakeref-232": { + "blobRef": "fakeref-232", + "size": 878 + }, + "fakeref-789": { + "blobRef": "fakeref-789", + "size": 789 + }, + "perma-123": { + "blobRef": "perma-123", + "camliType": "permanode", + "size": 123, + "permanode": { + "attr": { + "camliContent": [ "fakeref-232" ], + "camliContentImage": [ "fakeref-789" ], + "only-delete-b": [ "a", "c" ] + }, + "modtime": "` + addToClockOrigin(9*time.Second) + `" + } + } + } + }`), + }, + + // TODO(bradfitz): we'll probably will want to delete or redo this + // test when we remove depth=N support from describe. + { + name: "describe-permanode-embedded-references", + setup: handlerDescribeTestSetupWithEmbeddedRefs, + query: "describe?blobref=perma-123&depth=2", + want: parseJSON(`{ + "meta": { + "fakeref-01": { + "blobRef": "fakeref-01", + "size": 1 + }, + "fakeref-02": { + "blobRef": "fakeref-02", + "size": 2 + }, + "fakeref-03": { + "blobRef": "fakeref-03", + "size": 3 + }, + "fakeref-04": { + "blobRef": "fakeref-04", + "size": 4 + }, + "fakeref-05": { + "blobRef": "fakeref-05", + "size": 5 + }, + "fakeref-06": { + "blobRef": "fakeref-06", + "size": 6 + }, + "fakeref-232": { + "blobRef": "fakeref-232", + "size": 878 + }, + "perma-123": { + "blobRef": "perma-123", + "camliType": "permanode", + "size": 123, + "permanode": { + "attr": { + "bar": [ + "baz", + "monkey\nfakeref-06" + ], + "fakeref-01": [ + "foo" + ], + "camliContent": [ + "fakeref-232" + ], + "foo": [ + "fakeref-05" + ], + "foo,fakeref-02=bar": [ + "foo" + ], + "foo:fakeref-03?bar,fakeref-04": [ + "foo" + ], + "camliContent": [ "fakeref-232" ], + "only-delete-b": [ "a", "c" ] + }, + "modtime": "` + addToClockOrigin(14*time.Second) + `" + } + } + } + }`), + }, + + { + name: "describe-permanode-timetravel", + setup: handlerDescribeTestSetup, + query: "describe", + postBody: `{ + "blobref": "perma-123", + "at": "` + addToClockOrigin(3*time.Second) + `", + "rules": [ + {"attrs": ["camliContent"]} + ] +}`, + want: parseJSON(`{ + "meta": { + "fakeref-232": { + "blobRef": "fakeref-232", + "size": 878 + }, + "perma-123": { + "blobRef": "perma-123", + "camliType": "permanode", + "size": 123, + "permanode": { + "attr": { + "camliContent": [ "fakeref-232" ], + "wont-be-present": [ "x", "y" ] + }, + "modtime": "` + addToClockOrigin(3*time.Second) + `" + } + } + } + }`), + }, + + // test that describe follows camliPath:foo attributes + { + name: "describe-permanode-follows-camliPath", + setup: func(fi *test.FakeIndex) index.Interface { + pn := blob.MustParse("perma-123") + fi.AddMeta(pn, "permanode", 123) + fi.AddClaim(owner, pn, "set-attribute", "camliPath:foo", "fakeref-123") + + fi.AddMeta(blob.MustParse("fakeref-123"), "", 123) + return fi + }, + query: "describe", + postBody: `{ + "blobref": "perma-123", + "rules": [ + {"attrs": ["camliPath:*"]} + ] +}`, + want: parseJSON(`{ + "meta": { + "fakeref-123": { + "blobRef": "fakeref-123", + "size": 123 + }, + "perma-123": { + "blobRef": "perma-123", + "camliType": "permanode", + "size": 123, + "permanode": { + "attr": { + "camliPath:foo": [ + "fakeref-123" + ] + }, + "modtime": "` + addToClockOrigin(1*time.Second) + `" + } + } + } +}`), + }, + + // Test recent permanodes + { + name: "recent-1", + setup: func(*test.FakeIndex) index.Interface { + // Ignore the fakeindex and use the real (but in-memory) implementation, + // using IndexDeps to populate it. + idx := index.NewMemoryIndex() + id := indextest.NewIndexDeps(idx) + + pn := id.NewPlannedPermanode("pn1") + id.SetAttribute(pn, "title", "Some title") + return indexAndOwner{idx, id.SignerBlobRef} + }, + query: "recent", + want: parseJSON(`{ + "recent": [ + {"blobref": "sha1-7ca7743e38854598680d94ef85348f2c48a44513", + "modtime": "2011-11-28T01:32:37.000123456Z", + "owner": "sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007"} + ], + "meta": { + "sha1-7ca7743e38854598680d94ef85348f2c48a44513": { + "blobRef": "sha1-7ca7743e38854598680d94ef85348f2c48a44513", + "camliType": "permanode", + "permanode": { + "attr": { "title": [ "Some title" ] }, + "modtime": "` + addToClockOrigin(1*time.Second) + `" + }, + "size": 534 + } + } + }`), + }, + + // Test recent permanode of a file + { + name: "recent-file", + setup: func(*test.FakeIndex) index.Interface { + // Ignore the fakeindex and use the real (but in-memory) implementation, + // using IndexDeps to populate it. + idx := index.NewMemoryIndex() + id := indextest.NewIndexDeps(idx) + + // Upload a basic image + camliRootPath, err := osutil.GoPackagePath("camlistore.org") + if err != nil { + panic("Package camlistore.org no found in $GOPATH or $GOPATH not defined") + } + uploadFile := func(file string, modTime time.Time) blob.Ref { + fileName := filepath.Join(camliRootPath, "pkg", "index", "indextest", "testdata", file) + contents, err := ioutil.ReadFile(fileName) + if err != nil { + panic(err) + } + br, _ := id.UploadFile(file, string(contents), modTime) + return br + } + dudeFileRef := uploadFile("dude.jpg", time.Time{}) + + pn := id.NewPlannedPermanode("pn1") + id.SetAttribute(pn, "camliContent", dudeFileRef.String()) + return indexAndOwner{idx, id.SignerBlobRef} + }, + query: "recent", + want: parseJSON(`{ + "recent": [ + {"blobref": "sha1-7ca7743e38854598680d94ef85348f2c48a44513", + "modtime": "2011-11-28T01:32:37.000123456Z", + "owner": "sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007"} + ], + "meta": { + "sha1-7ca7743e38854598680d94ef85348f2c48a44513": { + "blobRef": "sha1-7ca7743e38854598680d94ef85348f2c48a44513", + "camliType": "permanode", + "permanode": { + "attr": { + "camliContent": [ + "sha1-e3f0ee86622dda4d7e8a4a4af51117fb79dbdbbb" + ] + }, + "modtime": "` + addToClockOrigin(1*time.Second) + `" + }, + "size": 534 + }, + "sha1-e3f0ee86622dda4d7e8a4a4af51117fb79dbdbbb": { + "blobRef": "sha1-e3f0ee86622dda4d7e8a4a4af51117fb79dbdbbb", + "camliType": "file", + "size": 184, + "file": { + "fileName": "dude.jpg", + "size": 1932, + "mimeType": "image/jpeg", + "wholeRef": "sha1-142b504945338158e0149d4ed25a41a522a28e88" + }, + "image": { + "width": 50, + "height": 100 + } + } + } + }`), + }, + + // Test recent permanode of a file, in a collection + { + name: "recent-file-collec", + setup: func(*test.FakeIndex) index.Interface { + SetTestHookBug121(func() { + time.Sleep(2 * time.Second) + }) + // Ignore the fakeindex and use the real (but in-memory) implementation, + // using IndexDeps to populate it. + idx := index.NewMemoryIndex() + id := indextest.NewIndexDeps(idx) + + // Upload a basic image + camliRootPath, err := osutil.GoPackagePath("camlistore.org") + if err != nil { + panic("Package camlistore.org no found in $GOPATH or $GOPATH not defined") + } + uploadFile := func(file string, modTime time.Time) blob.Ref { + fileName := filepath.Join(camliRootPath, "pkg", "index", "indextest", "testdata", file) + contents, err := ioutil.ReadFile(fileName) + if err != nil { + panic(err) + } + br, _ := id.UploadFile(file, string(contents), modTime) + return br + } + dudeFileRef := uploadFile("dude.jpg", time.Time{}) + pn := id.NewPlannedPermanode("pn1") + id.SetAttribute(pn, "camliContent", dudeFileRef.String()) + collec := id.NewPlannedPermanode("pn2") + id.SetAttribute(collec, "camliMember", pn.String()) + return indexAndOwner{idx, id.SignerBlobRef} + }, + query: "recent", + want: parseJSON(`{ + "recent": [ + { + "blobref": "sha1-3c8b5d36bd4182c6fe802984832f197786662ccf", + "modtime": "2011-11-28T01:32:38.000123456Z", + "owner": "sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007" + }, + { + "blobref": "sha1-7ca7743e38854598680d94ef85348f2c48a44513", + "modtime": "2011-11-28T01:32:37.000123456Z", + "owner": "sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007" + } + ], + "meta": { + "sha1-3c8b5d36bd4182c6fe802984832f197786662ccf": { + "blobRef": "sha1-3c8b5d36bd4182c6fe802984832f197786662ccf", + "camliType": "permanode", + "size": 534, + "permanode": { + "attr": { + "camliMember": [ + "sha1-7ca7743e38854598680d94ef85348f2c48a44513" + ] + }, + "modtime": "` + addToClockOrigin(2*time.Second) + `" + } + }, + "sha1-7ca7743e38854598680d94ef85348f2c48a44513": { + "blobRef": "sha1-7ca7743e38854598680d94ef85348f2c48a44513", + "camliType": "permanode", + "size": 534, + "permanode": { + "attr": { + "camliContent": [ + "sha1-e3f0ee86622dda4d7e8a4a4af51117fb79dbdbbb" + ] + }, + "modtime": "` + addToClockOrigin(1*time.Second) + `" + } + }, + "sha1-e3f0ee86622dda4d7e8a4a4af51117fb79dbdbbb": { + "blobRef": "sha1-e3f0ee86622dda4d7e8a4a4af51117fb79dbdbbb", + "camliType": "file", + "size": 184, + "file": { + "fileName": "dude.jpg", + "size": 1932, + "mimeType": "image/jpeg", + "wholeRef": "sha1-142b504945338158e0149d4ed25a41a522a28e88" + }, + "image": { + "width": 50, + "height": 100 + } + } + } + }`), + }, + + // Test recent permanodes with thumbnails + { + name: "recent-thumbs", + setup: func(*test.FakeIndex) index.Interface { + // Ignore the fakeindex and use the real (but in-memory) implementation, + // using IndexDeps to populate it. + idx := index.NewMemoryIndex() + id := indextest.NewIndexDeps(idx) + + pn := id.NewPlannedPermanode("pn1") + id.SetAttribute(pn, "title", "Some title") + return indexAndOwner{idx, id.SignerBlobRef} + }, + query: "recent?thumbnails=100", + want: parseJSON(`{ + "recent": [ + {"blobref": "sha1-7ca7743e38854598680d94ef85348f2c48a44513", + "modtime": "2011-11-28T01:32:37.000123456Z", + "owner": "sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007"} + ], + "meta": { + "sha1-7ca7743e38854598680d94ef85348f2c48a44513": { + "blobRef": "sha1-7ca7743e38854598680d94ef85348f2c48a44513", + "camliType": "permanode", + "permanode": { + "attr": { "title": [ "Some title" ] }, + "modtime": "` + addToClockOrigin(1*time.Second) + `" + }, + "size": 534 + } + } + }`), + }, + + // edgeto handler: put a permanode (member) in two parent + // permanodes, then delete the second and verify that edges + // back from member only reveal the first parent. + { + name: "edge-to", + setup: func(*test.FakeIndex) index.Interface { + // Ignore the fakeindex and use the real (but in-memory) implementation, + // using IndexDeps to populate it. + idx := index.NewMemoryIndex() + id := indextest.NewIndexDeps(idx) + + parent1 := id.NewPlannedPermanode("pn1") // sha1-7ca7743e38854598680d94ef85348f2c48a44513 + parent2 := id.NewPlannedPermanode("pn2") + member := id.NewPlannedPermanode("member") // always sha1-9ca84f904a9bc59e6599a53f0a3927636a6dbcae + id.AddAttribute(parent1, "camliMember", member.String()) + id.AddAttribute(parent2, "camliMember", member.String()) + id.DelAttribute(parent2, "camliMember", "") + return indexAndOwner{idx, id.SignerBlobRef} + }, + query: "edgesto?blobref=sha1-9ca84f904a9bc59e6599a53f0a3927636a6dbcae", + want: parseJSON(`{ + "toRef": "sha1-9ca84f904a9bc59e6599a53f0a3927636a6dbcae", + "edgesTo": [ + {"from": "sha1-7ca7743e38854598680d94ef85348f2c48a44513", + "fromType": "permanode"} + ] + }`), + }, +} + +func marshalJSON(v interface{}) string { + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + panic(err) + } + return string(b) +} + +func jmap(v interface{}) map[string]interface{} { + m := make(map[string]interface{}) + if err := json.NewDecoder(strings.NewReader(marshalJSON(v))).Decode(&m); err != nil { + panic(err) + } + return m +} + +func checkNoDups(sliceName string, tests []handlerTest) { + seen := map[string]bool{} + for _, tt := range tests { + if seen[tt.name] { + panic(fmt.Sprintf("duplicate handlerTest named %q in var %s", tt.name, sliceName)) + } + seen[tt.name] = true + } +} + +func init() { + checkNoDups("handlerTests", handlerTests) +} + +func (ht handlerTest) test(t *testing.T) { + SetTestHookBug121(func() {}) + + fakeIndex := test.NewFakeIndex() + idx := ht.setup(fakeIndex) + + indexOwner := owner + if io, ok := idx.(indexOwnerer); ok { + indexOwner = io.IndexOwner() + } + h := NewHandler(idx, indexOwner) + + var body io.Reader + var method = "GET" + if ht.postBody != "" { + method = "POST" + body = strings.NewReader(ht.postBody) + } + req, err := http.NewRequest(method, "/camli/search/"+ht.query, body) + if err != nil { + t.Fatalf("%s: bad query: %v", ht.name, err) + } + req.Header.Set(httputil.PathSuffixHeader, req.URL.Path[1:]) + + rr := httptest.NewRecorder() + rr.Body = new(bytes.Buffer) + + h.ServeHTTP(rr, req) + got := rr.Body.Bytes() + + if len(ht.wantDescribed) > 0 { + dr := new(DescribeResponse) + if err := json.NewDecoder(bytes.NewReader(got)).Decode(dr); err != nil { + t.Fatalf("On test %s: Non-JSON response: %s", ht.name, got) + } + var gotDesc []string + for k := range dr.Meta { + gotDesc = append(gotDesc, k) + } + sort.Strings(ht.wantDescribed) + sort.Strings(gotDesc) + if !reflect.DeepEqual(gotDesc, ht.wantDescribed) { + t.Errorf("On test %s: described blobs:\n%v\nwant:\n%v\n", + ht.name, gotDesc, ht.wantDescribed) + } + if ht.want == nil { + return + } + } + + want, _ := json.MarshalIndent(ht.want, "", " ") + trim := bytes.TrimSpace + + if bytes.Equal(trim(got), trim(want)) { + return + } + + // Try with re-encoded got, since the JSON ordering doesn't matter + // to the test, + gotj := parseJSON(string(got)) + got2, _ := json.MarshalIndent(gotj, "", " ") + if bytes.Equal(got2, want) { + return + } + diff := test.Diff(want, got2) + + t.Errorf("test %s:\nwant: %s\n got: %s\ndiff:\n%s", ht.name, want, got, diff) +} + +func TestHandler(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + return + } + defer SetTestHookBug121(func() {}) + for _, ht := range handlerTests { + ht.test(t) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/lexer.go b/vendor/github.com/camlistore/camlistore/pkg/search/lexer.go new file mode 100644 index 00000000..ea4faa90 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/lexer.go @@ -0,0 +1,315 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// This is the lexer for search expressions (see expr.go). + +package search + +import ( + "fmt" + "strings" + "unicode" + "unicode/utf8" +) + +type tokenType int + +const ( + tokenAnd tokenType = iota + tokenArg + tokenClose + tokenColon + tokenEOF + tokenError + tokenLiteral + tokenNot + tokenOpen + tokenOr + tokenPredicate + tokenQuotedArg + tokenQuotedLiteral +) + +const ( + eof = -1 // -1 is unused in utf8 + whitespace = "\t\n\f\v\r " + opBound = whitespace + "(" +) + +// IsSearchWordRune defines the runes that can be used in unquoted predicate arguments +// or unquoted literals. These are all non-space unicode characters except ':' which is +// used for predicate marking, and '(', ')', which are used for predicate grouping. +func isSearchWordRune(r rune) bool { + switch r { + case ':', ')', '(', eof: + return false + } + return !unicode.IsSpace(r) +} + +type token struct { + typ tokenType + val string + start int +} + +func (t token) String() string { + switch t.typ { + case tokenEOF: + return "EOF" + case tokenError: + return fmt.Sprintf("{err:%q at pos: %d}", t.val, t.start) + } + return fmt.Sprintf("{t:%v,%q (col: %d)}", t.typ, t.val, t.start) +} + +type lexer struct { + input string + start int + pos int + width int + tokens chan token + state stateFn +} + +func (l *lexer) emit(typ tokenType) { + l.tokens <- token{typ, l.input[l.start:l.pos], l.start} + l.start = l.pos +} + +func (l *lexer) next() (r rune) { + if l.pos >= len(l.input) { + l.width = 0 + return eof + } + r, l.width = utf8.DecodeRuneInString(l.input[l.pos:]) + l.pos += l.width + return +} + +func (l *lexer) ignore() { + l.start = l.pos +} + +func (l *lexer) backup() { + l.pos -= l.width +} + +func (l *lexer) peek() rune { + r := l.next() + l.backup() + return r +} + +func (l *lexer) accept(valid string) bool { + if strings.IndexRune(valid, l.next()) >= 0 { + return true + } + l.backup() + return false +} + +func (l *lexer) acceptString(s string) bool { + for _, r := range s { + if l.next() != r { + l.backup() + return false + } + } + return true +} + +func (l *lexer) acceptRun(valid string) { + for strings.IndexRune(valid, l.next()) >= 0 { + } + l.backup() +} + +func (l *lexer) acceptRunFn(valid func(rune) bool) { + for valid(l.next()) { + } + l.backup() +} + +func (l *lexer) errorf(format string, args ...interface{}) stateFn { + l.tokens <- token{ + typ: tokenError, + val: fmt.Sprintf(format, args...), + start: l.start, + } + return nil +} + +func lex(input string) (*lexer, chan token) { + l := &lexer{ + input: input, + tokens: make(chan token), + state: readExp, + } + go l.run() + return l, l.tokens +} + +func (l *lexer) run() { + for { + if l.state == nil { + close(l.tokens) + return + } + l.state = l.state(l) + } +} + +// +// State functions +// +type stateFn func(*lexer) stateFn + +func readNeg(l *lexer) stateFn { + l.accept("-") + l.emit(tokenNot) + return readExp +} + +func readClose(l *lexer) stateFn { + l.accept(")") + l.emit(tokenClose) + return readOperator +} + +func readOpen(l *lexer) stateFn { + l.accept("(") + l.emit(tokenOpen) + return readExp +} + +func readColon(l *lexer) stateFn { + l.accept(":") + l.emit(tokenColon) + return readArg +} + +func readPredicate(l *lexer) stateFn { + l.acceptRunFn(unicode.IsLetter) + switch l.peek() { + case ':': + l.emit(tokenPredicate) + return readColon + } + return readLiteral +} + +func readLiteral(l *lexer) stateFn { + l.acceptRunFn(isSearchWordRune) + l.emit(tokenLiteral) + return readOperator +} + +func readArg(l *lexer) stateFn { + if l.peek() == '"' { + return readQuotedArg + } + l.acceptRunFn(isSearchWordRune) + l.emit(tokenArg) + if l.peek() == ':' { + return readColon + } + return readOperator +} + +func readAND(l *lexer) stateFn { + if l.acceptString("and") && l.accept(opBound) { + l.backup() + l.emit(tokenAnd) + return readExp + } else { + return readPredicate + } +} + +func readOR(l *lexer) stateFn { + if l.acceptString("or") && l.accept(opBound) { + l.backup() + l.emit(tokenOr) + return readExp + } else { + return readPredicate + } +} + +func runQuoted(l *lexer) bool { + l.accept("\"") + for { + r := l.next() + switch r { + case eof: + return false + case '\\': + l.next() + case '"': + return true + } + } +} + +func readQuotedLiteral(l *lexer) stateFn { + if !runQuoted(l) { + return l.errorf("Unclosed quote") + } + l.emit(tokenQuotedLiteral) + return readOperator +} + +func readQuotedArg(l *lexer) stateFn { + if !runQuoted(l) { + return l.errorf("Unclosed quote") + } + l.emit(tokenQuotedArg) + if l.peek() == ':' { + return readColon + } + return readOperator +} + +func readExp(l *lexer) stateFn { + l.acceptRun(whitespace) + l.ignore() + switch l.peek() { + case eof: + return nil + case '(': + return readOpen + case ')': + return readClose + case '-': + return readNeg + case '"': + return readQuotedLiteral + } + return readPredicate +} + +func readOperator(l *lexer) stateFn { + l.acceptRun(whitespace) + l.ignore() + switch l.peek() { + case 'a': + return readAND + case 'o': + return readOR + } + return readExp +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/lexer_test.go b/vendor/github.com/camlistore/camlistore/pkg/search/lexer_test.go new file mode 100644 index 00000000..2042b7e9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/lexer_test.go @@ -0,0 +1,191 @@ +package search + +import ( + "reflect" + "testing" +) + +const scaryQuote = `"\"Hi there\""` + +var lexerTests = []struct { + in string + want []token +}{ + { + in: "width:++1", + want: []token{ + {tokenPredicate, "width", 0}, + {tokenColon, ":", 5}, + {tokenArg, "++1", 6}, + }, + }, + + { + in: "and and and", + want: []token{ + {tokenLiteral, "and", 0}, + {tokenAnd, "and", 4}, + {tokenLiteral, "and", 8}, + }, + }, + + { + in: "and nd and", + want: []token{ + {tokenLiteral, "and", 0}, + {tokenLiteral, "nd", 4}, + {tokenLiteral, "and", 7}, + }, + }, + + { + in: "or or or", + want: []token{ + {tokenLiteral, "or", 0}, + {tokenOr, "or", 3}, + {tokenLiteral, "or", 6}, + }, + }, + + { + in: "or r or", + want: []token{ + {tokenLiteral, "or", 0}, + {tokenLiteral, "r", 3}, + {tokenLiteral, "or", 5}, + }, + }, + + { + in: "(or or or) and or", + want: []token{ + {tokenOpen, "(", 0}, + {tokenLiteral, "or", 1}, + {tokenOr, "or", 4}, + {tokenLiteral, "or", 7}, + {tokenClose, ")", 9}, + {tokenAnd, "and", 11}, + {tokenLiteral, "or", 15}, + }, + }, + + { + in: `(or or "or) and or`, + want: []token{ + {tokenOpen, "(", 0}, + {tokenLiteral, "or", 1}, + {tokenOr, "or", 4}, + {tokenError, "Unclosed quote", 7}, + }, + }, + + { + in: "bar and baz", + want: []token{{tokenLiteral, "bar", 0}, {tokenAnd, "and", 4}, {tokenLiteral, "baz", 8}}, + }, + + { + in: "foo or bar", + want: []token{{tokenLiteral, "foo", 0}, {tokenOr, "or", 4}, {tokenLiteral, "bar", 7}}, + }, + + { + in: "foo or (bar )", + want: []token{{tokenLiteral, "foo", 0}, {tokenOr, "or", 4}, {tokenOpen, "(", 7}, {tokenLiteral, "bar", 8}, {tokenClose, ")", 12}}, + }, + + { + in: "foo or bar:foo:baz", + want: []token{ + {tokenLiteral, "foo", 0}, + {tokenOr, "or", 4}, + {tokenPredicate, "bar", 7}, + {tokenColon, ":", 10}, + {tokenArg, "foo", 11}, + {tokenColon, ":", 14}, + {tokenArg, "baz", 15}, + }, + }, + + { + in: "--foo or - bar", + want: []token{ + {tokenNot, "-", 0}, + {tokenNot, "-", 1}, + {tokenLiteral, "foo", 2}, + {tokenOr, "or", 6}, + {tokenNot, "-", 9}, + {tokenLiteral, "bar", 11}, + }, + }, + + { + in: "foo:bar:baz or bar", + want: []token{ + {tokenPredicate, "foo", 0}, + {tokenColon, ":", 3}, + {tokenArg, "bar", 4}, + {tokenColon, ":", 7}, + {tokenArg, "baz", 8}, + {tokenOr, "or", 12}, + {tokenLiteral, "bar", 15}, + }, + }, + + { + in: "is:pano or", + want: []token{ + {tokenPredicate, "is", 0}, + {tokenColon, ":", 2}, + {tokenArg, "pano", 3}, + {tokenLiteral, "or", 8}, + }, + }, + + { + in: "foo:" + scaryQuote + " or bar", + want: []token{ + {tokenPredicate, "foo", 0}, + {tokenColon, ":", 3}, + {tokenQuotedArg, scaryQuote, 4}, + {tokenOr, "or", 19}, + {tokenLiteral, "bar", 22}, + }, + }, + + { + in: scaryQuote, + want: []token{ + {tokenQuotedLiteral, scaryQuote, 0}}, + }, + + { + in: "foo:", + want: []token{ + {tokenPredicate, "foo", 0}, + {tokenColon, ":", 3}, + {tokenArg, "", 4}, + }, + }, +} + +func array(in string) (parsed []token) { + _, tokens := lex(in) + for token := range tokens { + if token.typ == tokenEOF { + break + } + parsed = append(parsed, token) + } + return +} + +func TestLex(t *testing.T) { + for _, tt := range lexerTests { + + tokens := array(tt.in) + if !reflect.DeepEqual(tokens, tt.want) { + t.Errorf("Got lex(%q)=%v expected %v", tt.in, tokens, tt.want) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/match_test.go b/vendor/github.com/camlistore/camlistore/pkg/search/match_test.go new file mode 100644 index 00000000..d20a3f00 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/match_test.go @@ -0,0 +1,75 @@ +package search + +import ( + "testing" + "time" + + "camlistore.org/pkg/types" +) + +const year = time.Hour * 24 * 365 + +func TestTimeConstraint(t *testing.T) { + tests := []struct { + c *TimeConstraint + t time.Time + want bool + }{ + { + &TimeConstraint{ + Before: types.Time3339(time.Unix(124, 0)), + }, + time.Unix(123, 0), + true, + }, + { + &TimeConstraint{ + Before: types.Time3339(time.Unix(123, 0)), + }, + time.Unix(123, 1), + false, + }, + { + &TimeConstraint{ + After: types.Time3339(time.Unix(123, 0)), + }, + time.Unix(123, 0), + true, + }, + { + &TimeConstraint{ + After: types.Time3339(time.Unix(123, 0)), + }, + time.Unix(123, 1), + true, + }, + { + &TimeConstraint{ + After: types.Time3339(time.Unix(123, 0)), + }, + time.Unix(122, 0), + false, + }, + { + // This test will pass for 20 years at least. + &TimeConstraint{ + InLast: 20 * year, + }, + time.Unix(1384034605, 0), + true, + }, + { + &TimeConstraint{ + InLast: 1 * year, + }, + time.Unix(123, 0), + false, + }, + } + for i, tt := range tests { + got := tt.c.timeMatches(tt.t) + if got != tt.want { + t.Errorf("%d. matches(tc=%+v, t=%v) = %v; want %v", i, tt.c, tt.t, got, tt.want) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/predicate.go b/vendor/github.com/camlistore/camlistore/pkg/search/predicate.go new file mode 100644 index 00000000..f30ba2fe --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/predicate.go @@ -0,0 +1,689 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// These are the search-atom definitions (see expr.go). + +package search + +import ( + "bytes" + "encoding/json" + "fmt" + "regexp" + "strconv" + "strings" + "time" + + "camlistore.org/pkg/context" + "camlistore.org/pkg/geocode" + "camlistore.org/pkg/types" +) + +const base = "0000-01-01T00:00:00Z" + +var ( + // used for width/height ranges. 10 is max length of 32-bit + // int (strconv.Atoi on 32-bit platforms), even though a max + // JPEG dimension is only 16-bit. + whRangeExpr = regexp.MustCompile(`^(\d{0,10})-(\d{0,10})$`) + whValueExpr = regexp.MustCompile(`^(\d{1,10})$`) +) + +// Atoms holds the parsed words of an atom without the colons. +// Eg. tag:holiday becomes atom{"tag", []string{"holiday"}} +// Note that the form of camlisearch atoms implies that len(args) > 0 +type atom struct { + predicate string + args []string +} + +func (a atom) String() string { + s := bytes.NewBufferString(a.predicate) + for _, a := range a.args { + s.WriteRune(':') + s.WriteString(a) + } + return s.String() +} + +// Keyword determines by its matcher when a predicate is used. +type keyword interface { + // Name is the part before the first colon, or the whole atom. + Name() string + // Description provides user documentation for this keyword. Should + // return documentation for max/min values, usage help, or examples. + Description() string + // Match gets called with the predicate and arguments that were parsed. + // It should return true if it wishes to handle this search atom. + // An error if the number of arguments mismatches. + Match(a atom) (bool, error) + // Predicates will be called with the args array from an atom instance. + // Note that len(args) > 0 (see atom-struct comment above). + // It should return a pointer to a Constraint object, expressing the meaning of + // its keyword. + Predicate(ctx *context.Context, args []string) (*Constraint, error) +} + +var keywords []keyword + +// RegisterKeyword registers search atom types. +// TODO (sls) Export for applications? (together with keyword and atom) +func registerKeyword(k keyword) { + keywords = append(keywords, k) +} + +// SearchHelp returns JSON of an array of predicate names and descriptions. +func SearchHelp() string { + type help struct{ Name, Description string } + h := []help{} + for _, p := range keywords { + h = append(h, help{p.Name(), p.Description()}) + } + b, err := json.MarshalIndent(h, "", " ") + if err != nil { + return "Error marshalling" + } + return string(b) +} + +func init() { + // Core predicates + registerKeyword(newAfter()) + registerKeyword(newBefore()) + registerKeyword(newAttribute()) + registerKeyword(newChildrenOf()) + registerKeyword(newFormat()) + registerKeyword(newTag()) + registerKeyword(newTitle()) + + // Image predicates + registerKeyword(newIsImage()) + registerKeyword(newHeight()) + registerKeyword(newIsLandscape()) + registerKeyword(newIsPano()) + registerKeyword(newIsPortait()) + registerKeyword(newWidth()) + + // Custom predicates + registerKeyword(newIsPost()) + registerKeyword(newIsCheckin()) + + // Location predicates + registerKeyword(newHasLocation()) + registerKeyword(newLocation()) +} + +// Helper implementation for mixing into keyword implementations +// that match the full keyword, i.e. 'is:pano' +type matchEqual string + +func (me matchEqual) Name() string { + return string(me) +} + +func (me matchEqual) Match(a atom) (bool, error) { + return string(me) == a.String(), nil +} + +// Helper implementation for mixing into keyword implementations +// that match only the beginning of the keyword, and get their paramertes from +// the rest, i.e. 'width:' for searches like 'width:100-200'. +type matchPrefix struct { + prefix string + count int +} + +func newMatchPrefix(p string) matchPrefix { + return matchPrefix{prefix: p, count: 1} +} + +func (mp matchPrefix) Name() string { + return mp.prefix +} +func (mp matchPrefix) Match(a atom) (bool, error) { + if mp.prefix == a.predicate { + if len(a.args) != mp.count { + return true, fmt.Errorf("Wrong number of arguments for %q, given %d, expected %d", mp.prefix, len(a.args), mp.count) + } else { + return true, nil + } + } else { + return false, nil + } +} + +// Core predicates + +type after struct { + matchPrefix +} + +func newAfter() keyword { + return after{newMatchPrefix("after")} +} + +func (a after) Description() string { + return "date format is RFC3339, but can be shortened as required.\n" + + "i.e. 2011-01-01 is Jan 1 of year 2011 and \"2011\" means the same." +} + +func (a after) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + t, err := parseTimePrefix(args[0]) + if err != nil { + return nil, err + } + tc := &TimeConstraint{} + tc.After = types.Time3339(t) + c := &Constraint{ + Permanode: &PermanodeConstraint{ + Time: tc, + }, + } + return c, nil +} + +type before struct { + matchPrefix +} + +func newBefore() keyword { + return before{newMatchPrefix("before")} +} + +func (b before) Description() string { + return "date format is RFC3339, but can be shortened as required.\n" + + "i.e. 2011-01-01 is Jan 1 of year 2011 and \"2011\" means the same." +} + +func (b before) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + t, err := parseTimePrefix(args[0]) + if err != nil { + return nil, err + } + tc := &TimeConstraint{} + tc.Before = types.Time3339(t) + c := &Constraint{ + Permanode: &PermanodeConstraint{ + Time: tc, + }, + } + return c, nil +} + +type attribute struct { + matchPrefix +} + +func newAttribute() keyword { + return attribute{matchPrefix{"attr", 2}} +} + +func (a attribute) Description() string { + return "match on attribute. Use attr:foo:bar to match nodes having their foo\n" + + "attribute set to bar or attr:foo:~bar to do a substring\n" + + "case-insensitive search for 'bar' in attribute foo" +} + +func (a attribute) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + c := attrConst(args[0], args[1]) + if strings.HasPrefix(args[1], "~") { + // Substring. Hack. Figure out better way to do this. + c.Permanode.Value = "" + c.Permanode.ValueMatches = &StringConstraint{ + Contains: args[1][1:], + CaseInsensitive: true, + } + } + return c, nil +} + +type childrenOf struct { + matchPrefix +} + +func newChildrenOf() keyword { + return childrenOf{newMatchPrefix("childrenof")} +} + +func (k childrenOf) Description() string { + return "Find child permanodes of a parent permanode (or prefix of a parent\n" + + "permanode): childrenof:sha1-527cf12 Only matches permanodes currently." +} + +func (k childrenOf) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + c := &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "parent", + Any: &Constraint{ + BlobRefPrefix: args[0], + }, + }, + }, + } + return c, nil +} + +type format struct { + matchPrefix +} + +func newFormat() keyword { + return format{newMatchPrefix("format")} +} + +func (f format) Description() string { + return "file's format (or MIME-type) such as jpg, pdf, tiff." +} + +func (f format) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + mimeType, err := mimeFromFormat(args[0]) + if err != nil { + return nil, err + } + c := permOfFile(&FileConstraint{ + MIMEType: &StringConstraint{ + Equals: mimeType, + }, + }) + return c, nil +} + +type tag struct { + matchPrefix +} + +func newTag() keyword { + return tag{newMatchPrefix("tag")} +} + +func (t tag) Description() string { + return "match on a tag" +} + +func (t tag) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + return attrConst("tag", args[0]), nil +} + +type title struct { + matchPrefix +} + +func newTitle() keyword { + return title{newMatchPrefix("title")} +} + +func (t title) Description() string { + return "match nodes containing substring in their title" +} + +func (t title) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + c := &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "title", + SkipHidden: true, + ValueMatches: &StringConstraint{ + Contains: args[0], + CaseInsensitive: true, + }, + }, + } + return c, nil +} + +// Image predicates + +type isImage struct { + matchEqual +} + +func newIsImage() keyword { + return isImage{"is:image"} +} + +func (k isImage) Description() string { + return "object is an image" +} + +func (k isImage) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + c := &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + }, + }, + }, + } + return c, nil +} + +type isLandscape struct { + matchEqual +} + +func newIsLandscape() keyword { + return isLandscape{"is:landscape"} +} + +func (k isLandscape) Description() string { + return "the image has a landscape aspect" +} + +func (k isLandscape) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + return whRatio(&FloatConstraint{Min: 1.0}), nil +} + +type isPano struct { + matchEqual +} + +func newIsPano() keyword { + return isPano{"is:pano"} +} + +func (k isPano) Description() string { + return "the image's aspect ratio is over 2 - panorama picture." +} + +func (k isPano) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + return whRatio(&FloatConstraint{Min: 2.0}), nil +} + +type isPortait struct { + matchEqual +} + +func newIsPortait() keyword { + return isPortait{"is:portrait"} +} + +func (k isPortait) Description() string { + return "the image has a portrait aspect" +} + +func (k isPortait) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + return whRatio(&FloatConstraint{Max: 1.0}), nil +} + +type width struct { + matchPrefix +} + +func newWidth() keyword { + return width{newMatchPrefix("width")} +} + +func (w width) Description() string { + return "use width:min-max to match images having a width of at least min\n" + + "and at most max. Use width:min- to specify only an underbound and\n" + + "width:-max to specify only an upperbound.\n" + + "Exact matches should use width:640 " +} + +func (w width) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + mins, maxs, err := parseWHExpression(args[0]) + if err != nil { + return nil, err + } + c := permOfFile(&FileConstraint{ + IsImage: true, + Width: whIntConstraint(mins, maxs), + }) + return c, nil +} + +type height struct { + matchPrefix +} + +func newHeight() keyword { + return height{newMatchPrefix("height")} +} + +func (h height) Description() string { + return "use height:min-max to match images having a height of at least min\n" + + "and at most max. Use height:min- to specify only an underbound and\n" + + "height:-max to specify only an upperbound.\n" + + "Exact matches should use height:480" +} + +func (h height) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + mins, maxs, err := parseWHExpression(args[0]) + if err != nil { + return nil, err + } + c := permOfFile(&FileConstraint{ + IsImage: true, + Height: whIntConstraint(mins, maxs), + }) + return c, nil +} + +// Location predicates + +type location struct { + matchPrefix +} + +func newLocation() keyword { + return location{newMatchPrefix("loc")} +} + +func (l location) Description() string { + return "matches images and permanodes having a location near\n" + + "the specified location. Locations are resolved using\n" + + "maps.googleapis.com. For example: loc:\"new york, new york\" " +} + +func (l location) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + where := args[0] + rects, err := geocode.Lookup(ctx, where) + if err != nil { + return nil, err + } + if len(rects) == 0 { + return nil, fmt.Errorf("No location found for %q", where) + } + var c *Constraint + for i, rect := range rects { + loc := &LocationConstraint{ + West: rect.SouthWest.Long, + East: rect.NorthEast.Long, + North: rect.NorthEast.Lat, + South: rect.SouthWest.Lat, + } + fileLoc := permOfFile(&FileConstraint{ + IsImage: true, + Location: loc, + }) + permLoc := &Constraint{ + Permanode: &PermanodeConstraint{ + Location: loc, + }, + } + rectConstraint := orConst(fileLoc, permLoc) + if i == 0 { + c = rectConstraint + } else { + c = orConst(c, rectConstraint) + } + } + return c, nil +} + +type hasLocation struct { + matchEqual +} + +func newHasLocation() keyword { + return hasLocation{"has:location"} +} + +func (h hasLocation) Description() string { + return "matches images and permanodes that have a location (GPSLatitude\n" + + "and GPSLongitude can be retrieved from the image's EXIF tags)." +} + +func (h hasLocation) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + fileLoc := permOfFile(&FileConstraint{ + IsImage: true, + Location: &LocationConstraint{ + Any: true, + }, + }) + permLoc := &Constraint{ + Permanode: &PermanodeConstraint{ + Location: &LocationConstraint{ + Any: true, + }, + }, + } + return orConst(fileLoc, permLoc), nil +} + +// Helpers + +func attrConst(attr, val string) *Constraint { + c := &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: attr, + SkipHidden: true, + }, + } + if val == "" { + c.Permanode.ValueMatches = &StringConstraint{Empty: true} + } else { + c.Permanode.Value = val + } + return c +} + +func permOfFile(fc *FileConstraint) *Constraint { + return &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{File: fc}, + }, + } +} + +func whRatio(fc *FloatConstraint) *Constraint { + return permOfFile(&FileConstraint{ + IsImage: true, + WHRatio: fc, + }) +} + +func parseWHExpression(expr string) (min, max string, err error) { + if m := whRangeExpr.FindStringSubmatch(expr); m != nil { + return m[1], m[2], nil + } + if m := whValueExpr.FindStringSubmatch(expr); m != nil { + return m[1], m[1], nil + } + return "", "", fmt.Errorf("Unable to parse %q as range, wanted something like 480-1024, 480-, -1024 or 1024", expr) +} + +func parseTimePrefix(when string) (time.Time, error) { + if len(when) < len(base) { + when += base[len(when):] + } + return time.Parse(time.RFC3339, when) +} + +func whIntConstraint(mins, maxs string) *IntConstraint { + ic := &IntConstraint{} + if mins != "" { + if mins == "0" { + ic.ZeroMin = true + } else { + n, _ := strconv.Atoi(mins) + ic.Min = int64(n) + } + } + if maxs != "" { + if maxs == "0" { + ic.ZeroMax = true + } else { + n, _ := strconv.Atoi(maxs) + ic.Max = int64(n) + } + } + return ic +} + +func mimeFromFormat(v string) (string, error) { + if strings.Contains(v, "/") { + return v, nil + } + switch v { + case "jpg", "jpeg": + return "image/jpeg", nil + case "gif": + return "image/gif", nil + case "png": + return "image/png", nil + case "pdf": + return "application/pdf", nil // RFC 3778 + } + return "", fmt.Errorf("Unknown format: %s", v) +} + +// Custom predicates + +type isPost struct { + matchEqual +} + +func newIsPost() keyword { + return isPost{"is:post"} +} + +func (k isPost) Description() string { + return "matches tweets, status updates, blog posts, etc" +} + +func (k isPost) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + return &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliNodeType", + Value: "twitter.com:tweet", + }, + }, nil +} + +type isCheckin struct { + matchEqual +} + +func newIsCheckin() keyword { + return isCheckin{"is:checkin"} +} + +func (k isCheckin) Description() string { + return "matches location check-ins (foursquare, etc)" +} + +func (k isCheckin) Predicate(ctx *context.Context, args []string) (*Constraint, error) { + return &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliNodeType", + Value: "foursquare.com:checkin", + }, + }, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/predicate_test.go b/vendor/github.com/camlistore/camlistore/pkg/search/predicate_test.go new file mode 100644 index 00000000..76425204 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/predicate_test.go @@ -0,0 +1,669 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search + +import ( + "encoding/json" + "net/http" + "reflect" + "strings" + "testing" + "time" + + "camlistore.org/pkg/context" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/types" +) + +func TestSearchHelp(t *testing.T) { + s := SearchHelp() + type help struct{ Name, Description string } + h := []help{} + err := json.Unmarshal([]byte(s), &h) + if err != nil { + t.Fatal(err) + } + count := len(keywords) + if len(h) != count { + t.Errorf("Expected %d help items, got %d", count, len(h)) + } +} + +type keywordTestcase struct { + name string + object keyword + args []string + want *Constraint + errContains string + ctx *context.Context +} + +var uitdamLC = &LocationConstraint{ + North: 52.4486802, + West: 5.0353014, + East: 5.094973299999999, + South: 52.4152441, +} + +func newGeocodeContext() *context.Context { + url := "https://maps.googleapis.com/maps/api/geocode/json?address=Uitdam&sensor=false" + transport := httputil.NewFakeTransport(map[string]func() *http.Response{url: httputil.StaticResponder(uitdamGoogle)}) + return context.New(context.WithHTTPClient(&http.Client{Transport: transport})) +} + +var uitdamGoogle = `HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 + Date: Tue, 13 May 2014 21:15:01 GMT + Expires: Wed, 14 May 2014 21:15:01 GMT + Cache-Control: public, max-age=86400 + Vary: Accept-Language + Access-Control-Allow-Origin: * + Server: mafe + X-XSS-Protection: 1; mode=block + X-Frame-Options: SAMEORIGIN + Transfer-Encoding: chunked + + +{ + "results" : [ + { + "address_components" : [ + { + "long_name" : "Uitdam", + "short_name" : "Uitdam", + "types" : [ "locality", "political" ] + }, + { + "long_name" : "Waterland", + "short_name" : "Waterland", + "types" : [ "administrative_area_level_2", "political" ] + }, + { + "long_name" : "North Holland", + "short_name" : "NH", + "types" : [ "administrative_area_level_1", "political" ] + }, + { + "long_name" : "The Netherlands", + "short_name" : "NL", + "types" : [ "country", "political" ] + }, + { + "long_name" : "1154", + "short_name" : "1154", + "types" : [ "postal_code_prefix", "postal_code" ] + } + ], + "formatted_address" : "1154 Uitdam, The Netherlands", + "geometry" : { + "bounds" : { + "northeast" : { + "lat" : 52.4486802, + "lng" : 5.094973299999999 + }, + "southwest" : { + "lat" : 52.4152441, + "lng" : 5.0353014 + } + }, + "location" : { + "lat" : 52.4210268, + "lng" : 5.0724962 + }, + "location_type" : "APPROXIMATE", + "viewport" : { + "northeast" : { + "lat" : 52.4486802, + "lng" : 5.094973299999999 + }, + "southwest" : { + "lat" : 52.4152441, + "lng" : 5.0353014 + } + } + }, + "types" : [ "locality", "political" ] + } + ], + "status" : "OK" +} +` +var testtime = time.Date(2013, time.February, 3, 0, 0, 0, 0, time.UTC) + +var keywordTests = []keywordTestcase{ + // Core predicates + { + object: newAfter(), + args: []string{"faulty"}, + errContains: "faulty", + }, + + { + object: newAfter(), + args: []string{"2013-02-03"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Time: &TimeConstraint{ + After: types.Time3339(testtime), + }, + }, + }, + }, + + { + object: newBefore(), + args: []string{"faulty"}, + errContains: "faulty", + }, + + { + object: newBefore(), + args: []string{"2013-02-03"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Time: &TimeConstraint{ + Before: types.Time3339(testtime), + }, + }, + }, + }, + + { + object: newAttribute(), + args: []string{"foo", "bar"}, + want: attrfoobarC, + }, + + { + object: newAttribute(), + args: []string{"foo", ""}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "foo", + ValueMatches: &StringConstraint{Empty: true}, + SkipHidden: true, + }, + }, + }, + + { + object: newAttribute(), + args: []string{"foo", "~bar"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "foo", + ValueMatches: &StringConstraint{ + Contains: "bar", + CaseInsensitive: true, + }, + SkipHidden: true, + }, + }, + }, + + { + object: newChildrenOf(), + args: []string{"foo"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "parent", + Any: &Constraint{ + BlobRefPrefix: "foo", + }, + }, + }, + }, + }, + + { + object: newFormat(), + args: []string{"faulty"}, + errContains: "Unknown format: faulty", + }, + + { + object: newFormat(), + args: []string{"pdf"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + MIMEType: &StringConstraint{ + Equals: "application/pdf", + }, + }, + }, + }, + }, + }, + + { + object: newTag(), + args: []string{"foo"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "tag", + Value: "foo", + SkipHidden: true, + }, + }, + }, + + { + object: newTag(), + args: []string{""}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "tag", + ValueMatches: &StringConstraint{Empty: true}, + SkipHidden: true, + }, + }, + }, + + { + object: newTitle(), + args: []string{""}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "title", + SkipHidden: true, + ValueMatches: &StringConstraint{ + CaseInsensitive: true, + }, + }}, + }, + + { + object: newTitle(), + args: []string{"foo"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "title", + SkipHidden: true, + ValueMatches: &StringConstraint{ + Contains: "foo", + CaseInsensitive: true, + }, + }, + }, + }, + + // Image predicates + { + object: newIsImage(), + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + }, + }, + }, + }, + }, + + { + object: newIsPano(), + want: ispanoC, + }, + + { + object: newIsLandscape(), + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + WHRatio: &FloatConstraint{ + Min: 1.0, + }, + }, + }, + }, + }, + }, + + { + object: newIsPortait(), + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + WHRatio: &FloatConstraint{ + Max: 1.0, + }, + }, + }, + }, + }, + }, + + { + object: newWidth(), + args: []string{""}, + errContains: "Unable to parse \"\" as range, wanted something like 480-1024, 480-, -1024 or 1024", + }, + + { + object: newWidth(), + args: []string{"100-"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Width: &IntConstraint{ + Min: 100, + }, + }, + }, + }, + }, + }, + + { + object: newWidth(), + args: []string{"0-200"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Width: &IntConstraint{ + ZeroMin: true, + Max: 200, + }, + }, + }, + }, + }, + }, + + { + object: newWidth(), + args: []string{"-200"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Width: &IntConstraint{ + Max: 200, + }, + }, + }, + }, + }, + }, + + { + object: newWidth(), + args: []string{"100-200"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Width: &IntConstraint{ + Min: 100, + Max: 200, + }, + }, + }, + }, + }, + }, + + { + object: newHeight(), + args: []string{""}, + errContains: "Unable to parse \"\" as range, wanted something like 480-1024, 480-, -1024 or 1024", + }, + + { + object: newHeight(), + args: []string{"100-200"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Height: &IntConstraint{ + Min: 100, + Max: 200, + }, + }, + }, + }, + }, + }, + + { + object: newHeight(), + args: []string{"-200"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Height: &IntConstraint{ + Max: 200, + }, + }, + }, + }, + }, + }, + + { + object: newHeight(), + args: []string{"100-"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Height: &IntConstraint{ + Min: 100, + }, + }, + }, + }, + }, + }, + + { + object: newHeight(), + args: []string{"0-200"}, + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Height: &IntConstraint{ + ZeroMin: true, + Max: 200, + }, + }, + }, + }, + }, + }, + + // Location predicates + { + object: newLocation(), + args: []string{"Uitdam"}, // Small dutch town + want: orConst(&Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + IsImage: true, + Location: uitdamLC, + }, + }, + }, + }, &Constraint{ + Permanode: &PermanodeConstraint{ + Location: uitdamLC, + }, + }), + ctx: newGeocodeContext(), + }, + + { + object: newHasLocation(), + want: hasLocationC, + }, +} + +func TestKeywords(t *testing.T) { + cj := func(c *Constraint) []byte { + v, err := json.MarshalIndent(c, "", " ") + if err != nil { + t.Fatal(err) + } + return v + } + for _, tt := range keywordTests { + got, err := tt.object.Predicate(tt.ctx, tt.args) + if err != nil { + if tt.errContains != "" && strings.Contains(err.Error(), tt.errContains) { + continue + } + t.Errorf("%v: %#v(%q) error: %v, but wanted an error containing: %v", tt.name, tt.object, tt.args, err, tt.errContains) + continue + } + if tt.errContains != "" { + t.Errorf("%v: %#v(%q) succeeded; want error containing %q", tt.name, tt.object, tt.args, tt.errContains) + continue + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("%v: %#v(%q) got:\n%s\n\nwant:%s\n", tt.name, tt.object, tt.args, cj(got), cj(tt.want)) + } + } +} + +func TestParseWHExpression(t *testing.T) { + tests := []struct { + in string + wantMin string + wantMax string + errContains string + }{ + {in: "450-470", wantMin: "450", wantMax: "470"}, + {in: "450-470+", errContains: "Unable to parse \"450-470+\" as range, wanted something like 480-1024, 480-, -1024 or 1024"}, + {in: "", errContains: "Unable to parse \"\" as range, wanted something like 480-1024, 480-, -1024 or 1024"}, + {in: "450", wantMin: "450", wantMax: "450"}, + } + + for _, tt := range tests { + gotMin, gotMax, err := parseWHExpression(tt.in) + if err != nil { + if tt.errContains != "" && strings.Contains(err.Error(), tt.errContains) { + continue + } + t.Errorf("parseWHExpression(%v) error: %v, but wanted an error containing: %v", tt.in, err, tt.errContains) + continue + } + if tt.errContains != "" { + t.Errorf("parseWHExpression(%v) succeeded; want error containing %v got: %s,%s ", tt.in, tt.errContains, gotMin, gotMax) + continue + } + if !reflect.DeepEqual(gotMin, tt.wantMin) { + t.Errorf("parseWHExpression(%s) min = %v; want %v", tt.in, gotMin, tt.wantMin) + } + if !reflect.DeepEqual(gotMax, tt.wantMax) { + t.Errorf("parseWHExpression(%s) max = %v; want %v", tt.in, gotMax, tt.wantMax) + } + } +} + +func TestMatchEqual(t *testing.T) { + me := matchEqual("foo:bar:baz") + a := atom{"foo", []string{"bar", "baz"}} + + if m, _ := me.Match(a); !m { + t.Error("Expected a match") + } + + a = atom{"foo", []string{"foo", "baz"}} + if m, _ := me.Match(a); m { + t.Error("Did not expect a match") + } +} + +func TestMatchPrefix(t *testing.T) { + mp := matchPrefix{"foo", 1} + a := atom{"foo", []string{"bar"}} + if m, err := mp.Match(a); err != nil || !m { + t.Error("Expected a match") + } + + a = atom{"foo", []string{}} + if _, err := mp.Match(a); err == nil { + t.Error("Expected an error got nil") + } + a = atom{"bar", []string{}} + if m, err := mp.Match(a); err != nil || m { + t.Error("Expected simple mismatch") + } +} + +func TestLocationConstraint(t *testing.T) { + var c LocationConstraint + if c.matchesLatLong(1, 2) { + t.Error("zero value shouldn't match") + } + c.Any = true + if !c.matchesLatLong(1, 2) { + t.Error("Any should match") + } + + c = LocationConstraint{North: 2, South: 1, West: 0, East: 2} + tests := []struct { + lat, long float64 + want bool + }{ + {1, 1, true}, + {3, 1, false}, // too north + {1, 3, false}, // too east + {1, -1, false}, // too west + {0, 1, false}, // too south + } + for _, tt := range tests { + if got := c.matchesLatLong(tt.lat, tt.long); got != tt.want { + t.Errorf("matches(%v, %v) = %v; want %v", tt.lat, tt.long, got, tt.want) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/query.go b/vendor/github.com/camlistore/camlistore/pkg/search/query.go new file mode 100644 index 00000000..2e8bc9cb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/query.go @@ -0,0 +1,1616 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "os" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/context" + "camlistore.org/pkg/index" + "camlistore.org/pkg/strutil" + "camlistore.org/pkg/types" + "camlistore.org/pkg/types/camtypes" +) + +type SortType int + +const ( + UnspecifiedSort SortType = iota + Unsorted + LastModifiedDesc + LastModifiedAsc + CreatedDesc + CreatedAsc + BlobRefAsc + maxSortType +) + +var sortName = map[SortType][]byte{ + Unsorted: []byte(`"unsorted"`), + LastModifiedDesc: []byte(`"-mod"`), + LastModifiedAsc: []byte(`"mod"`), + CreatedDesc: []byte(`"-created"`), + CreatedAsc: []byte(`"created"`), + BlobRefAsc: []byte(`"blobref"`), +} + +func (t SortType) MarshalJSON() ([]byte, error) { + v, ok := sortName[t] + if !ok { + panic("unnamed SortType " + strconv.Itoa(int(t))) + } + return v, nil +} + +func (t *SortType) UnmarshalJSON(v []byte) error { + for n, nv := range sortName { + if bytes.Equal(v, nv) { + *t = n + return nil + } + } + return fmt.Errorf("Bogus search sort type %q", v) +} + +type SearchQuery struct { + // Exactly one of Expression or Contraint must be set. + // If an Expression is set, it's compiled to a Constraint. + + // Expression is a textual search query in minimal form, + // e.g. "hawaii before:2008" or "tag:foo" or "foo" or "location:portland" + // See expr.go and expr_test.go for all the operators. + Expression string `json:"expression,omitempty"` + Constraint *Constraint `json:"constraint,omitempty"` + + // Limit is the maximum number of returned results. A negative value means no + // limit. If unspecified, a default (of 200) will be used. + Limit int `json:"limit,omitempty"` + + // Sort specifies how the results will be sorted. It defaults to CreatedDesc when the + // query is about permanodes only. + Sort SortType `json:"sort,omitempty"` + + // Around specifies that the results, after sorting, should be centered around + // this result. If Around is not found the returned results will be empty. + // If both Continue and Around are set, an error is returned. + Around blob.Ref `json:"around,omitempty"` + + // Continue specifies the opaque token (as returned by a + // SearchResult) for where to continue fetching results when + // the Limit on a previous query was interrupted. + // Continue is only valid for the same query (Expression or Constraint), + // Limit, and Sort values. + // If empty, the top-most query results are returned, as given + // by Limit and Sort. + // Continue is not compatible with the Around option. + Continue string `json:"continue,omitempty"` + + // If Describe is specified, the matched blobs are also described, + // as if the Describe.BlobRefs field was populated. + Describe *DescribeRequest `json:"describe,omitempty"` +} + +func (q *SearchQuery) URLSuffix() string { return "camli/search/query" } + +func (q *SearchQuery) fromHTTP(req *http.Request) error { + dec := json.NewDecoder(io.LimitReader(req.Body, 1<<20)) + if err := dec.Decode(q); err != nil { + return err + } + + if q.Constraint == nil && q.Expression == "" { + return errors.New("query must have at least a constraint or an expression") + } + + return nil +} + +// exprQuery optionally specifies the *SearchQuery prototype that was generated +// by parsing the search expression +func (q *SearchQuery) plannedQuery(expr *SearchQuery) *SearchQuery { + pq := new(SearchQuery) + *pq = *q + if expr != nil { + pq.Constraint = expr.Constraint + if expr.Sort != 0 { + pq.Sort = expr.Sort + } + if expr.Limit != 0 { + pq.Limit = expr.Limit + } + } + if pq.Sort == UnspecifiedSort { + if pq.Constraint.onlyMatchesPermanode() { + pq.Sort = CreatedDesc + } + } + if pq.Limit == 0 { + pq.Limit = 200 // arbitrary + } + if err := pq.addContinueConstraint(); err != nil { + log.Printf("Ignoring continue token: %v", err) + } + pq.Constraint = optimizePlan(pq.Constraint) + return pq +} + +// For permanodes, the continue token is (currently!) +// of form "pn:nnnnnnn:sha1-xxxxx" where "pn" is a +// literal, "nnnnnn" is the UnixNano of the time +// (modified or created) and "sha1-xxxxx" was the item +// seen in the final result set, used as a tie breaker +// if multiple permanodes had the same mod/created +// time. This format is NOT an API promise or standard and +// clients should not rely on it. It may change without notice +func parsePermanodeContinueToken(v string) (t time.Time, br blob.Ref, ok bool) { + if !strings.HasPrefix(v, "pn:") { + return + } + v = v[len("pn:"):] + col := strings.Index(v, ":") + if col < 0 { + return + } + nano, err := strconv.ParseUint(v[:col], 10, 64) + if err != nil { + return + } + t = time.Unix(0, int64(nano)) + br, ok = blob.Parse(v[col+1:]) + return +} + +// addContinueConstraint conditionally modifies q.Constraint to scroll +// past the results as indicated by q.Continue. +func (q *SearchQuery) addContinueConstraint() error { + cont := q.Continue + if cont == "" { + return nil + } + if q.Constraint.onlyMatchesPermanode() { + tokent, lastbr, ok := parsePermanodeContinueToken(cont) + if !ok { + return errors.New("Unexpected continue token") + } + if q.Sort == LastModifiedDesc || q.Sort == CreatedDesc { + var lastMod, lastCreated time.Time + switch q.Sort { + case LastModifiedDesc: + lastMod = tokent + case CreatedDesc: + lastCreated = tokent + } + baseConstraint := q.Constraint + q.Constraint = &Constraint{ + Logical: &LogicalConstraint{ + Op: "and", + A: &Constraint{ + Permanode: &PermanodeConstraint{ + Continue: &PermanodeContinueConstraint{ + LastCreated: lastCreated, + LastMod: lastMod, + Last: lastbr, + }, + }, + }, + B: baseConstraint, + }, + } + } + return nil + } + return errors.New("token not valid for query type") +} + +func (q *SearchQuery) checkValid(ctx *context.Context) (sq *SearchQuery, err error) { + if q.Sort >= maxSortType || q.Sort < 0 { + return nil, errors.New("invalid sort type") + } + if q.Continue != "" && q.Around.Valid() { + return nil, errors.New("Continue and Around parameters are mutually exclusive") + } + if q.Constraint == nil { + if expr := q.Expression; expr != "" { + sq, err := parseExpression(ctx, expr) + if err != nil { + return nil, fmt.Errorf("Error parsing search expression %q: %v", expr, err) + } + if err := sq.Constraint.checkValid(); err != nil { + return nil, fmt.Errorf("Internal error: parseExpression(%q) returned invalid constraint: %v", expr, err) + } + return sq, nil + } + return nil, errors.New("no search constraint or expression") + } + return nil, q.Constraint.checkValid() +} + +// SearchResult is the result of the Search method for a given SearchQuery. +type SearchResult struct { + Blobs []*SearchResultBlob `json:"blobs"` + Describe *DescribeResponse `json:"description"` + + // Continue optionally specifies the continuation token to to + // continue fetching results in this result set, if interrupted + // by a Limit. + Continue string `json:"continue,omitempty"` +} + +type SearchResultBlob struct { + Blob blob.Ref `json:"blob"` + // ... file info, permanode info, blob info ... ? +} + +func (r *SearchResultBlob) String() string { + return fmt.Sprintf("[blob: %s]", r.Blob) +} + +// Constraint specifies a blob matching constraint. +// A blob matches if it matches all non-zero fields' predicates. +// A zero constraint matches nothing. +type Constraint struct { + // If Logical is non-nil, all other fields are ignored. + Logical *LogicalConstraint `json:"logical,omitempty"` + + // Anything, if true, matches all blobs. + Anything bool `json:"anything,omitempty"` + + CamliType string `json:"camliType,omitempty"` // camliType of the JSON blob + AnyCamliType bool `json:"anyCamliType,omitempty"` // if true, any camli JSON blob matches + BlobRefPrefix string `json:"blobRefPrefix,omitempty"` + + File *FileConstraint `json:"file,omitempty"` + Dir *DirConstraint `json:"dir,omitempty"` + + Claim *ClaimConstraint `json:"claim,omitempty"` + BlobSize *IntConstraint `json:"blobSize,omitempty"` + + Permanode *PermanodeConstraint `json:"permanode,omitempty"` + + matcherOnce sync.Once + matcherFn matchFn +} + +func (c *Constraint) checkValid() error { + type checker interface { + checkValid() error + } + if c.Claim != nil { + return errors.New("TODO: implement ClaimConstraint") + } + for _, cv := range []checker{ + c.Logical, + c.File, + c.Dir, + c.BlobSize, + c.Permanode, + } { + if err := cv.checkValid(); err != nil { + return err + } + } + return nil +} + +func (c *Constraint) onlyMatchesPermanode() bool { + if c.Permanode != nil || c.CamliType == "permanode" { + return true + } + + if c.Logical != nil && c.Logical.Op == "and" { + if c.Logical.A.onlyMatchesPermanode() || c.Logical.B.onlyMatchesPermanode() { + return true + } + } + + // TODO: There are other cases we can return true here, like: + // Logical:{Op:'or', A:PermanodeConstraint{...}, B:PermanodeConstraint{...} + + return false +} + +type FileConstraint struct { + // (All non-zero fields must match) + + FileSize *IntConstraint `json:"fileSize,omitempty"` + FileName *StringConstraint `json:"fileName,omitempty"` + MIMEType *StringConstraint `json:"mimeType,omitempty"` + Time *TimeConstraint `json:"time,omitempty"` + ModTime *TimeConstraint `json:"modTime,omitempty"` + + // WholeRef if non-zero only matches if the entire checksum of the + // file (the concatenation of all its blobs) is equal to the + // provided blobref. The index may not have every file's digest for + // every known hash algorithm. + WholeRef blob.Ref `json:"wholeRef,omitempty"` + + // For images: + IsImage bool `json:"isImage,omitempty"` + EXIF *EXIFConstraint `json:"exif,omitempty"` // TODO: implement + Width *IntConstraint `json:"width,omitempty"` + Height *IntConstraint `json:"height,omitempty"` + WHRatio *FloatConstraint `json:"widthHeightRation,omitempty"` + Location *LocationConstraint `json:"location,omitempty"` + + // MediaTag is for ID3 (and similar) embedded metadata in files. + MediaTag *MediaTagConstraint `json:"mediaTag,omitempty"` +} + +type MediaTagConstraint struct { + // Tag is the tag to match. + // For ID3, this includes: title, artist, album, genre, musicbrainzalbumid, year, track, disc, mediaref, durationms. + Tag string `json:"tag"` + + String *StringConstraint `json:"string,omitempty"` + Int *IntConstraint `json:"int,omitempty"` +} + +type DirConstraint struct { + // (All non-zero fields must match) + + // TODO: implement. mostly need more things in the index. + + FileName *StringConstraint + + TopFileSize, // not recursive + TopFileCount, // not recursive + FileSize, + FileCount *IntConstraint + + // TODO: these would need thought on how to index efficiently: + // (Also: top-only variants?) + // ContainsFile *FileConstraint + // ContainsDir *DirConstraint +} + +// An IntConstraint specifies constraints on an integer. +type IntConstraint struct { + // Min and Max are both optional and inclusive bounds. + // Zero means don't check. + Min int64 `json:"min,omitempty"` + Max int64 `json:"max,omitempty"` + ZeroMin bool `json:"zeroMin,omitempty"` // if true, min is actually zero + ZeroMax bool `json:"zeroMax,omitempty"` // if true, max is actually zero +} + +func (c *IntConstraint) hasMin() bool { return c.Min != 0 || c.ZeroMin } +func (c *IntConstraint) hasMax() bool { return c.Max != 0 || c.ZeroMax } + +func (c *IntConstraint) checkValid() error { + if c == nil { + return nil + } + if c.ZeroMin && c.Min != 0 { + return errors.New("in IntConstraint, can't set both ZeroMin and Min") + } + if c.ZeroMax && c.Max != 0 { + return errors.New("in IntConstraint, can't set both ZeroMax and Max") + } + if c.hasMax() && c.hasMin() && c.Min > c.Max { + return errors.New("in IntConstraint, min is greater than max") + } + return nil +} + +func (c *IntConstraint) intMatches(v int64) bool { + if c.hasMin() && v < c.Min { + return false + } + if c.hasMax() && v > c.Max { + return false + } + return true +} + +// A FloatConstraint specifies constraints on a float. +type FloatConstraint struct { + // Min and Max are both optional and inclusive bounds. + // Zero means don't check. + Min float64 `json:"min,omitempty"` + Max float64 `json:"max,omitempty"` + ZeroMin bool `json:"zeroMin,omitempty"` // if true, min is actually zero + ZeroMax bool `json:"zeroMax,omitempty"` // if true, max is actually zero +} + +func (c *FloatConstraint) hasMin() bool { return c.Min != 0 || c.ZeroMin } +func (c *FloatConstraint) hasMax() bool { return c.Max != 0 || c.ZeroMax } + +func (c *FloatConstraint) checkValid() error { + if c == nil { + return nil + } + if c.ZeroMin && c.Min != 0 { + return errors.New("in FloatConstraint, can't set both ZeroMin and Min") + } + if c.ZeroMax && c.Max != 0 { + return errors.New("in FloatConstraint, can't set both ZeroMax and Max") + } + if c.hasMax() && c.hasMin() && c.Min > c.Max { + return errors.New("in FloatConstraint, min is greater than max") + } + return nil +} + +func (c *FloatConstraint) floatMatches(v float64) bool { + if c.hasMin() && v < c.Min { + return false + } + if c.hasMax() && v > c.Max { + return false + } + return true +} + +type EXIFConstraint struct { + // TODO. need to put this in the index probably. + // Maybe: GPS *LocationConstraint + // ISO, Aperature, Camera Make/Model, etc. +} + +type LocationConstraint struct { + // Any, if true, matches any photo with a known location. + Any bool + + // North, West, East, and South define a region in which a photo + // must be in order to match. + North float64 + West float64 + East float64 + South float64 +} + +func (c *LocationConstraint) matchesLatLong(lat, long float64) bool { + return c.Any || (c.West <= long && long <= c.East && c.South <= lat && lat <= c.North) +} + +// A StringConstraint specifies constraints on a string. +// All non-zero must match. +type StringConstraint struct { + Empty bool `json:"empty,omitempty"` // matches empty string + Equals string `json:"equals,omitempty"` + Contains string `json:"contains,omitempty"` + HasPrefix string `json:"hasPrefix,omitempty"` + HasSuffix string `json:"hasSuffix,omitempty"` + ByteLength *IntConstraint `json:"byteLength,omitempty"` // length in bytes (not chars) + CaseInsensitive bool `json:"caseInsensitive,omitempty"` + + // TODO: CharLength (assume UTF-8) +} + +// stringCompareFunc contains a function to get a value from a StringConstraint and a second function to compare it +// against the string s that's being matched. +type stringConstraintFunc struct { + v func(*StringConstraint) string + fn func(s, v string) bool +} + +// Functions to compare fields of a StringConstraint against strings in a case-sensitive manner. +var stringConstraintFuncs = []stringConstraintFunc{ + {func(c *StringConstraint) string { return c.Equals }, func(a, b string) bool { return a == b }}, + {func(c *StringConstraint) string { return c.Contains }, strings.Contains}, + {func(c *StringConstraint) string { return c.HasPrefix }, strings.HasPrefix}, + {func(c *StringConstraint) string { return c.HasSuffix }, strings.HasSuffix}, +} + +// Functions to compare fields of a StringConstraint against strings in a case-insensitive manner. +var stringConstraintFuncsFold = []stringConstraintFunc{ + {func(c *StringConstraint) string { return c.Equals }, strings.EqualFold}, + {func(c *StringConstraint) string { return c.Contains }, strutil.ContainsFold}, + {func(c *StringConstraint) string { return c.HasPrefix }, strutil.HasPrefixFold}, + {func(c *StringConstraint) string { return c.HasSuffix }, strutil.HasSuffixFold}, +} + +func (c *StringConstraint) stringMatches(s string) bool { + if c.Empty && len(s) > 0 { + return false + } + if c.ByteLength != nil && !c.ByteLength.intMatches(int64(len(s))) { + return false + } + + funcs := stringConstraintFuncs + if c.CaseInsensitive { + funcs = stringConstraintFuncsFold + } + for _, pair := range funcs { + if v := pair.v(c); v != "" && !pair.fn(s, v) { + return false + } + } + return true +} + +type TimeConstraint struct { + Before types.Time3339 `json:"before"` // < + After types.Time3339 `json:"after"` // >= + + // TODO: this won't JSON-marshal/unmarshal well. Make a time.Duration marshal type? + // Likewise with time that supports omitempty? + InLast time.Duration `json:"inLast"` // >= +} + +type ClaimConstraint struct { + SignedBy string `json:"signedBy"` // identity + SignedAfter time.Time `json:"signedAfter"` + SignedBefore time.Time `json:"signedBefore"` +} + +func (c *ClaimConstraint) checkValid() error { + return errors.New("TODO: implement blobMatches and checkValid on ClaimConstraint") +} + +type LogicalConstraint struct { + Op string `json:"op"` // "and", "or", "xor", "not" + A *Constraint `json:"a"` + B *Constraint `json:"b"` // only valid if Op != "not" +} + +// PermanodeConstraint matches permanodes. +type PermanodeConstraint struct { + // At specifies the time at which to pretend we're resolving attributes. + // Attribute claims after this point in time are ignored. + // If zero, the current time is used. + At time.Time `json:"at,omitempty"` + + // ModTime optionally matches on the last modtime of the permanode. + ModTime *TimeConstraint `json:"modTime,omitempty"` + + // Time optionally matches the permanode's time. A Permanode + // may not have a known time. If the permanode does not have a + // known time, one may be guessed if the top-level search + // parameters request so. + Time *TimeConstraint `json:"time,omitempty"` + + // Attr optionally specifies the attribute to match. + // e.g. "camliContent", "camliMember", "tag" + // This is required if any of the items below are used. + Attr string `json:"attr,omitempty"` + + // SkipHidden skips hidden or other boring files. + SkipHidden bool `json:"skipHidden,omitempty"` + + // NumValue optionally tests the number of values this + // permanode has for Attr. + NumValue *IntConstraint `json:"numValue,omitempty"` + + // ValueAll modifies the matching behavior when an attribute + // is multi-valued. By default, when ValueAll is false, only + // one value of a multi-valued attribute needs to match. If + // ValueAll is true, all attributes must match. + ValueAll bool `json:"valueAllMatch,omitempty"` + + // Value specifies an exact string to match. + // This is a convenience form for the simple case of exact + // equality. The same can be accomplished with ValueMatches. + Value string `json:"value,omitempty"` // if non-zero, absolute match + + // ValueMatches optionally specifies a StringConstraint to + // match the value against. + ValueMatches *StringConstraint `json:"valueMatches,omitempty"` + + // ValueMatchesInt optionally specifies an IntConstraint to match + // the value against. Non-integer values will not match. + ValueMatchesInt *IntConstraint `json:"valueMatchesInt,omitempty"` + + // ValueMatchesFloat optionally specifies a FloatConstraint to match + // the value against. Non-float values will not match. + ValueMatchesFloat *FloatConstraint `json:"valueMatchesFloat,omitempty"` + + // ValueInSet optionally specifies a sub-query which the value + // (which must be a blobref) must be a part of. + ValueInSet *Constraint `json:"valueInSet,omitempty"` + + // Relation optionally specifies a constraint based on relations + // to other permanodes (e.g. camliMember or camliPath sets). + // You can use it to test the properties of a parent, ancestor, + // child, or progeny. + Relation *RelationConstraint `json:"relation,omitempty"` + + // Location optionally restricts matches to permanodes having + // this location. This only affects permanodes with a known + // type to have an lat/long location. + Location *LocationConstraint `json:"location,omitempty"` + + // Continue is for internal use. + Continue *PermanodeContinueConstraint `json:"-"` + + // TODO: + // NumClaims *IntConstraint // by owner + // Owner blob.Ref // search for permanodes by an owner + + // Note: When adding a field, update hasValueConstraint. +} + +type PermanodeContinueConstraint struct { + // LastMod if non-zero is the modtime of the last item + // that was seen. One of this or LastCreated will be set. + LastMod time.Time + + // LastCreated if non-zero is the creation time of the last + // item that was seen. + LastCreated time.Time + + // Last is the last blobref that was shown at the time + // given in ModLessEqual or CreateLessEqual. + // This is used as a tie-breaker. + // If the time is equal, permanodes <= this are not matched. + // If the time is past this in the scroll position, then this + // field is ignored. + Last blob.Ref +} + +func (pcc *PermanodeContinueConstraint) checkValid() error { + if pcc.LastMod.IsZero() == pcc.LastCreated.IsZero() { + return errors.New("exactly one of PermanodeContinueConstraint LastMod or LastCreated must be defined") + } + return nil +} + +type RelationConstraint struct { + // Relation must be one of: + // * "child" + // * "parent" (immediate parent only) + // * "progeny" (any level down) + // * "ancestor" (any level up) + Relation string + + // EdgeType optionally specifies an edge type. + // By default it matches "camliMember" and "camliPath:*". + EdgeType string + + // After finding all the nodes matching the Relation and + // EdgeType, either one or all (depending on whether Any or + // All is set) must then match for the RelationConstraint + // itself to match. + // + // It is an error to set both. + Any, All *Constraint +} + +func (rc *RelationConstraint) checkValid() error { + if rc.Relation != "parent" { + return errors.New("only RelationConstraint.Relation of \"parent\" is currently supported") + } + if (rc.Any == nil) == (rc.All == nil) { + return errors.New("exactly one of RelationConstraint Any or All must be defined") + } + return nil +} + +func (rc *RelationConstraint) matchesAttr(attr string) bool { + if rc.EdgeType != "" { + return attr == rc.EdgeType + } + return attr == "camliMember" || strings.HasPrefix(attr, "camliPath:") +} + +// The PermanodeConstraint matching of RelationConstraint. +func (rc *RelationConstraint) match(s *search, pn blob.Ref, at time.Time) (ok bool, err error) { + corpus := s.h.corpus + if corpus == nil { + // TODO: care? + return false, errors.New("RelationConstraint requires an in-memory corpus") + } + + if rc.Relation != "parent" { + panic("bogus") + } + + var matcher matchFn + if rc.Any != nil { + matcher = rc.Any.matcher() + } else { + matcher = rc.All.matcher() + } + + var anyGood bool + var anyBad bool + var lastChecked blob.Ref + var permanodesChecked map[blob.Ref]bool // lazily created to optimize for common case of 1 match + corpus.ForeachClaimBackLocked(pn, at, func(cl *camtypes.Claim) bool { + if !rc.matchesAttr(cl.Attr) { + return true // skip claim + } + if lastChecked.Valid() { + if permanodesChecked == nil { + permanodesChecked = make(map[blob.Ref]bool) + } + permanodesChecked[lastChecked] = true + lastChecked = blob.Ref{} // back to zero + } + if permanodesChecked[cl.Permanode] { + return true // skip checking + } + if !corpus.PermanodeHasAttrValueLocked(cl.Permanode, at, cl.Attr, cl.Value) { + return true // claim once matched permanode, but no longer + } + + var bm camtypes.BlobMeta + bm, err = s.blobMeta(cl.Permanode) + if err != nil { + return false + } + var ok bool + ok, err = matcher(s, cl.Permanode, bm) + if err != nil { + return false + } + if ok { + anyGood = true + if rc.Any != nil { + return false // done. stop searching. + } + } else { + anyBad = true + if rc.All != nil { + return false // fail fast + } + } + lastChecked = cl.Permanode + return true + }) + if err != nil { + return false, err + } + if rc.All != nil { + return anyGood && !anyBad, nil + } + return anyGood, nil +} + +// search is the state of an in-progress search +type search struct { + h *Handler + q *SearchQuery + res *SearchResult + ctx *context.Context + + // ss is a scratch string slice to avoid allocations. + // We assume (at least so far) that only 1 goroutine is used + // for a given search, so anything can use this. + ss []string // scratch +} + +func (s *search) blobMeta(br blob.Ref) (camtypes.BlobMeta, error) { + if c := s.h.corpus; c != nil { + return c.GetBlobMetaLocked(br) + } else { + return s.h.index.GetBlobMeta(br) + } +} + +func (s *search) fileInfo(br blob.Ref) (camtypes.FileInfo, error) { + if c := s.h.corpus; c != nil { + return c.GetFileInfoLocked(br) + } else { + return s.h.index.GetFileInfo(br) + } +} + +// optimizePlan returns an optimized version of c which will hopefully +// execute faster than executing c literally. +func optimizePlan(c *Constraint) *Constraint { + // TODO: what the comment above says. + return c +} + +func (h *Handler) Query(rawq *SearchQuery) (*SearchResult, error) { + ctx := context.TODO() // TODO: set from rawq + exprResult, err := rawq.checkValid(ctx) + if err != nil { + return nil, fmt.Errorf("Invalid SearchQuery: %v", err) + } + q := rawq.plannedQuery(exprResult) + res := new(SearchResult) + s := &search{ + h: h, + q: q, + res: res, + ctx: context.TODO(), + } + defer s.ctx.Cancel() + + corpus := h.corpus + var unlockOnce sync.Once + if corpus != nil { + corpus.RLock() + defer unlockOnce.Do(corpus.RUnlock) + } + + ch := make(chan camtypes.BlobMeta, buffered) + errc := make(chan error, 1) + + cands := q.pickCandidateSource(s) + if candSourceHook != nil { + candSourceHook(cands.name) + } + + sendCtx := s.ctx.New() + defer sendCtx.Cancel() + go func() { errc <- cands.send(sendCtx, s, ch) }() + + wantAround, foundAround := false, false + if q.Around.Valid() { + wantAround = true + } + blobMatches := q.Constraint.matcher() + for meta := range ch { + match, err := blobMatches(s, meta.Ref, meta) + if err != nil { + return nil, err + } + if match { + res.Blobs = append(res.Blobs, &SearchResultBlob{ + Blob: meta.Ref, + }) + if q.Limit <= 0 || !cands.sorted { + continue + } + if !wantAround || foundAround { + if len(res.Blobs) == q.Limit { + sendCtx.Cancel() + break + } + continue + } + if q.Around == meta.Ref { + foundAround = true + if len(res.Blobs)*2 > q.Limit { + // If we've already collected more than half of the Limit when Around is found, + // we ditch the surplus from the beginning of the slice of results. + // If Limit is even, and the number of results before and after Around + // are both greater than half the limit, then there will be one more result before + // than after. + discard := len(res.Blobs) - q.Limit/2 - 1 + if discard < 0 { + discard = 0 + } + res.Blobs = res.Blobs[discard:] + } + if len(res.Blobs) == q.Limit { + sendCtx.Cancel() + break + } + continue + } + if len(res.Blobs) == q.Limit { + n := copy(res.Blobs, res.Blobs[len(res.Blobs)/2:]) + res.Blobs = res.Blobs[:n] + } + } + } + if err := <-errc; err != nil && err != context.ErrCanceled { + return nil, err + } + if q.Limit > 0 && cands.sorted && wantAround && !foundAround { + // results are ignored if Around was not found + res.Blobs = nil + } + if !cands.sorted { + switch q.Sort { + case UnspecifiedSort, Unsorted: + // Nothing to do. + case BlobRefAsc: + sort.Sort(sortSearchResultBlobs{res.Blobs, func(a, b *SearchResultBlob) bool { + return a.Blob.Less(b.Blob) + }}) + case CreatedDesc, CreatedAsc: + if corpus == nil { + return nil, errors.New("TODO: Sorting without a corpus unsupported") + } + var err error + corpus.RLock() + sort.Sort(sortSearchResultBlobs{res.Blobs, func(a, b *SearchResultBlob) bool { + if err != nil { + return false + } + ta, ok := corpus.PermanodeAnyTimeLocked(a.Blob) + if !ok { + err = fmt.Errorf("no ctime or modtime found for %v", a.Blob) + return false + } + tb, ok := corpus.PermanodeAnyTimeLocked(b.Blob) + if !ok { + err = fmt.Errorf("no ctime or modtime found for %v", b.Blob) + return false + } + if q.Sort == CreatedAsc { + return ta.Before(tb) + } + return tb.Before(ta) + }}) + corpus.RUnlock() + if err != nil { + return nil, err + } + // TODO(mpl): LastModifiedDesc, LastModifiedAsc + default: + return nil, errors.New("TODO: unsupported sort+query combination.") + } + if q.Limit > 0 && len(res.Blobs) > q.Limit { + res.Blobs = res.Blobs[:q.Limit] + } + } + if corpus != nil { + if !wantAround { + q.setResultContinue(corpus, res) + } + unlockOnce.Do(corpus.RUnlock) + } + + if q.Describe != nil { + q.Describe.BlobRef = blob.Ref{} // zero this out, if caller set it + blobs := make([]blob.Ref, 0, len(res.Blobs)) + for _, srb := range res.Blobs { + blobs = append(blobs, srb.Blob) + } + q.Describe.BlobRefs = blobs + res, err := s.h.Describe(q.Describe) + if err != nil { + return nil, err + } + s.res.Describe = res + } + return s.res, nil +} + +// setResultContinue sets res.Continue if q is suitable for having a continue token. +// The corpus is locked for reads. +func (q *SearchQuery) setResultContinue(corpus *index.Corpus, res *SearchResult) { + if !q.Constraint.onlyMatchesPermanode() { + return + } + var pnTimeFunc func(blob.Ref) (t time.Time, ok bool) + switch q.Sort { + case LastModifiedDesc: + pnTimeFunc = corpus.PermanodeModtimeLocked + case CreatedDesc: + pnTimeFunc = corpus.PermanodeAnyTimeLocked + default: + return + } + + if q.Limit <= 0 || len(res.Blobs) != q.Limit { + return + } + lastpn := res.Blobs[len(res.Blobs)-1].Blob + t, ok := pnTimeFunc(lastpn) + if !ok { + return + } + res.Continue = fmt.Sprintf("pn:%d:%v", t.UnixNano(), lastpn) +} + +const camliTypeMIME = "application/json; camliType=" + +type matchFn func(*search, blob.Ref, camtypes.BlobMeta) (bool, error) + +func alwaysMatch(*search, blob.Ref, camtypes.BlobMeta) (bool, error) { + return true, nil +} + +func neverMatch(*search, blob.Ref, camtypes.BlobMeta) (bool, error) { + return false, nil +} + +func anyCamliType(s *search, br blob.Ref, bm camtypes.BlobMeta) (bool, error) { + return bm.CamliType != "", nil +} + +// Test hook. +var candSourceHook func(string) + +type candidateSource struct { + name string + sorted bool + + // sends sends to the channel and must close it, regardless of error + // or interruption from context.Done(). + send func(*context.Context, *search, chan<- camtypes.BlobMeta) error +} + +func (q *SearchQuery) pickCandidateSource(s *search) (src candidateSource) { + c := q.Constraint + corpus := s.h.corpus + if corpus != nil { + if c.onlyMatchesPermanode() { + src.sorted = true + switch q.Sort { + case LastModifiedDesc: + src.name = "corpus_permanode_lastmod" + src.send = func(ctx *context.Context, s *search, dst chan<- camtypes.BlobMeta) error { + return corpus.EnumeratePermanodesLastModifiedLocked(ctx, dst) + } + return + case CreatedDesc: + src.name = "corpus_permanode_created" + src.send = func(ctx *context.Context, s *search, dst chan<- camtypes.BlobMeta) error { + return corpus.EnumeratePermanodesCreatedLocked(ctx, dst, true) + } + return + default: + src.sorted = false + } + } + if c.AnyCamliType || c.CamliType != "" { + camType := c.CamliType // empty means all + src.name = "corpus_blob_meta" + src.send = func(ctx *context.Context, s *search, dst chan<- camtypes.BlobMeta) error { + return corpus.EnumerateCamliBlobsLocked(ctx, camType, dst) + } + return + } + } + src.name = "index_blob_meta" + src.send = func(ctx *context.Context, s *search, dst chan<- camtypes.BlobMeta) error { + return s.h.index.EnumerateBlobMeta(ctx, dst) + } + return +} + +type allMustMatch []matchFn + +func (fns allMustMatch) blobMatches(s *search, br blob.Ref, blobMeta camtypes.BlobMeta) (bool, error) { + for _, condFn := range fns { + match, err := condFn(s, br, blobMeta) + if !match || err != nil { + return match, err + } + } + return true, nil +} + +func (c *Constraint) matcher() func(s *search, br blob.Ref, blobMeta camtypes.BlobMeta) (bool, error) { + c.matcherOnce.Do(c.initMatcherFn) + return c.matcherFn +} + +func (c *Constraint) initMatcherFn() { + c.matcherFn = c.genMatcher() +} + +func (c *Constraint) genMatcher() matchFn { + var ncond int + var cond matchFn + var conds []matchFn + addCond := func(fn matchFn) { + ncond++ + if ncond == 1 { + cond = fn + return + } else if ncond == 2 { + conds = append(conds, cond) + } + conds = append(conds, fn) + } + if c.Logical != nil { + addCond(c.Logical.matcher()) + } + if c.Anything { + addCond(alwaysMatch) + } + if c.CamliType != "" { + addCond(func(s *search, br blob.Ref, bm camtypes.BlobMeta) (bool, error) { + return bm.CamliType == c.CamliType, nil + }) + } + if c.AnyCamliType { + addCond(anyCamliType) + } + if c.Permanode != nil { + addCond(c.Permanode.blobMatches) + } + // TODO: ClaimConstraint + if c.File != nil { + addCond(c.File.blobMatches) + } + if c.Dir != nil { + addCond(c.Dir.blobMatches) + } + if bs := c.BlobSize; bs != nil { + addCond(func(s *search, br blob.Ref, bm camtypes.BlobMeta) (bool, error) { + return bs.intMatches(int64(bm.Size)), nil + }) + } + if pfx := c.BlobRefPrefix; pfx != "" { + addCond(func(s *search, br blob.Ref, meta camtypes.BlobMeta) (bool, error) { + return strings.HasPrefix(br.String(), pfx), nil + }) + } + switch ncond { + case 0: + return neverMatch + case 1: + return cond + default: + return allMustMatch(conds).blobMatches + } +} + +func (c *LogicalConstraint) checkValid() error { + if c == nil { + return nil + } + if c.A == nil { + return errors.New("In LogicalConstraint, need to set A") + } + if err := c.A.checkValid(); err != nil { + return err + } + switch c.Op { + case "and", "xor", "or": + if c.B == nil { + return errors.New("In LogicalConstraint, need both A and B set") + } + if err := c.B.checkValid(); err != nil { + return err + } + case "not": + default: + return fmt.Errorf("In LogicalConstraint, unknown operation %q", c.Op) + } + return nil +} + +func (c *LogicalConstraint) matcher() matchFn { + amatches := c.A.matcher() + var bmatches matchFn + if c.Op != "not" { + bmatches = c.B.matcher() + } + return func(s *search, br blob.Ref, bm camtypes.BlobMeta) (bool, error) { + + // Note: not using multiple goroutines here, because + // so far the *search type assumes it's + // single-threaded. (e.g. the .ss scratch type). + // Also, not using multiple goroutines means we can + // short-circuit when Op == "and" and av is false. + + av, err := amatches(s, br, bm) + if err != nil { + return false, err + } + switch c.Op { + case "not": + return !av, nil + case "and": + if !av { + // Short-circuit. + return false, nil + } + case "or": + if av { + // Short-circuit. + return true, nil + } + } + + bv, err := bmatches(s, br, bm) + if err != nil { + return false, err + } + + switch c.Op { + case "and", "or": + return bv, nil + case "xor": + return av != bv, nil + } + panic("unreachable") + } +} + +func (c *PermanodeConstraint) checkValid() error { + if c == nil { + return nil + } + if c.Attr != "" { + if c.NumValue == nil && !c.hasValueConstraint() { + return errors.New("PermanodeConstraint with Attr requires also setting NumValue or a value-matching constraint") + } + if nv := c.NumValue; nv != nil { + if nv.ZeroMin { + return errors.New("NumValue with ZeroMin makes no sense; matches everything") + } + if nv.ZeroMax && c.hasValueConstraint() { + return errors.New("NumValue with ZeroMax makes no sense in conjunction with a value-matching constraint; matches nothing") + } + if nv.Min < 0 || nv.Max < 0 { + return errors.New("NumValue with negative Min or Max makes no sense") + } + } + } + if rc := c.Relation; rc != nil { + if err := rc.checkValid(); err != nil { + return err + } + } + if pcc := c.Continue; pcc != nil { + if err := pcc.checkValid(); err != nil { + return err + } + } + return nil +} + +var numPermanodeFields = reflect.TypeOf(PermanodeConstraint{}).NumField() + +// hasValueConstraint returns true if one or more constraints that check an attribute's value are set. +func (c *PermanodeConstraint) hasValueConstraint() bool { + // If a field has been added or removed, update this after adding the new field to the return statement if necessary. + const expectedFields = 15 + if numPermanodeFields != expectedFields { + panic(fmt.Sprintf("PermanodeConstraint field count changed (now %v rather than %v)", numPermanodeFields, expectedFields)) + } + return c.Value != "" || + c.ValueMatches != nil || + c.ValueMatchesInt != nil || + c.ValueMatchesFloat != nil || + c.ValueInSet != nil +} + +func (c *PermanodeConstraint) blobMatches(s *search, br blob.Ref, bm camtypes.BlobMeta) (ok bool, err error) { + if bm.CamliType != "permanode" { + return false, nil + } + corpus := s.h.corpus + + var dp *DescribedPermanode + if corpus == nil { + dr, err := s.h.Describe(&DescribeRequest{BlobRef: br}) + if err != nil { + return false, err + } + db := dr.Meta[br.String()] + if db == nil || db.Permanode == nil { + return false, nil + } + dp = db.Permanode + } + + if c.Attr != "" { + if !c.At.IsZero() && corpus == nil { + panic("PermanodeConstraint.At not supported without an in-memory corpus") + } + var vals []string + if corpus == nil { + vals = dp.Attr[c.Attr] + } else { + s.ss = corpus.AppendPermanodeAttrValuesLocked( + s.ss[:0], br, c.Attr, c.At, s.h.owner) + vals = s.ss + } + ok, err := c.permanodeMatchesAttrVals(s, vals) + if !ok || err != nil { + return false, err + } + } + + if c.SkipHidden && corpus != nil { + defVis := corpus.PermanodeAttrValueLocked(br, "camliDefVis", c.At, s.h.owner) + if defVis == "hide" { + return false, nil + } + nodeType := corpus.PermanodeAttrValueLocked(br, "camliNodeType", c.At, s.h.owner) + if nodeType == "foursquare.com:venue" { + // TODO: temporary. remove this, or change + // when/where (time) we show these. But these + // are flooding my results and I'm about to + // demo this. + return false, nil + } + } + + if c.ModTime != nil { + if corpus != nil { + mt, ok := corpus.PermanodeModtimeLocked(br) + if !ok || !c.ModTime.timeMatches(mt) { + return false, nil + } + } else if !c.ModTime.timeMatches(dp.ModTime) { + return false, nil + } + } + + if c.Time != nil { + if corpus != nil { + t, ok := corpus.PermanodeAnyTimeLocked(br) + if !ok || !c.Time.timeMatches(t) { + return false, nil + } + } else { + panic("TODO: not yet supported") + } + } + + if rc := c.Relation; rc != nil { + ok, err := rc.match(s, br, c.At) + if !ok || err != nil { + return ok, err + } + } + + if c.Location != nil { + if corpus == nil { + return false, nil + } + lat, long, ok := corpus.PermanodeLatLongLocked(br, c.At) + if !ok || !c.Location.matchesLatLong(lat, long) { + return false, nil + } + } + + if cc := c.Continue; cc != nil { + if corpus == nil { + // Requires an in-memory index for infinite + // scroll. At least for now. + return false, nil + } + var pnTime time.Time + var ok bool + switch { + case !cc.LastMod.IsZero(): + pnTime, ok = corpus.PermanodeModtimeLocked(br) + if !ok || pnTime.After(cc.LastMod) { + return false, nil + } + case !cc.LastCreated.IsZero(): + pnTime, ok = corpus.PermanodeAnyTimeLocked(br) + if !ok || pnTime.After(cc.LastCreated) { + return false, nil + } + default: + panic("Continue constraint without a LastMod or a LastCreated") + } + // Blobs are sorted by modtime, and then by + // blobref, and then reversed overall. From + // top of page, imagining this scenario, where + // the user requested a page size Limit of 4: + // mod5, sha1-25 + // mod4, sha1-72 + // mod3, sha1-cc + // mod3, sha1-bb <--- last seen item, continue = "pn:mod3:sha1-bb" + // mod3, sha1-aa <-- and we want this one next. + // In the case above, we'll see all of cc, bb, and cc for mod3. + if (pnTime.Equal(cc.LastMod) || pnTime.Equal(cc.LastCreated)) && !br.Less(cc.Last) { + return false, nil + } + } + return true, nil +} + +// permanodeMatchesAttrVals checks that the values in vals - all of them, if c.ValueAll is set - +// match the values for c.Attr. +// vals are the current permanode values of c.Attr. +func (c *PermanodeConstraint) permanodeMatchesAttrVals(s *search, vals []string) (bool, error) { + if c.NumValue != nil && !c.NumValue.intMatches(int64(len(vals))) { + return false, nil + } + if c.hasValueConstraint() { + nmatch := 0 + for _, val := range vals { + match, err := c.permanodeMatchesAttrVal(s, val) + if err != nil { + return false, err + } + if match { + nmatch++ + } + } + if nmatch == 0 { + return false, nil + } + if c.ValueAll { + return nmatch == len(vals), nil + } + } + return true, nil +} + +func (c *PermanodeConstraint) permanodeMatchesAttrVal(s *search, val string) (bool, error) { + if c.Value != "" && c.Value != val { + return false, nil + } + if c.ValueMatches != nil && !c.ValueMatches.stringMatches(val) { + return false, nil + } + if c.ValueMatchesInt != nil { + if i, err := strconv.ParseInt(val, 10, 64); err != nil || !c.ValueMatchesInt.intMatches(i) { + return false, nil + } + } + if c.ValueMatchesFloat != nil { + if f, err := strconv.ParseFloat(val, 64); err != nil || !c.ValueMatchesFloat.floatMatches(f) { + return false, nil + } + } + if subc := c.ValueInSet; subc != nil { + br, ok := blob.Parse(val) // TODO: use corpus's parse, or keep this as blob.Ref in corpus attr + if !ok { + return false, nil + } + meta, err := s.blobMeta(br) + if err == os.ErrNotExist { + return false, nil + } + if err != nil { + return false, err + } + return subc.matcher()(s, br, meta) + } + return true, nil +} + +func (c *FileConstraint) checkValid() error { + return nil +} + +func (c *FileConstraint) blobMatches(s *search, br blob.Ref, bm camtypes.BlobMeta) (bool, error) { + if bm.CamliType != "file" { + return false, nil + } + fi, err := s.fileInfo(br) + if err == os.ErrNotExist { + return false, nil + } + if err != nil { + return false, err + } + if fs := c.FileSize; fs != nil && !fs.intMatches(fi.Size) { + return false, nil + } + if c.IsImage && !strings.HasPrefix(fi.MIMEType, "image/") { + return false, nil + } + if sc := c.FileName; sc != nil && !sc.stringMatches(fi.FileName) { + return false, nil + } + if sc := c.MIMEType; sc != nil && !sc.stringMatches(fi.MIMEType) { + return false, nil + } + if tc := c.Time; tc != nil { + if fi.Time == nil || !tc.timeMatches(fi.Time.Time()) { + return false, nil + } + } + if tc := c.ModTime; tc != nil { + if fi.ModTime == nil || !tc.timeMatches(fi.ModTime.Time()) { + return false, nil + } + } + corpus := s.h.corpus + if c.WholeRef.Valid() { + if corpus == nil { + return false, nil + } + wholeRef, ok := corpus.GetWholeRefLocked(br) + if !ok || wholeRef != c.WholeRef { + return false, nil + } + } + var width, height int64 + if c.Width != nil || c.Height != nil || c.WHRatio != nil { + if corpus == nil { + return false, nil + } + imageInfo, err := corpus.GetImageInfoLocked(br) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + width = int64(imageInfo.Width) + height = int64(imageInfo.Height) + } + if c.Width != nil && !c.Width.intMatches(width) { + return false, nil + } + if c.Height != nil && !c.Height.intMatches(height) { + return false, nil + } + if c.WHRatio != nil && !c.WHRatio.floatMatches(float64(width)/float64(height)) { + return false, nil + } + if c.Location != nil { + if corpus == nil { + return false, nil + } + lat, long, ok := corpus.FileLatLongLocked(br) + if ok && c.Location.Any { + // Pass. + } else if !ok || !c.Location.matchesLatLong(lat, long) { + return false, nil + } + } + if mt := c.MediaTag; mt != nil { + if corpus == nil { + return false, nil + } + var tagValue string + if mediaTags, err := corpus.GetMediaTagsLocked(br); err == nil && mt.Tag != "" { + tagValue = mediaTags[mt.Tag] + } + if mt.Int != nil { + if i, err := strconv.ParseInt(tagValue, 10, 64); err != nil || !mt.Int.intMatches(i) { + return false, nil + } + } + if mt.String != nil && !mt.String.stringMatches(tagValue) { + return false, nil + } + } + // TOOD: EXIF timeconstraint + return true, nil +} + +func (c *TimeConstraint) timeMatches(t time.Time) bool { + if t.IsZero() { + return false + } + if !c.Before.IsZero() { + if !t.Before(time.Time(c.Before)) { + return false + } + } + after := time.Time(c.After) + if after.IsZero() && c.InLast > 0 { + after = time.Now().Add(-c.InLast) + } + if !after.IsZero() { + if !(t.Equal(after) || t.After(after)) { // after is >= + return false + } + } + return true +} + +func (c *DirConstraint) checkValid() error { + return nil +} + +func (c *DirConstraint) blobMatches(s *search, br blob.Ref, bm camtypes.BlobMeta) (bool, error) { + if bm.CamliType != "directory" { + return false, nil + } + + // TODO: implement + panic("TODO: implement DirConstraint.blobMatches") +} + +type sortSearchResultBlobs struct { + s []*SearchResultBlob + less func(a, b *SearchResultBlob) bool +} + +func (ss sortSearchResultBlobs) Len() int { return len(ss.s) } +func (ss sortSearchResultBlobs) Swap(i, j int) { ss.s[i], ss.s[j] = ss.s[j], ss.s[i] } +func (ss sortSearchResultBlobs) Less(i, j int) bool { return ss.less(ss.s[i], ss.s[j]) } diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/query_test.go b/vendor/github.com/camlistore/camlistore/pkg/search/query_test.go new file mode 100644 index 00000000..89809458 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/query_test.go @@ -0,0 +1,1382 @@ +package search_test + +import ( + "encoding/json" + "flag" + "fmt" + "reflect" + "sort" + "strings" + "testing" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/index" + "camlistore.org/pkg/index/indextest" + . "camlistore.org/pkg/search" + "camlistore.org/pkg/test" + "camlistore.org/pkg/types" +) + +// indexType is one of the three ways we test the query handler code. +type indexType int + +var queryType = flag.String("querytype", "", "Empty for all query types, else 'classic', 'scan', or 'build'") + +const ( + indexClassic indexType = iota // sorted key/value pairs from index.Storage + indexCorpusScan // *Corpus scanned from key/value pairs on start + indexCorpusBuild // empty *Corpus, built iteratively as blob received. +) + +var ( + allIndexTypes = []indexType{indexClassic, indexCorpusScan, indexCorpusBuild} + memIndexTypes = []indexType{indexCorpusScan, indexCorpusBuild} + corpusTypeOnly = []indexType{indexCorpusScan} +) + +func (i indexType) String() string { + switch i { + case indexClassic: + return "classic" + case indexCorpusScan: + return "scan" + case indexCorpusBuild: + return "build" + default: + return fmt.Sprintf("unknown-index-type-%d", i) + } +} + +type queryTest struct { + t testing.TB + id *indextest.IndexDeps + itype indexType + + Handler func() *Handler +} + +func querySetup(t testing.TB) (*indextest.IndexDeps, *Handler) { + idx := index.NewMemoryIndex() // string key-value pairs in memory, as if they were on disk + id := indextest.NewIndexDeps(idx) + id.Fataler = t + h := NewHandler(idx, id.SignerBlobRef) + return id, h +} + +func testQuery(t testing.TB, fn func(*queryTest)) { + testQueryTypes(t, allIndexTypes, fn) +} + +func testQueryTypes(t testing.TB, types []indexType, fn func(*queryTest)) { + defer test.TLog(t)() + for _, it := range types { + if *queryType == "" || *queryType == it.String() { + t.Logf("Testing: --querytype=%s ...", it) + testQueryType(t, fn, it) + } + } +} + +func testQueryType(t testing.TB, fn func(*queryTest), itype indexType) { + defer index.SetVerboseCorpusLogging(true) + index.SetVerboseCorpusLogging(false) + + idx := index.NewMemoryIndex() // string key-value pairs in memory, as if they were on disk + var err error + var corpus *index.Corpus + if itype == indexCorpusBuild { + if corpus, err = idx.KeepInMemory(); err != nil { + t.Fatal(err) + } + } + qt := &queryTest{ + t: t, + id: indextest.NewIndexDeps(idx), + itype: itype, + } + qt.id.Fataler = t + qt.Handler = func() *Handler { + h := NewHandler(idx, qt.id.SignerBlobRef) + if itype == indexCorpusScan { + if corpus, err = idx.KeepInMemory(); err != nil { + t.Fatal(err) + } + idx.PreventStorageAccessForTesting() + } + if corpus != nil { + h.SetCorpus(corpus) + } + return h + } + fn(qt) +} + +func dumpRes(t *testing.T, res *SearchResult) { + t.Logf("Got: %#v", res) + for i, got := range res.Blobs { + t.Logf(" %d. %s", i, got) + } +} + +func (qt *queryTest) wantRes(req *SearchQuery, wanted ...blob.Ref) { + if qt.itype == indexClassic { + req.Sort = Unsorted + } + res, err := qt.Handler().Query(req) + if err != nil { + qt.t.Fatal(err) + } + + need := make(map[blob.Ref]bool) + for _, br := range wanted { + need[br] = true + } + for _, bi := range res.Blobs { + if !need[bi.Blob] { + qt.t.Errorf("unexpected search result: %v", bi.Blob) + } else { + delete(need, bi.Blob) + } + } + for br := range need { + qt.t.Errorf("missing from search result: %v", br) + } +} + +func TestQuery(t *testing.T) { + testQuery(t, func(qt *queryTest) { + fileRef, wholeRef := qt.id.UploadFile("file.txt", "the content", time.Unix(1382073153, 0)) + + sq := &SearchQuery{ + Constraint: &Constraint{ + Anything: true, + }, + Limit: 0, + Sort: UnspecifiedSort, + } + qt.wantRes(sq, fileRef, wholeRef) + }) +} + +func TestQueryCamliType(t *testing.T) { + testQuery(t, func(qt *queryTest) { + fileRef, _ := qt.id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0)) + sq := &SearchQuery{ + Constraint: &Constraint{ + CamliType: "file", + }, + } + qt.wantRes(sq, fileRef) + }) +} + +func TestQueryAnyCamliType(t *testing.T) { + testQuery(t, func(qt *queryTest) { + fileRef, _ := qt.id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0)) + + sq := &SearchQuery{ + Constraint: &Constraint{ + AnyCamliType: true, + }, + } + qt.wantRes(sq, fileRef) + }) +} + +func TestQueryBlobSize(t *testing.T) { + testQuery(t, func(qt *queryTest) { + _, smallFileRef := qt.id.UploadFile("file.txt", strings.Repeat("x", 5<<10), time.Unix(1382073153, 0)) + qt.id.UploadFile("file.txt", strings.Repeat("x", 20<<10), time.Unix(1382073153, 0)) + + sq := &SearchQuery{ + Constraint: &Constraint{ + BlobSize: &IntConstraint{ + Min: 4 << 10, + Max: 6 << 10, + }, + }, + } + qt.wantRes(sq, smallFileRef) + }) +} + +func TestQueryBlobRefPrefix(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + // foo is 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33 + id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0)) + // "bar.." is 08ef767ba2c93f8f40902118fa5260a65a2a4975 + id.UploadFile("file.txt", "bar..", time.Unix(1382073153, 0)) + + sq := &SearchQuery{ + Constraint: &Constraint{ + BlobRefPrefix: "sha1-0", + }, + } + sres, err := qt.Handler().Query(sq) + if err != nil { + t.Fatal(err) + } + if len(sres.Blobs) < 2 { + t.Errorf("expected at least 2 matches; got %d", len(sres.Blobs)) + } + for _, res := range sres.Blobs { + brStr := res.Blob.String() + if !strings.HasPrefix(brStr, "sha1-0") { + t.Errorf("matched blob %s didn't begin with sha1-0", brStr) + } + } + }) +} + +func TestQueryTwoConstraints(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + id.UploadString("a") // 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 + b := id.UploadString("b") // e9d71f5ee7c92d6dc9e92ffdad17b8bd49418f98 + id.UploadString("c4") // e4666a670f042877c67a84473a71675ee0950a08 + + sq := &SearchQuery{ + Constraint: &Constraint{ + BlobRefPrefix: "sha1-e", // matches b and c4 + BlobSize: &IntConstraint{ // matches a and b + Min: 1, + Max: 1, + }, + }, + } + qt.wantRes(sq, b) + }) +} + +func TestQueryLogicalOr(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + // foo is 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33 + _, foo := id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0)) + // "bar.." is 08ef767ba2c93f8f40902118fa5260a65a2a4975 + _, bar := id.UploadFile("file.txt", "bar..", time.Unix(1382073153, 0)) + + sq := &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "or", + A: &Constraint{ + BlobRefPrefix: "sha1-0beec7b5ea3f0fdbc95d0dd", + }, + B: &Constraint{ + BlobRefPrefix: "sha1-08ef767ba2c93f8f40", + }, + }, + }, + } + qt.wantRes(sq, foo, bar) + }) +} + +func TestQueryLogicalAnd(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + // foo is 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33 + _, foo := id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0)) + // "bar.." is 08ef767ba2c93f8f40902118fa5260a65a2a4975 + id.UploadFile("file.txt", "bar..", time.Unix(1382073153, 0)) + + sq := &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "and", + A: &Constraint{ + BlobRefPrefix: "sha1-0", + }, + B: &Constraint{ + BlobSize: &IntConstraint{ + Max: int64(len("foo")), // excludes "bar.." + }, + }, + }, + }, + } + qt.wantRes(sq, foo) + }) +} + +func TestQueryLogicalXor(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + // foo is 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33 + _, foo := id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0)) + // "bar.." is 08ef767ba2c93f8f40902118fa5260a65a2a4975 + id.UploadFile("file.txt", "bar..", time.Unix(1382073153, 0)) + + sq := &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "xor", + A: &Constraint{ + BlobRefPrefix: "sha1-0", + }, + B: &Constraint{ + BlobRefPrefix: "sha1-08ef767ba2c93f8f40", + }, + }, + }, + } + qt.wantRes(sq, foo) + }) +} + +func TestQueryLogicalNot(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + // foo is 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33 + _, foo := id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0)) + // "bar.." is 08ef767ba2c93f8f40902118fa5260a65a2a4975 + _, bar := id.UploadFile("file.txt", "bar..", time.Unix(1382073153, 0)) + + sq := &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "not", + A: &Constraint{ + CamliType: "file", + }, + }, + }, + } + qt.wantRes(sq, foo, bar) + }) +} + +func TestQueryPermanodeAttrExact(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + p1 := id.NewPlannedPermanode("1") + p2 := id.NewPlannedPermanode("2") + id.SetAttribute(p1, "someAttr", "value1") + id.SetAttribute(p2, "someAttr", "value2") + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "someAttr", + Value: "value1", + }, + }, + } + qt.wantRes(sq, p1) + }) +} + +func TestQueryPermanodeAttrMatches(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + p1 := id.NewPlannedPermanode("1") + p2 := id.NewPlannedPermanode("2") + p3 := id.NewPlannedPermanode("3") + id.SetAttribute(p1, "someAttr", "value1") + id.SetAttribute(p2, "someAttr", "value2") + id.SetAttribute(p3, "someAttr", "NOT starting with value") + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "someAttr", + ValueMatches: &StringConstraint{ + HasPrefix: "value", + }, + }, + }, + } + qt.wantRes(sq, p1, p2) + }) +} + +func TestQueryPermanodeAttrNumValue(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + // TODO(bradfitz): if we set an empty attribute value here and try to search + // by NumValue IntConstraint Min = 1, it fails only in classic (no corpus) mode. + // Something there must be skipping empty values. + p1 := id.NewPlannedPermanode("1") + id.AddAttribute(p1, "x", "1") + id.AddAttribute(p1, "x", "2") + p2 := id.NewPlannedPermanode("2") + id.AddAttribute(p2, "x", "1") + id.AddAttribute(p2, "x", "2") + id.AddAttribute(p2, "x", "3") + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "x", + NumValue: &IntConstraint{ + Min: 3, + }, + }, + }, + } + qt.wantRes(sq, p2) + }) +} + +// Tests that NumValue queries with ZeroMax return permanodes without any values. +func TestQueryPermanodeAttrNumValueZeroMax(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + p1 := id.NewPlannedPermanode("1") + id.AddAttribute(p1, "x", "1") + p2 := id.NewPlannedPermanode("2") + id.AddAttribute(p2, "y", "1") // Permanodes without any attributes are ignored. + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "x", + NumValue: &IntConstraint{ + ZeroMax: true, + }, + }, + }, + } + qt.wantRes(sq, p2) + }) +} + +// find a permanode (p2) that has a property being a blobref pointing +// to a sub-query +func TestQueryPermanodeAttrValueInSet(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + p1 := id.NewPlannedPermanode("1") + id.SetAttribute(p1, "bar", "baz") + p2 := id.NewPlannedPermanode("2") + id.SetAttribute(p2, "foo", p1.String()) + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "foo", + ValueInSet: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "bar", + Value: "baz", + }, + }, + }, + }, + } + qt.wantRes(sq, p2) + }) +} + +// Tests PermanodeConstraint.ValueMatchesInt. +func TestQueryPermanodeValueMatchesInt(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + p1 := id.NewPlannedPermanode("1") + p2 := id.NewPlannedPermanode("2") + p3 := id.NewPlannedPermanode("3") + p4 := id.NewPlannedPermanode("4") + p5 := id.NewPlannedPermanode("5") + id.SetAttribute(p1, "x", "-5") + id.SetAttribute(p2, "x", "0") + id.SetAttribute(p3, "x", "2") + id.SetAttribute(p4, "x", "10.0") + id.SetAttribute(p5, "x", "abc") + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "x", + ValueMatchesInt: &IntConstraint{ + Min: -2, + }, + }, + }, + } + qt.wantRes(sq, p2, p3) + }) +} + +// Tests PermanodeConstraint.ValueMatchesFloat. +func TestQueryPermanodeValueMatchesFloat(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + p1 := id.NewPlannedPermanode("1") + p2 := id.NewPlannedPermanode("2") + p3 := id.NewPlannedPermanode("3") + p4 := id.NewPlannedPermanode("4") + id.SetAttribute(p1, "x", "2.5") + id.SetAttribute(p2, "x", "5.7") + id.SetAttribute(p3, "x", "10") + id.SetAttribute(p4, "x", "abc") + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "x", + ValueMatchesFloat: &FloatConstraint{ + Max: 6.0, + }, + }, + }, + } + qt.wantRes(sq, p1, p2) + }) +} + +// find permanodes matching a certain file query +func TestQueryFileConstraint(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + fileRef, _ := id.UploadFile("some-stuff.txt", "hello", time.Unix(123, 0)) + qt.t.Logf("fileRef = %q", fileRef) + p1 := id.NewPlannedPermanode("1") + id.SetAttribute(p1, "camliContent", fileRef.String()) + + fileRef2, _ := id.UploadFile("other-file", "hellooooo", time.Unix(456, 0)) + qt.t.Logf("fileRef2 = %q", fileRef2) + p2 := id.NewPlannedPermanode("2") + id.SetAttribute(p2, "camliContent", fileRef2.String()) + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + FileName: &StringConstraint{ + Contains: "-stuff", + }, + FileSize: &IntConstraint{ + Max: 5, + }, + }, + }, + }, + }, + } + qt.wantRes(sq, p1) + }) +} + +func TestQueryFileConstraint_WholeRef(t *testing.T) { + testQueryTypes(t, memIndexTypes, func(qt *queryTest) { + id := qt.id + fileRef, _ := id.UploadFile("some-stuff.txt", "hello", time.Unix(123, 0)) + qt.t.Logf("fileRef = %q", fileRef) + p1 := id.NewPlannedPermanode("1") + id.SetAttribute(p1, "camliContent", fileRef.String()) + + fileRef2, _ := id.UploadFile("other-file", "hellooooo", time.Unix(456, 0)) + qt.t.Logf("fileRef2 = %q", fileRef2) + p2 := id.NewPlannedPermanode("2") + id.SetAttribute(p2, "camliContent", fileRef2.String()) + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "camliContent", + ValueInSet: &Constraint{ + File: &FileConstraint{ + WholeRef: blob.SHA1FromString("hello"), + }, + }, + }, + }, + } + qt.wantRes(sq, p1) + }) +} + +func TestQueryPermanodeModtime(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + // indextest advances time one second per operation: + p1 := id.NewPlannedPermanode("1") + p2 := id.NewPlannedPermanode("2") + p3 := id.NewPlannedPermanode("3") + id.SetAttribute(p1, "someAttr", "value1") // 2011-11-28 01:32:37.000123456 +0000 UTC 1322443957 + id.SetAttribute(p2, "someAttr", "value2") // 2011-11-28 01:32:38.000123456 +0000 UTC 1322443958 + id.SetAttribute(p3, "someAttr", "value3") // 2011-11-28 01:32:39.000123456 +0000 UTC 1322443959 + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + ModTime: &TimeConstraint{ + After: types.Time3339(time.Unix(1322443957, 456789)), + Before: types.Time3339(time.Unix(1322443959, 0)), + }, + }, + }, + } + qt.wantRes(sq, p2) + }) +} + +// This really belongs in pkg/index for the index-vs-corpus tests, but +// it's easier here for now. +// TODO: make all the indextest/tests.go +// also test the three memory build modes that testQuery does. +func TestDecodeFileInfo(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + fileRef, wholeRef := id.UploadFile("file.gif", "GIF87afoo", time.Unix(456, 0)) + res, err := qt.Handler().Describe(&DescribeRequest{ + BlobRef: fileRef, + }) + if err != nil { + qt.t.Error(err) + return + } + db := res.Meta[fileRef.String()] + if db == nil { + qt.t.Error("DescribedBlob missing") + return + } + if db.File == nil { + qt.t.Error("DescribedBlob.File is nil") + return + } + if db.File.MIMEType != "image/gif" { + qt.t.Errorf("DescribedBlob.File = %+v; mime type is not image/gif", db.File) + return + } + if db.File.WholeRef != wholeRef { + qt.t.Errorf("DescribedBlob.WholeRef: got %v, wanted %v", wholeRef, db.File.WholeRef) + return + } + }) +} + +func TestQueryRecentPermanodes_UnspecifiedSort(t *testing.T) { + testQueryRecentPermanodes(t, UnspecifiedSort, "corpus_permanode_created") +} + +func TestQueryRecentPermanodes_LastModifiedDesc(t *testing.T) { + testQueryRecentPermanodes(t, LastModifiedDesc, "corpus_permanode_lastmod") +} + +func TestQueryRecentPermanodes_CreatedDesc(t *testing.T) { + testQueryRecentPermanodes(t, CreatedDesc, "corpus_permanode_created") +} + +func testQueryRecentPermanodes(t *testing.T, sortType SortType, source string) { + testQueryTypes(t, memIndexTypes, func(qt *queryTest) { + id := qt.id + + p1 := id.NewPlannedPermanode("1") + id.SetAttribute(p1, "foo", "p1") + p2 := id.NewPlannedPermanode("2") + id.SetAttribute(p2, "foo", "p2") + p3 := id.NewPlannedPermanode("3") + id.SetAttribute(p3, "foo", "p3") + + var usedSource string + ExportSetCandidateSourceHook(func(s string) { + usedSource = s + }) + + req := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{}, + }, + Limit: 2, + Sort: sortType, + Describe: &DescribeRequest{}, + } + handler := qt.Handler() + res, err := handler.Query(req) + if err != nil { + qt.t.Fatal(err) + } + if usedSource != source { + t.Errorf("used candidate source strategy %q; want %v", usedSource, source) + } + wantBlobs := []*SearchResultBlob{ + {Blob: p3}, + {Blob: p2}, + } + if !reflect.DeepEqual(res.Blobs, wantBlobs) { + gotj, wantj := prettyJSON(res.Blobs), prettyJSON(wantBlobs) + t.Errorf("Got blobs:\n%s\nWant:\n%s\n", gotj, wantj) + } + if got := len(res.Describe.Meta); got != 2 { + t.Errorf("got %d described blobs; want 2", got) + } + + // And test whether continue (for infinite scroll) works: + { + if got, want := res.Continue, "pn:1322443958000123456:sha1-fbb5be10fcb4c88d32cfdddb20a7b8d13e9ba284"; got != want { + t.Fatalf("Continue token = %q; want %q", got, want) + } + req := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{}, + }, + Limit: 2, + Sort: sortType, + Continue: res.Continue, + } + res, err := handler.Query(req) + if err != nil { + qt.t.Fatal(err) + } + wantBlobs := []*SearchResultBlob{{Blob: p1}} + if !reflect.DeepEqual(res.Blobs, wantBlobs) { + gotj, wantj := prettyJSON(res.Blobs), prettyJSON(wantBlobs) + t.Errorf("After scroll, got blobs:\n%s\nWant:\n%s\n", gotj, wantj) + } + } + }) +} + +func TestQueryRecentPermanodes_Continue_UnspecifiedSort(t *testing.T) { + testQueryRecentPermanodes_Continue(t, UnspecifiedSort) +} + +func TestQueryRecentPermanodes_Continue_LastModifiedDesc(t *testing.T) { + testQueryRecentPermanodes_Continue(t, LastModifiedDesc) +} + +func TestQueryRecentPermanodes_Continue_CreatedDesc(t *testing.T) { + testQueryRecentPermanodes_Continue(t, CreatedDesc) +} + +// Tests the continue token on recent permanodes, notably when the +// page limit truncates in the middle of a bunch of permanodes with the +// same modtime. +func testQueryRecentPermanodes_Continue(t *testing.T, sortType SortType) { + testQueryTypes(t, memIndexTypes, func(qt *queryTest) { + id := qt.id + + var blobs []blob.Ref + for i := 1; i <= 4; i++ { + pn := id.NewPlannedPermanode(fmt.Sprint(i)) + blobs = append(blobs, pn) + t.Logf("permanode %d is %v", i, pn) + id.SetAttribute_NoTimeMove(pn, "foo", "bar") + } + sort.Sort(blob.ByRef(blobs)) + for i, br := range blobs { + t.Logf("Sorted %d = %v", i, br) + } + handler := qt.Handler() + + contToken := "" + tests := [][]blob.Ref{ + []blob.Ref{blobs[3], blobs[2]}, + []blob.Ref{blobs[1], blobs[0]}, + []blob.Ref{}, + } + + for i, wantBlobs := range tests { + req := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{}, + }, + Limit: 2, + Sort: sortType, + Continue: contToken, + } + res, err := handler.Query(req) + if err != nil { + qt.t.Fatalf("Error on query %d: %v", i+1, err) + } + t.Logf("Query %d/%d: continue = %q", i+1, len(tests), res.Continue) + for i, sb := range res.Blobs { + t.Logf(" res[%d]: %v", i, sb.Blob) + } + + var want []*SearchResultBlob + for _, br := range wantBlobs { + want = append(want, &SearchResultBlob{Blob: br}) + } + if !reflect.DeepEqual(res.Blobs, want) { + gotj, wantj := prettyJSON(res.Blobs), prettyJSON(want) + t.Fatalf("Query %d: Got blobs:\n%s\nWant:\n%s\n", i+1, gotj, wantj) + } + contToken = res.Continue + haveToken := contToken != "" + wantHaveToken := (i + 1) < len(tests) + if haveToken != wantHaveToken { + t.Fatalf("Query %d: token = %q; want token = %v", i+1, contToken, wantHaveToken) + } + } + }) +} + +func TestQueryRecentPermanodes_ContinueEndMidPage_UnspecifiedSort(t *testing.T) { + testQueryRecentPermanodes_ContinueEndMidPage(t, UnspecifiedSort) +} + +func TestQueryRecentPermanodes_ContinueEndMidPage_LastModifiedDesc(t *testing.T) { + testQueryRecentPermanodes_ContinueEndMidPage(t, LastModifiedDesc) +} + +func TestQueryRecentPermanodes_ContinueEndMidPage_CreatedDesc(t *testing.T) { + testQueryRecentPermanodes_ContinueEndMidPage(t, CreatedDesc) +} + +// Tests continue token hitting the end mid-page. +func testQueryRecentPermanodes_ContinueEndMidPage(t *testing.T, sortType SortType) { + testQueryTypes(t, memIndexTypes, func(qt *queryTest) { + id := qt.id + + var blobs []blob.Ref + for i := 1; i <= 3; i++ { + pn := id.NewPlannedPermanode(fmt.Sprint(i)) + blobs = append(blobs, pn) + t.Logf("permanode %d is %v", i, pn) + id.SetAttribute_NoTimeMove(pn, "foo", "bar") + } + sort.Sort(blob.ByRef(blobs)) + for i, br := range blobs { + t.Logf("Sorted %d = %v", i, br) + } + handler := qt.Handler() + + contToken := "" + tests := [][]blob.Ref{ + []blob.Ref{blobs[2], blobs[1]}, + []blob.Ref{blobs[0]}, + } + + for i, wantBlobs := range tests { + req := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{}, + }, + Limit: 2, + Sort: sortType, + Continue: contToken, + } + res, err := handler.Query(req) + if err != nil { + qt.t.Fatalf("Error on query %d: %v", i+1, err) + } + t.Logf("Query %d/%d: continue = %q", i+1, len(tests), res.Continue) + for i, sb := range res.Blobs { + t.Logf(" res[%d]: %v", i, sb.Blob) + } + + var want []*SearchResultBlob + for _, br := range wantBlobs { + want = append(want, &SearchResultBlob{Blob: br}) + } + if !reflect.DeepEqual(res.Blobs, want) { + gotj, wantj := prettyJSON(res.Blobs), prettyJSON(want) + t.Fatalf("Query %d: Got blobs:\n%s\nWant:\n%s\n", i+1, gotj, wantj) + } + contToken = res.Continue + haveToken := contToken != "" + wantHaveToken := (i + 1) < len(tests) + if haveToken != wantHaveToken { + t.Fatalf("Query %d: token = %q; want token = %v", i+1, contToken, wantHaveToken) + } + } + }) +} + +// Tests PermanodeConstraint.ValueAll +func TestQueryPermanodeValueAll(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + p1 := id.NewPlannedPermanode("1") + p2 := id.NewPlannedPermanode("2") + id.SetAttribute(p1, "attr", "foo") + id.SetAttribute(p1, "attr", "barrrrr") + id.SetAttribute(p2, "attr", "foo") + id.SetAttribute(p2, "attr", "bar") + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "attr", + ValueAll: true, + ValueMatches: &StringConstraint{ + ByteLength: &IntConstraint{ + Min: 3, + Max: 3, + }, + }, + }, + }, + } + qt.wantRes(sq, p2) + }) +} + +// Tests PermanodeConstraint.ValueMatches.CaseInsensitive. +func TestQueryPermanodeValueMatchesCaseInsensitive(t *testing.T) { + testQuery(t, func(qt *queryTest) { + id := qt.id + + p1 := id.NewPlannedPermanode("1") + p2 := id.NewPlannedPermanode("2") + + id.SetAttribute(p1, "x", "Foo") + id.SetAttribute(p2, "x", "start") + + sq := &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "or", + + A: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "x", + ValueMatches: &StringConstraint{ + Equals: "foo", + CaseInsensitive: true, + }, + }, + }, + + B: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "x", + ValueMatches: &StringConstraint{ + Contains: "TAR", + CaseInsensitive: true, + }, + }, + }, + }, + }, + } + qt.wantRes(sq, p1, p2) + }) +} + +func TestQueryChildren(t *testing.T) { + testQueryTypes(t, memIndexTypes, func(qt *queryTest) { + id := qt.id + + pdir := id.NewPlannedPermanode("some_dir") + p1 := id.NewPlannedPermanode("1") + p2 := id.NewPlannedPermanode("2") + p3 := id.NewPlannedPermanode("3") + + id.AddAttribute(pdir, "camliMember", p1.String()) + id.AddAttribute(pdir, "camliPath:foo", p2.String()) + id.AddAttribute(pdir, "other", p3.String()) + + // Make p1, p2, and p3 actually exist. (permanodes without attributes are dead) + id.AddAttribute(p1, "x", "x") + id.AddAttribute(p2, "x", "x") + id.AddAttribute(p3, "x", "x") + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "parent", + Any: &Constraint{ + BlobRefPrefix: pdir.String(), + }, + }, + }, + }, + } + qt.wantRes(sq, p1, p2) + }) +} + +// 13 permanodes are created. 1 of them the parent, 11 are children +// (== results), 1 is unrelated to the parent. +// limit is the limit on the number of results. +// pos is the position of the around permanode. +// note: pos is in the permanode creation order, but keep in mind +// they're enumerated in the opposite order. +func testAroundChildren(limit, pos int, t *testing.T) { + testQueryTypes(t, memIndexTypes, func(qt *queryTest) { + id := qt.id + + pdir := id.NewPlannedPermanode("some_dir") + p0 := id.NewPlannedPermanode("0") + p1 := id.NewPlannedPermanode("1") + p2 := id.NewPlannedPermanode("2") + p3 := id.NewPlannedPermanode("3") + p4 := id.NewPlannedPermanode("4") + p5 := id.NewPlannedPermanode("5") + p6 := id.NewPlannedPermanode("6") + p7 := id.NewPlannedPermanode("7") + p8 := id.NewPlannedPermanode("8") + p9 := id.NewPlannedPermanode("9") + p10 := id.NewPlannedPermanode("10") + p11 := id.NewPlannedPermanode("11") + + id.AddAttribute(pdir, "camliMember", p0.String()) + id.AddAttribute(pdir, "camliMember", p1.String()) + id.AddAttribute(pdir, "camliPath:foo", p2.String()) + const noMatchIndex = 3 + id.AddAttribute(pdir, "other", p3.String()) + id.AddAttribute(pdir, "camliPath:bar", p4.String()) + id.AddAttribute(pdir, "camliMember", p5.String()) + id.AddAttribute(pdir, "camliMember", p6.String()) + id.AddAttribute(pdir, "camliMember", p7.String()) + id.AddAttribute(pdir, "camliMember", p8.String()) + id.AddAttribute(pdir, "camliMember", p9.String()) + id.AddAttribute(pdir, "camliMember", p10.String()) + id.AddAttribute(pdir, "camliMember", p11.String()) + + // Predict the results + var around blob.Ref + lowLimit := pos - limit/2 + if lowLimit <= noMatchIndex { + // Because 3 is not included in the results + lowLimit-- + } + if lowLimit < 0 { + lowLimit = 0 + } + highLimit := lowLimit + limit + if highLimit >= noMatchIndex { + // Because noMatchIndex is not included in the results + highLimit++ + } + var want []blob.Ref + // Make the permanodes actually exist. (permanodes without attributes are dead) + for k, v := range []blob.Ref{p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11} { + id.AddAttribute(v, "x", "x") + if k == pos { + around = v + } + if k != noMatchIndex && k >= lowLimit && k < highLimit { + want = append(want, v) + } + } + // invert the order because the results are appended in reverse creation order + // because that's how we enumerate. + revWant := make([]blob.Ref, len(want)) + for k, v := range want { + revWant[len(want)-1-k] = v + } + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "parent", + Any: &Constraint{ + BlobRefPrefix: pdir.String(), + }, + }, + }, + }, + Limit: limit, + Around: around, + } + qt.wantRes(sq, revWant...) + }) + +} + +// TODO(mpl): more tests. at least the 0 results case. + +// Around will be found in the first buffered window of results, +// because it's a position that fits within the limit. +// So it doesn't exercice the part of the algorithm that discards +// the would-be results that are not within the "around zone". +func TestQueryChildrenAroundNear(t *testing.T) { + testAroundChildren(5, 9, t) +} + +// pos is near the end of the results enumeration and the limit is small +// so this test should go through the part of the algorithm that discards +// results not within the "around zone". +func TestQueryChildrenAroundFar(t *testing.T) { + testAroundChildren(3, 4, t) +} + +// permanodes tagged "foo" or those in sets where the parent +// permanode set itself is tagged "foo". +func TestQueryPermanodeTaggedViaParent(t *testing.T) { + t.Skip("TODO: finish implementing") + + testQuery(t, func(qt *queryTest) { + id := qt.id + + ptagged := id.NewPlannedPermanode("tagged_photo") + pindirect := id.NewPlannedPermanode("via_parent") + pset := id.NewPlannedPermanode("set") + pboth := id.NewPlannedPermanode("both") // funny directly and via its parent + pnotfunny := id.NewPlannedPermanode("not_funny") + + id.SetAttribute(ptagged, "tag", "funny") + id.SetAttribute(pset, "tag", "funny") + id.SetAttribute(pboth, "tag", "funny") + id.AddAttribute(pset, "camliMember", pindirect.String()) + id.AddAttribute(pset, "camliMember", pboth.String()) + id.SetAttribute(pnotfunny, "tag", "boring") + + sq := &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "or", + + // Those tagged funny directly: + A: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "tag", + Value: "funny", + }, + }, + + // Those tagged funny indirectly: + B: &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "ancestor", + Any: &Constraint{ + Permanode: &PermanodeConstraint{ + Attr: "tag", + Value: "funny", + }, + }, + }, + }, + }, + }, + }, + } + qt.wantRes(sq, ptagged, pset, pboth, pindirect) + }) +} + +func TestLimitDoesntDeadlock_UnspecifiedSort(t *testing.T) { + testLimitDoesntDeadlock(t, UnspecifiedSort) +} + +func TestLimitDoesntDeadlock_LastModifiedDesc(t *testing.T) { + testLimitDoesntDeadlock(t, LastModifiedDesc) +} + +func TestLimitDoesntDeadlock_CreatedDesc(t *testing.T) { + testLimitDoesntDeadlock(t, CreatedDesc) +} + +func testLimitDoesntDeadlock(t *testing.T, sortType SortType) { + testQueryTypes(t, memIndexTypes, func(qt *queryTest) { + id := qt.id + + const limit = 2 + for i := 0; i < ExportBufferedConst()+limit+1; i++ { + pn := id.NewPlannedPermanode(fmt.Sprint(i)) + id.SetAttribute(pn, "foo", "bar") + } + + req := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{}, + }, + Limit: limit, + Sort: sortType, + Describe: &DescribeRequest{}, + } + h := qt.Handler() + gotRes := make(chan bool, 1) + go func() { + _, err := h.Query(req) + if err != nil { + qt.t.Error(err) + } + gotRes <- true + }() + select { + case <-gotRes: + case <-time.After(5 * time.Second): + t.Error("timeout; deadlock?") + } + }) +} + +func prettyJSON(v interface{}) string { + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + panic(err) + } + return string(b) +} + +func TestPlannedQuery(t *testing.T) { + tests := []struct { + in, want *SearchQuery + }{ + { + in: &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{}, + }, + }, + want: &SearchQuery{ + Sort: CreatedDesc, + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{}, + }, + Limit: 200, + }, + }, + } + for i, tt := range tests { + got := tt.in.ExportPlannedQuery() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("%d. for input:\n%s\ngot:\n%s\nwant:\n%s\n", i, + prettyJSON(tt.in), prettyJSON(got), prettyJSON(tt.want)) + } + } +} + +func TestDescribeMarshal(t *testing.T) { + // Empty Describe + q := &SearchQuery{ + Describe: &DescribeRequest{}, + } + enc, err := json.Marshal(q) + if err != nil { + t.Fatal(err) + } + if got, want := string(enc), `{"around":null,"describe":{"blobref":null,"at":null}}`; got != want { + t.Errorf("JSON: %s; want %s", got, want) + } + back := &SearchQuery{} + err = json.Unmarshal(enc, back) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(q, back) { + t.Errorf("Didn't round-trip. Got %#v; want %#v", back, q) + } + + // DescribeRequest with multiple blobref + q = &SearchQuery{ + Describe: &DescribeRequest{ + BlobRefs: []blob.Ref{blob.MustParse("sha-1234"), blob.MustParse("sha-abcd")}, + }, + } + enc, err = json.Marshal(q) + if err != nil { + t.Fatal(err) + } + if got, want := string(enc), `{"around":null,"describe":{"blobrefs":["sha-1234","sha-abcd"],"blobref":null,"at":null}}`; got != want { + t.Errorf("JSON: %s; want %s", got, want) + } + back = &SearchQuery{} + err = json.Unmarshal(enc, back) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(q, back) { + t.Errorf("Didn't round-trip. Got %#v; want %#v", back, q) + } + + // and the zero value + q = &SearchQuery{} + enc, err = json.Marshal(q) + if err != nil { + t.Fatal(err) + } + if string(enc) != `{"around":null}` { + t.Errorf(`Zero value: %q; want null`, enc) + } +} + +func TestSortMarshal_UnspecifiedSort(t *testing.T) { + testSortMarshal(t, UnspecifiedSort) +} + +func TestSortMarshal_LastModifiedDesc(t *testing.T) { + testSortMarshal(t, LastModifiedDesc) +} + +func TestSortMarshal_CreatedDesc(t *testing.T) { + testSortMarshal(t, CreatedDesc) +} + +var sortMarshalWant = map[SortType]string{ + UnspecifiedSort: `{"around":null}`, + LastModifiedDesc: `{"sort":` + string(SortName[LastModifiedDesc]) + `,"around":null}`, + CreatedDesc: `{"sort":` + string(SortName[CreatedDesc]) + `,"around":null}`, +} + +func testSortMarshal(t *testing.T, sortType SortType) { + q := &SearchQuery{ + Sort: sortType, + } + enc, err := json.Marshal(q) + if err != nil { + t.Fatal(err) + } + if got, want := string(enc), sortMarshalWant[sortType]; got != want { + t.Errorf("JSON: %s; want %s", got, want) + } + back := &SearchQuery{} + err = json.Unmarshal(enc, back) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(q, back) { + t.Errorf("Didn't round-trip. Got %#v; want %#v", back, q) + } + + // and the zero value + q = &SearchQuery{} + enc, err = json.Marshal(q) + if err != nil { + t.Fatal(err) + } + if string(enc) != `{"around":null}` { + t.Errorf("Zero value: %s; want {}", enc) + } +} + +func BenchmarkQueryRecentPermanodes(b *testing.B) { + b.ReportAllocs() + testQueryTypes(b, corpusTypeOnly, func(qt *queryTest) { + id := qt.id + + p1 := id.NewPlannedPermanode("1") + id.SetAttribute(p1, "foo", "p1") + p2 := id.NewPlannedPermanode("2") + id.SetAttribute(p2, "foo", "p2") + p3 := id.NewPlannedPermanode("3") + id.SetAttribute(p3, "foo", "p3") + + req := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{}, + }, + Limit: 2, + Sort: UnspecifiedSort, + Describe: &DescribeRequest{}, + } + + h := qt.Handler() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + *req.Describe = DescribeRequest{} + _, err := h.Query(req) + if err != nil { + qt.t.Fatal(err) + } + } + }) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/search.go b/vendor/github.com/camlistore/camlistore/pkg/search/search.go new file mode 100644 index 00000000..8ba0badb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/search.go @@ -0,0 +1,29 @@ +/* +Copyright 2011 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package search describes and answers Camlistore search queries. +// +// Many of the search methods or functions provide results that are +// ordered by modification time, or at least depend on modification +// times. In that context, (un)deletions (of permanodes, or attributes) +// are not considered modifications and therefore the time at which they +// occured does not affect the result. +package search + +type QueryDescriber interface { + Query(*SearchQuery) (*SearchResult, error) + Describe(*DescribeRequest) (*DescribeResponse, error) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/search/websocket.go b/vendor/github.com/camlistore/camlistore/pkg/search/websocket.go new file mode 100644 index 00000000..bb6aec00 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/search/websocket.go @@ -0,0 +1,287 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "sync" + "time" + + "camlistore.org/third_party/github.com/gorilla/websocket" +) + +const ( + // Time allowed to write a message to the peer. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the peer. + pongWait = 60 * time.Second + + // Send pings to peer with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Maximum message size allowed from peer. + maxMessageSize = 10 << 10 +) + +type wsHub struct { + sh *Handler + register chan *wsConn + unregister chan *wsConn + watchReq chan watchReq + newBlobRecv chan string // new blob received. string is camliType. + updatedResults chan *watchedQuery + statusUpdate chan json.RawMessage + + // Owned by func run: + conns map[*wsConn]bool +} + +func newWebsocketHub(sh *Handler) *wsHub { + return &wsHub{ + sh: sh, + register: make(chan *wsConn), // unbuffered; issue 563 + unregister: make(chan *wsConn), // unbuffered; issue 563 + conns: make(map[*wsConn]bool), + watchReq: make(chan watchReq, buffered), + newBlobRecv: make(chan string, buffered), + updatedResults: make(chan *watchedQuery, buffered), + statusUpdate: make(chan json.RawMessage, buffered), + } +} + +func (h *wsHub) run() { + var lastStatusMsg []byte + for { + select { + case st := <-h.statusUpdate: + const prefix = `{"tag":"_status","status":` + lastStatusMsg = make([]byte, 0, len(prefix)+len(st)+1) + lastStatusMsg = append(lastStatusMsg, prefix...) + lastStatusMsg = append(lastStatusMsg, st...) + lastStatusMsg = append(lastStatusMsg, '}') + for c := range h.conns { + c.send <- lastStatusMsg + } + case c := <-h.register: + h.conns[c] = true + c.send <- lastStatusMsg + case c := <-h.unregister: + delete(h.conns, c) + close(c.send) + case camliType := <-h.newBlobRecv: + if camliType == "" { + // TODO: something smarter. some + // queries might care about all blobs. + // But for now only re-kick off + // queries if schema blobs arrive. We + // should track per-WatchdQuery which + // blob types the search cares about. + continue + } + // New blob was received. Kick off standing search queries to see if any changed. + for conn := range h.conns { + for _, wq := range conn.queries { + go h.doSearch(wq) + } + } + case wr := <-h.watchReq: + // Unsubscribe + if wr.q == nil { + delete(wr.conn.queries, wr.tag) + log.Printf("Removed subscription for %v, %q", wr.conn, wr.tag) + continue + } + // Very similar type, but semantically + // different, so separate for now: + wq := &watchedQuery{ + conn: wr.conn, + tag: wr.tag, + q: wr.q, + } + wr.conn.queries[wr.tag] = wq + log.Printf("Added/updated search subscription for tag %q", wr.tag) + go h.doSearch(wq) + + case wq := <-h.updatedResults: + if !h.conns[wq.conn] || wq.conn.queries[wq.tag] == nil { + // Client has since disconnected or unsubscribed. + continue + } + wq.mu.Lock() + lastres := wq.lastres + wq.mu.Unlock() + resb, err := json.Marshal(wsUpdateMessage{ + Tag: wq.tag, + Result: lastres, + }) + if err != nil { + panic(err) + } + wq.conn.send <- resb + } + } +} + +func (h *wsHub) doSearch(wq *watchedQuery) { + // Make our own copy, in case + q := new(SearchQuery) + *q = *wq.q // shallow copy, since Query will mutate its internal state fields + if q.Describe != nil { + q.Describe = new(DescribeRequest) + *q.Describe = *wq.q.Describe + } + + res, err := h.sh.Query(q) + if err != nil { + log.Printf("Query error: %v", err) + return + } + resj, _ := json.Marshal(res) + + wq.mu.Lock() + eq := bytes.Equal(wq.lastresj, resj) + wq.lastres = res + wq.lastresj = resj + wq.mu.Unlock() + if eq { + // No change in search. Ignore. + return + } + h.updatedResults <- wq +} + +type wsConn struct { + ws *websocket.Conn + send chan []byte // Buffered channel of outbound messages. + sh *Handler + + // queries is owned by the wsHub.run goroutine. + queries map[string]*watchedQuery // tag -> subscription +} + +type watchedQuery struct { + conn *wsConn + tag string + q *SearchQuery + + mu sync.Mutex // guards lastRes + lastres *SearchResult + lastresj []byte // as JSON +} + +// watchReq is a (un)subscribe request. +type watchReq struct { + conn *wsConn + tag string // required + q *SearchQuery // if nil, subscribe +} + +// Client->Server subscription message. +type wsClientMessage struct { + // Tag is required. + Tag string `json:"tag"` + // Query is required to subscribe. If absent, it means unsubscribe. + Query *SearchQuery `json:"query,omitempty"` +} + +type wsUpdateMessage struct { + Tag string `json:"tag"` + Result *SearchResult `json:"result,omitempty"` +} + +// readPump pumps messages from the websocket connection to the hub. +func (c *wsConn) readPump() { + defer func() { + c.sh.wsHub.unregister <- c + c.ws.Close() + }() + c.ws.SetReadLimit(maxMessageSize) + c.ws.SetReadDeadline(time.Now().Add(pongWait)) + c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + for { + _, message, err := c.ws.ReadMessage() + if err != nil { + break + } + log.Printf("Got websocket message %#q", message) + cm := new(wsClientMessage) + if err := json.Unmarshal(message, cm); err != nil { + log.Printf("Ignoring bogus websocket message. Err: %v", err) + continue + } + c.sh.wsHub.watchReq <- watchReq{ + conn: c, + tag: cm.Tag, + q: cm.Query, + } + } +} + +// write writes a message with the given message type and payload. +func (c *wsConn) write(mt int, payload []byte) error { + c.ws.SetWriteDeadline(time.Now().Add(writeWait)) + return c.ws.WriteMessage(mt, payload) +} + +// writePump pumps messages from the hub to the websocket connection. +func (c *wsConn) writePump() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + c.ws.Close() + }() + for { + select { + case message, ok := <-c.send: + if !ok { + c.write(websocket.CloseMessage, []byte{}) + return + } + if err := c.write(websocket.TextMessage, message); err != nil { + return + } + case <-ticker.C: + if err := c.write(websocket.PingMessage, []byte{}); err != nil { + return + } + } + } +} + +func (sh *Handler) serveWebSocket(rw http.ResponseWriter, req *http.Request) { + ws, err := websocket.Upgrade(rw, req, nil, 1024, 1024) + if _, ok := err.(websocket.HandshakeError); ok { + http.Error(rw, "Not a websocket handshake", 400) + return + } else if err != nil { + log.Println(err) + return + } + c := &wsConn{ + ws: ws, + send: make(chan []byte, 256), + sh: sh, + queries: make(map[string]*watchedQuery), + } + sh.wsHub.register <- c + go c.writePump() + c.readPump() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/app/app.go b/vendor/github.com/camlistore/camlistore/pkg/server/app/app.go new file mode 100644 index 00000000..9199ab70 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/app/app.go @@ -0,0 +1,274 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package app helps with configuring and starting server applications +// from Camlistore. +package app + +import ( + "errors" + "fmt" + "log" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "camlistore.org/pkg/auth" + camhttputil "camlistore.org/pkg/httputil" + "camlistore.org/pkg/jsonconfig" +) + +// Handler acts as a reverse proxy for a server application started by +// Camlistore. It can also serve some extra JSON configuration to the app. +type Handler struct { + name string // Name of the app's program. + envVars map[string]string // Variables set in the app's process environment. See doc/app-environment.txt. + + auth auth.AuthMode // Used for basic HTTP authenticating against the app requests. + appConfig jsonconfig.Obj // Additional parameters the app can request, or nil. + + proxy *httputil.ReverseProxy // For redirecting requests to the app. + backendURL string // URL that we proxy to (i.e. base URL of the app). + + process *os.Process // The app's Pid. To send it signals on restart, etc. +} + +func (a *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if camhttputil.PathSuffix(req) == "config.json" { + if a.auth.AllowedAccess(req)&auth.OpGet == auth.OpGet { + camhttputil.ReturnJSON(rw, a.appConfig) + } else { + auth.SendUnauthorized(rw, req) + } + return + } + if a.proxy == nil { + http.Error(rw, "no proxy for the app", 500) + return + } + a.proxy.ServeHTTP(rw, req) +} + +// randPortBackendURL picks a random free port to listen on, and combines it +// with apiHost and appHandlerPrefix to create the appBackendURL that the app +// will listen on, and that the app handler will proxy to. +func randPortBackendURL(apiHost, appHandlerPrefix string) (string, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return "", err + } + listener, err := net.ListenTCP("tcp", addr) + if err != nil { + return "", fmt.Errorf("could not listen to find random port: %v", err) + } + randAddr := listener.Addr().(*net.TCPAddr) + if err := listener.Close(); err != nil { + return "", fmt.Errorf("could not close random listener: %v", err) + } + + // TODO(mpl): see if can use netutil.TCPAddress. + scheme := "https://" + noScheme := strings.TrimPrefix(apiHost, scheme) + if strings.HasPrefix(noScheme, "http://") { + scheme = "http://" + noScheme = strings.TrimPrefix(noScheme, scheme) + } + hostPortPrefix := strings.SplitN(noScheme, "/", 2) + if len(hostPortPrefix) != 2 { + return "", fmt.Errorf("invalid apiHost: %q (no trailing slash?)", apiHost) + } + var host string + if strings.Contains(hostPortPrefix[0], "]") { + // we've got some IPv6 probably + hostPort := strings.Split(hostPortPrefix[0], "]") + host = hostPort[0] + "]" + } else { + hostPort := strings.Split(hostPortPrefix[0], ":") + host = hostPort[0] + } + return fmt.Sprintf("%s%s:%d%s", scheme, host, randAddr.Port, appHandlerPrefix), nil +} + +// NewHandler returns a Handler that proxies requests to an app. Start() on the +// Handler starts the app. +// The apiHost must end in a slash and is the camlistored API server for the app +// process to hit. +// The appHandlerPrefix is the URL path prefix on apiHost where the app is mounted. +// It must end in a slash, and be at minimum "/". +// The conf object has the following members, related to the vars described in +// doc/app-environment.txt: +// "program", string, required. File name of the app's program executable. Either +// an absolute path, or the name of a file located in CAMLI_APP_BINDIR or in PATH. +// "backendURL", string, optional. Automatic if absent. It sets CAMLI_APP_BACKEND_URL. +// "appConfig", object, optional. Additional configuration that the app can request from Camlistore. +func NewHandler(conf jsonconfig.Obj, apiHost, appHandlerPrefix string) (*Handler, error) { + // TODO: remove the appHandlerPrefix if/when we change where the app config JSON URL is made available. + name := conf.RequiredString("program") + backendURL := conf.OptionalString("backendURL", "") + appConfig := conf.OptionalObject("appConfig") + // TODO(mpl): add an auth token in the extra config of the dev server config, + // that the hello app can use to setup a status handler than only responds + // to requests with that token. + if err := conf.Validate(); err != nil { + return nil, err + } + + if apiHost == "" { + return nil, fmt.Errorf("app: could not initialize Handler for %q: Camlistore apiHost is unknown", name) + } + if appHandlerPrefix == "" { + return nil, fmt.Errorf("app: could not initialize Handler for %q: empty appHandlerPrefix", name) + } + + if backendURL == "" { + var err error + // If not specified in the conf, we're dynamically picking the port of the CAMLI_APP_BACKEND_URL + // now (instead of letting the app itself do it), because we need to know it in advance in order + // to set the app handler's proxy. + backendURL, err = randPortBackendURL(apiHost, appHandlerPrefix) + if err != nil { + return nil, err + } + } + + username, password := auth.RandToken(20), auth.RandToken(20) + camliAuth := username + ":" + password + basicAuth := auth.NewBasicAuth(username, password) + envVars := map[string]string{ + "CAMLI_API_HOST": apiHost, + "CAMLI_AUTH": camliAuth, + "CAMLI_APP_BACKEND_URL": backendURL, + } + if appConfig != nil { + envVars["CAMLI_APP_CONFIG_URL"] = apiHost + strings.TrimPrefix(appHandlerPrefix, "/") + "config.json" + } + + proxyURL, err := url.Parse(backendURL) + if err != nil { + return nil, fmt.Errorf("could not parse backendURL %q: %v", backendURL, err) + } + return &Handler{ + name: name, + envVars: envVars, + auth: basicAuth, + appConfig: appConfig, + proxy: httputil.NewSingleHostReverseProxy(proxyURL), + backendURL: backendURL, + }, nil +} + +func (a *Handler) Start() error { + name := a.name + if name == "" { + return fmt.Errorf("invalid app name: %q", name) + } + var binPath string + var err error + if e := os.Getenv("CAMLI_APP_BINDIR"); e != "" { + binPath, err = exec.LookPath(filepath.Join(e, name)) + if err != nil { + log.Printf("%q executable not found in %q", name, e) + } + } + if binPath == "" || err != nil { + binPath, err = exec.LookPath(name) + if err != nil { + return fmt.Errorf("%q executable not found in PATH.", name) + } + } + + cmd := exec.Command(binPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + // TODO(mpl): extract Env methods from dev/devcam/env.go to a util pkg and use them here. + newVars := make(map[string]string, len(a.envVars)) + for k, v := range a.envVars { + newVars[k+"="] = v + } + env := os.Environ() + for pos, oldkv := range env { + for k, newVal := range newVars { + if strings.HasPrefix(oldkv, k) { + env[pos] = k + newVal + delete(newVars, k) + break + } + } + } + for k, v := range newVars { + env = append(env, k+v) + } + cmd.Env = env + if err := cmd.Start(); err != nil { + return fmt.Errorf("could not start app %v: %v", name, err) + } + a.process = cmd.Process + return nil +} + +// ProgramName returns the name of the app's binary. It may be a file name in +// CAMLI_APP_BINDIR or PATH, or an absolute path. +func (a *Handler) ProgramName() string { + return a.name +} + +// AuthMode returns the app handler's auth mode, which is also the auth that the +// app's client will be configured with. This mode should be registered with +// the server's auth modes, for the app to have access to the server's resources. +func (a *Handler) AuthMode() auth.AuthMode { + return a.auth +} + +// AppConfig returns the optional configuration parameters object that the app +// can request from the app handler. It can be nil. +func (a *Handler) AppConfig() map[string]interface{} { + return a.appConfig +} + +// BackendURL returns the appBackendURL that the app handler will proxy to. +func (a *Handler) BackendURL() string { + return a.backendURL +} + +var errProcessTookTooLong = errors.New("proccess took too long to quit") + +// Quit sends the app's process a SIGINT, and waits up to 5 seconds for it +// to exit, returning an error if it doesn't. +func (a *Handler) Quit() error { + err := a.process.Signal(os.Interrupt) + if err != nil { + return err + } + + c := make(chan error) + go func() { + _, err := a.process.Wait() + c <- err + }() + select { + case err = <-c: + case <-time.After(5 * time.Second): + // TODO Do we want to SIGKILL here or just leave the app alone? + err = errProcessTookTooLong + } + return err +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/app/app_test.go b/vendor/github.com/camlistore/camlistore/pkg/server/app/app_test.go new file mode 100644 index 00000000..019d0174 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/app/app_test.go @@ -0,0 +1,220 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package app + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "regexp" + "testing" +) + +func TestRandPortBackendURL(t *testing.T) { + tests := []struct { + apiHost string + appHandlerPrefix string + wantBackendURL string + wantErr bool + }{ + { + apiHost: "http://foo.com/", + appHandlerPrefix: "/pics/", + wantBackendURL: "http://foo.com:[0-9]+/pics/", + }, + + { + apiHost: "https://foo.com/", + appHandlerPrefix: "/pics/", + wantBackendURL: "https://foo.com:[0-9]+/pics/", + }, + + { + apiHost: "http://foo.com:8080/", + appHandlerPrefix: "/pics/", + wantBackendURL: "http://foo.com:[0-9]+/pics/", + }, + + { + apiHost: "https://foo.com:8080/", + appHandlerPrefix: "/pics/", + wantBackendURL: "https://foo.com:[0-9]+/pics/", + }, + + { + apiHost: "http://foo.com:/", + appHandlerPrefix: "/pics/", + wantBackendURL: "http://foo.com:[0-9]+/pics/", + }, + + { + apiHost: "https://foo.com:/", + appHandlerPrefix: "/pics/", + wantBackendURL: "https://foo.com:[0-9]+/pics/", + }, + + { + apiHost: "http://foo.com/bar/", + appHandlerPrefix: "/pics/", + wantBackendURL: "http://foo.com:[0-9]+/pics/", + }, + + { + apiHost: "https://foo.com/bar/", + appHandlerPrefix: "/pics/", + wantBackendURL: "https://foo.com:[0-9]+/pics/", + }, + + { + apiHost: "http://foo.com:8080/bar/", + appHandlerPrefix: "/pics/", + wantBackendURL: "http://foo.com:[0-9]+/pics/", + }, + + { + apiHost: "https://foo.com:8080/bar/", + appHandlerPrefix: "/pics/", + wantBackendURL: "https://foo.com:[0-9]+/pics/", + }, + + { + apiHost: "http://foo.com:/bar/", + appHandlerPrefix: "/pics/", + wantBackendURL: "http://foo.com:[0-9]+/pics/", + }, + + { + apiHost: "https://foo.com:/bar/", + appHandlerPrefix: "/pics/", + wantBackendURL: "https://foo.com:[0-9]+/pics/", + }, + + { + apiHost: "http://[::1]:80/", + appHandlerPrefix: "/pics/", + wantBackendURL: `http://\[::1\]:[0-9]+/pics/`, + }, + + { + apiHost: "https://[::1]:80/", + appHandlerPrefix: "/pics/", + wantBackendURL: `https://\[::1\]:[0-9]+/pics/`, + }, + + { + apiHost: "http://[::1]/", + appHandlerPrefix: "/pics/", + wantBackendURL: `http://\[::1\]:[0-9]+/pics/`, + }, + + { + apiHost: "https://[::1]/", + appHandlerPrefix: "/pics/", + wantBackendURL: `https://\[::1\]:[0-9]+/pics/`, + }, + + { + apiHost: "http://[::1]:/", + appHandlerPrefix: "/pics/", + wantBackendURL: `http://\[::1\]:[0-9]+/pics/`, + }, + + { + apiHost: "https://[::1]:/", + appHandlerPrefix: "/pics/", + wantBackendURL: `https://\[::1\]:[0-9]+/pics/`, + }, + } + for _, v := range tests { + got, err := randPortBackendURL(v.apiHost, v.appHandlerPrefix) + if err != nil { + t.Error(err) + continue + } + reg := regexp.MustCompile(v.wantBackendURL) + if !reg.MatchString(got) { + t.Errorf("got: %v for %v, want: %v", got, v.apiHost, v.wantBackendURL) + } + } +} + +// We just want a helper command that ignores SIGINT. +func ignoreInterrupt() (*os.Process, error) { + script := `trap "echo hello" SIGINT +echo READY +sleep 10000` + cmd := exec.Command("bash") + + w, err := cmd.StdinPipe() + if err != nil { + return nil, fmt.Errorf("couldn't get pipe for helper shell") + } + go io.WriteString(w, script) + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("couldn't get pipe for helper shell") + } + + err = cmd.Start() + if err != nil { + return nil, fmt.Errorf("couldn't start helper shell") + } + + r := bufio.NewReader(stdout) + l, err := r.ReadBytes('\n') + if err != nil { + return nil, fmt.Errorf("couldn't read from helper shell") + } + if string(l) != "READY\n" { + return nil, fmt.Errorf("unexpected output from helper shell script") + } + return cmd.Process, nil +} + +func TestQuit(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + + cmd := exec.Command("sleep", "10000") + err := cmd.Start() + if err != nil { + t.Skip("couldn't run test helper command") + } + h := Handler{ + process: cmd.Process, + } + err = h.Quit() + if err != nil { + t.Errorf("got %v, wanted %v", err, nil) + } + + pid, err := ignoreInterrupt() + if err != nil { + t.Skip("couldn't run test helper command: %v", err) + } + h = Handler{ + process: pid, + } + err = h.Quit() + if err != errProcessTookTooLong { + t.Errorf("got %v, wanted %v", err, errProcessTookTooLong) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/cgo_probe.go b/vendor/github.com/camlistore/camlistore/pkg/server/cgo_probe.go new file mode 100644 index 00000000..e6a010df --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/cgo_probe.go @@ -0,0 +1,23 @@ +/* +Copyright 2015 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +build cgo + +package server + +func init() { + cgoEnabled = true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/doc.go b/vendor/github.com/camlistore/camlistore/pkg/server/doc.go new file mode 100644 index 00000000..651ee0dd --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package server implements the server HTTP interface for the UI, +// publishing, setup, status, sync, thubnailing, etc. +package server diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/download.go b/vendor/github.com/camlistore/camlistore/pkg/server/download.go new file mode 100644 index 00000000..83bc1998 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/download.go @@ -0,0 +1,199 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/magic" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/search" + "camlistore.org/pkg/types" +) + +const oneYear = 365 * 86400 * time.Second + +var debugPack = strings.Contains(os.Getenv("CAMLI_DEBUG_X"), "packserve") + +type DownloadHandler struct { + Fetcher blob.Fetcher + Cache blobserver.Storage + + // Search is optional. If present, it's used to map a fileref + // to a wholeref, if the Fetcher is of a type that knows how + // to get at a wholeref more efficiently. (e.g. blobpacked) + Search *search.Handler + + ForceMIME string // optional +} + +func (dh *DownloadHandler) blobSource() blob.Fetcher { + return dh.Fetcher // TODO: use dh.Cache +} + +type fileInfo struct { + mime string + name string + size int64 + rs io.ReadSeeker + close func() error // release the rs + whyNot string // for testing, why fileInfoPacked failed. +} + +func (dh *DownloadHandler) fileInfo(req *http.Request, file blob.Ref) (fi fileInfo, packed bool, err error) { + // Fast path for blobpacked. + fi, ok := fileInfoPacked(dh.Search, dh.Fetcher, req, file) + if debugPack { + log.Printf("download.go: fileInfoPacked: ok=%v, %+v", ok, fi) + } + if ok { + return fi, true, nil + } + fr, err := schema.NewFileReader(dh.blobSource(), file) + if err != nil { + return + } + mime := dh.ForceMIME + if mime == "" { + mime = magic.MIMETypeFromReaderAt(fr) + } + if mime == "" { + mime = "application/octet-stream" + } + return fileInfo{ + mime: mime, + name: fr.FileName(), + size: fr.Size(), + rs: fr, + close: fr.Close, + }, false, nil +} + +// Fast path for blobpacked. +func fileInfoPacked(sh *search.Handler, src blob.Fetcher, req *http.Request, file blob.Ref) (packFileInfo fileInfo, ok bool) { + if sh == nil { + return fileInfo{whyNot: "no search"}, false + } + wf, ok := src.(blobserver.WholeRefFetcher) + if !ok { + return fileInfo{whyNot: "fetcher type"}, false + } + if req != nil && req.Header.Get("Range") != "" { + // TODO: not handled yet. Maybe not even important, + // considering rarity. + return fileInfo{whyNot: "range header"}, false + } + des, err := sh.Describe(&search.DescribeRequest{BlobRef: file}) + if err != nil { + log.Printf("ui: fileInfoPacked: skipping fast path due to error from search: %v", err) + return fileInfo{whyNot: "search error"}, false + } + db, ok := des.Meta[file.String()] + if !ok || db.File == nil { + return fileInfo{whyNot: "search index doesn't know file"}, false + } + fi := db.File + if !fi.WholeRef.Valid() { + return fileInfo{whyNot: "no wholeref from search index"}, false + } + + offset := int64(0) + rc, wholeSize, err := wf.OpenWholeRef(fi.WholeRef, offset) + if err == os.ErrNotExist { + return fileInfo{whyNot: "WholeRefFetcher returned ErrNotexist"}, false + } + if wholeSize != fi.Size { + log.Printf("ui: fileInfoPacked: OpenWholeRef size %d != index size %d; ignoring fast path", wholeSize, fi.Size) + return fileInfo{whyNot: "WholeRefFetcher and index don't agree"}, false + } + if err != nil { + log.Printf("ui: fileInfoPacked: skipping fast path due to error from WholeRefFetcher (%T): %v", src, err) + return fileInfo{whyNot: "WholeRefFetcher error"}, false + } + return fileInfo{ + mime: fi.MIMEType, + name: fi.FileName, + size: fi.Size, + rs: types.NewFakeSeeker(rc, fi.Size-offset), + close: rc.Close, + }, true +} + +func (dh *DownloadHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, file blob.Ref) { + if req.Method != "GET" && req.Method != "HEAD" { + http.Error(rw, "Invalid download method", http.StatusBadRequest) + return + } + if req.Header.Get("If-Modified-Since") != "" { + // Immutable, so any copy's a good copy. + rw.WriteHeader(http.StatusNotModified) + return + } + + fi, packed, err := dh.fileInfo(req, file) + if err != nil { + http.Error(rw, "Can't serve file: "+err.Error(), http.StatusInternalServerError) + return + } + defer fi.close() + + h := rw.Header() + h.Set("Content-Length", fmt.Sprint(fi.size)) + h.Set("Expires", time.Now().Add(oneYear).Format(http.TimeFormat)) + h.Set("Content-Type", fi.mime) + if packed { + h.Set("X-Camlistore-Packed", "1") + } + + if fi.mime == "application/octet-stream" { + // Chrome seems to silently do nothing on + // application/octet-stream unless this is set. + // Maybe it's confused by lack of URL it recognizes + // along with lack of mime type? + fileName := fi.name + if fileName == "" { + fileName = "file-" + file.String() + ".dat" + } + rw.Header().Set("Content-Disposition", "attachment; filename="+fileName) + } + + if req.Method == "HEAD" && req.FormValue("verifycontents") != "" { + vbr, ok := blob.Parse(req.FormValue("verifycontents")) + if !ok { + return + } + hash := vbr.Hash() + if hash == nil { + return + } + io.Copy(hash, fi.rs) // ignore errors, caught later + if vbr.HashMatches(hash) { + rw.Header().Set("X-Camli-Contents", vbr.String()) + } + return + } + + http.ServeContent(rw, req, "", time.Now(), fi.rs) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/favicon.ico b/vendor/github.com/camlistore/camlistore/pkg/server/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b63b2b56e51c53dd0b4e15c45fdbbf66ecdab2d0 GIT binary patch literal 1150 zcmZ{jKX21e6vb~<%AW2MT@inMxX;8g7f9}Ke2UPtOjw9t*I~8maL^YNNiU*{0DF!?YSj+o}k+%=Ew%% zSih_XzUjARn%W_GcGM?ki%@eAPcz#GWA3MLC)CqdkN7co z2QGtS>YLbHKZC|~IrWzMUH0d2DO{g*2FBFaR)+Q~*t9)zXG$x*YfF6>eiAgYA?p!& zhHz~(k)DDJV2j*dcV2lH`h@<|)A~gnIsUnIsi~ch1iyb^RH;O`+0v)c^X`nlQBDmgwgbFX0@a=706 z_xX?D_IRI>K2^Tu+4;NPAb&#q7=2EzqJN*?58hGC++$s>ysJdqlsO_j3TLl4KQrEK zM!uADD7+sm)RuXQe$FzVx500pe!Jj1eKhqNkG0CD)GS&f-NLJd=OiFj#Pj;1(4}0Y z=V<)9aXecZ_y0yY2j^qG4=7{md#7Cf!YsXJ*5Rvsxl)k6IzFxfc;6dcD&jmgyKCk; F#~;4p@)iI9 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/fileembed.go b/vendor/github.com/camlistore/camlistore/pkg/server/fileembed.go new file mode 100644 index 00000000..fdb3aa81 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/fileembed.go @@ -0,0 +1,35 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +#fileembed pattern .+\.(ico)$ +*/ +package server + +import ( + "os" + "path/filepath" + + "camlistore.org/pkg/fileembed" +) + +var Files = &fileembed.Files{} + +func init() { + if root := os.Getenv("CAMLI_DEV_CAMLI_ROOT"); root != "" { + Files.DirFallback = filepath.Join(root, filepath.FromSlash("pkg/server")) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/filetree.go b/vendor/github.com/camlistore/camlistore/pkg/server/filetree.go new file mode 100644 index 00000000..daef6b35 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/filetree.go @@ -0,0 +1,87 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "log" + "net/http" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/schema" +) + +type FileTreeHandler struct { + Fetcher blob.Fetcher + file blob.Ref +} + +// FileTreeNode represents a file in a file tree. +// It is part of the FileTreeResponse. +type FileTreeNode struct { + // Name is the basename of the node. + Name string `json:"name"` + // Type is the camliType of the node. This may be "file", "directory", "symlink" + // or other in the future. + Type string `json:"type"` + // BlobRef is the blob.Ref of the node. + BlobRef blob.Ref `json:"blobRef"` +} + +// FileTreeResponse is the JSON response for the FileTreeHandler. +type FileTreeResponse struct { + // Children is the list of children files of a directory. + Children []FileTreeNode `json:"children"` +} + +func (fth *FileTreeHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if req.Method != "GET" && req.Method != "HEAD" { + http.Error(rw, "Invalid method", 400) + return + } + + de, err := schema.NewDirectoryEntryFromBlobRef(fth.Fetcher, fth.file) + if err != nil { + http.Error(rw, "Error reading directory", 500) + log.Printf("Error reading directory from blobref %s: %v\n", fth.file, err) + return + } + dir, err := de.Directory() + if err != nil { + http.Error(rw, "Error reading directory", 500) + log.Printf("Error reading directory from blobref %s: %v\n", fth.file, err) + return + } + entries, err := dir.Readdir(-1) + if err != nil { + http.Error(rw, "Error reading directory", 500) + log.Printf("reading dir from blobref %s: %v\n", fth.file, err) + return + } + + var ret = FileTreeResponse{ + Children: make([]FileTreeNode, 0, len(entries)), + } + for _, v := range entries { + ret.Children = append(ret.Children, FileTreeNode{ + Name: v.FileName(), + Type: v.CamliType(), + BlobRef: v.BlobRef(), + }) + } + httputil.ReturnJSON(rw, ret) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/help.go b/vendor/github.com/camlistore/camlistore/pkg/server/help.go new file mode 100644 index 00000000..f731bf60 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/help.go @@ -0,0 +1,124 @@ +/* +Copyright 2015 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "encoding/json" + "fmt" + "html/template" + "net/http" + "strconv" + "sync" + + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/types/clientconfig" +) + +const helpHTML string = ` + + Help + + +

    Help

    + +

    Web User Interface

    +

    Search bar predicates.

    + +

    Client Configuration

    +

    You will need to use the following client configuration in order to access this server using the Camlistore command line tools.

    +
    {{ . }}
    + +

    Anything Else?

    +

    See the Camlistore online documentation and community contacts.

    + + ` + +// HelpHandler publishes information related to accessing the server +type HelpHandler struct { + clientConfig *clientconfig.Config // generated from serverConfig + serverConfig jsonconfig.Obj // low-level config + goTemplate *template.Template // for rendering +} + +// setServerConfigOnce guards operation within SetServerConfig +var setServerConfigOnce sync.Once + +// SetServerConfig enables the handler to receive the server config +// before InitHandler, which generates a client config from the server config, is called. +func (hh *HelpHandler) SetServerConfig(config jsonconfig.Obj) { + setServerConfigOnce.Do(func() { hh.serverConfig = config }) +} + +func init() { + blobserver.RegisterHandlerConstructor("help", newHelpFromConfig) +} + +func (hh *HelpHandler) InitHandler(hl blobserver.FindHandlerByTyper) error { + if hh.serverConfig == nil { + return fmt.Errorf("HelpHandler's serverConfig must be set before calling its InitHandler") + } + + clientConfig, err := clientconfig.GenerateClientConfig(hh.serverConfig) + if err != nil { + return fmt.Errorf("error generating client config: %v", err) + } + hh.clientConfig = clientConfig + + tmpl, err := template.New("help").Parse(helpHTML) + if err != nil { + return fmt.Errorf("error creating template: %v", err) + } + hh.goTemplate = tmpl + + return nil +} + +func newHelpFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) { + return &HelpHandler{}, nil +} + +func (hh *HelpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + suffix := httputil.PathSuffix(req) + if !httputil.IsGet(req) { + http.Error(rw, "Illegal help method.", http.StatusMethodNotAllowed) + return + } + switch suffix { + case "": + if clientConfig := req.FormValue("clientConfig"); clientConfig != "" { + if clientConfigOnly, err := strconv.ParseBool(clientConfig); err == nil && clientConfigOnly { + httputil.ReturnJSON(rw, hh.clientConfig) + return + } + } + hh.serveHelpHTML(rw, req) + default: + http.Error(rw, "Illegal help path.", http.StatusNotFound) + } +} + +func (hh *HelpHandler) serveHelpHTML(rw http.ResponseWriter, req *http.Request) { + jsonBytes, err := json.MarshalIndent(hh.clientConfig, "", " ") + if err != nil { + httputil.ServeError(rw, req, fmt.Errorf("could not serialize client config JSON: %v", err)) + return + } + + hh.goTemplate.Execute(rw, string(jsonBytes)) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/image.go b/vendor/github.com/camlistore/camlistore/pkg/server/image.go new file mode 100644 index 00000000..256e5a16 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/image.go @@ -0,0 +1,418 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "bytes" + "errors" + "expvar" + "fmt" + "image" + "image/png" + "io" + "io/ioutil" + "log" + "net/http" + "strconv" + "strings" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/constants" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/images" + "camlistore.org/pkg/magic" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/search" + "camlistore.org/pkg/singleflight" + "camlistore.org/pkg/syncutil" + "camlistore.org/pkg/types" + + _ "camlistore.org/third_party/github.com/nf/cr2" + "camlistore.org/third_party/go/pkg/image/jpeg" +) + +const imageDebug = false + +var ( + imageBytesServedVar = expvar.NewInt("image-bytes-served") + imageBytesFetchedVar = expvar.NewInt("image-bytes-fetched") + thumbCacheMiss = expvar.NewInt("thumbcache-miss") + thumbCacheHitFull = expvar.NewInt("thumbcache-hit-full") + thumbCacheHitFile = expvar.NewInt("thumbcache-hit-file") + thumbCacheHeader304 = expvar.NewInt("thumbcache-header-304") +) + +type ImageHandler struct { + Fetcher blob.Fetcher + Search *search.Handler // optional + Cache blobserver.Storage // optional + MaxWidth, MaxHeight int + Square bool + ThumbMeta *ThumbMeta // optional cache index for scaled images + ResizeSem *syncutil.Sem // Limit peak RAM used by concurrent image thumbnail calls. +} + +type subImager interface { + SubImage(image.Rectangle) image.Image +} + +func squareImage(i image.Image) image.Image { + si, ok := i.(subImager) + if !ok { + log.Fatalf("image %T isn't a subImager", i) + } + b := i.Bounds() + if b.Dx() > b.Dy() { + thin := (b.Dx() - b.Dy()) / 2 + newB := b + newB.Min.X += thin + newB.Max.X -= thin + return si.SubImage(newB) + } + thin := (b.Dy() - b.Dx()) / 2 + newB := b + newB.Min.Y += thin + newB.Max.Y -= thin + return si.SubImage(newB) +} + +func writeToCache(cache blobserver.Storage, thumbBytes []byte, name string) (br blob.Ref, err error) { + tr := bytes.NewReader(thumbBytes) + if len(thumbBytes) < constants.MaxBlobSize { + br = blob.SHA1FromBytes(thumbBytes) + _, err = blobserver.Receive(cache, br, tr) + } else { + // TODO: don't use rolling checksums when writing this. Tell + // the filewriter to use 16 MB chunks instead. + br, err = schema.WriteFileFromReader(cache, name, tr) + } + if err != nil { + return br, errors.New("failed to cache " + name + ": " + err.Error()) + } + if imageDebug { + log.Printf("Image Cache: saved as %v\n", br) + } + return br, nil +} + +// cacheScaled saves in the image handler's cache the scaled image bytes +// in thumbBytes, and puts its blobref in the scaledImage under the key name. +func (ih *ImageHandler) cacheScaled(thumbBytes []byte, name string) error { + br, err := writeToCache(ih.Cache, thumbBytes, name) + if err != nil { + return err + } + ih.ThumbMeta.Put(name, br) + return nil +} + +// cached returns a FileReader for the given blobref, which may +// point to either a blob representing the entire thumbnail (max +// 16MB) or a file schema blob. +// +// The ReadCloser should be closed when done reading. +func (ih *ImageHandler) cached(br blob.Ref) (io.ReadCloser, error) { + rsc, _, err := ih.Cache.Fetch(br) + if err != nil { + return nil, err + } + slurp, err := ioutil.ReadAll(rsc) + rsc.Close() + if err != nil { + return nil, err + } + // In the common case, when the scaled image itself is less than 16 MB, it's + // all together in one blob. + if strings.HasPrefix(magic.MIMEType(slurp), "image/") { + thumbCacheHitFull.Add(1) + if imageDebug { + log.Printf("Image Cache: hit: %v\n", br) + } + return ioutil.NopCloser(bytes.NewReader(slurp)), nil + } + + // For large scaled images, the cached blob is a file schema blob referencing + // the sub-chunks. + fileBlob, err := schema.BlobFromReader(br, bytes.NewReader(slurp)) + if err != nil { + log.Printf("Failed to parse non-image thumbnail cache blob %v: %v", br, err) + return nil, err + } + fr, err := fileBlob.NewFileReader(ih.Cache) + if err != nil { + log.Printf("cached(%v) NewFileReader = %v", br, err) + return nil, err + } + thumbCacheHitFile.Add(1) + if imageDebug { + log.Printf("Image Cache: fileref hit: %v\n", br) + } + return fr, nil +} + +// Key format: "scaled:" + bref + ":" + width "x" + height +// where bref is the blobref of the unscaled image. +func cacheKey(bref string, width int, height int) string { + return fmt.Sprintf("scaled:%v:%dx%d:tv%v", bref, width, height, images.ThumbnailVersion()) +} + +// ScaledCached reads the scaled version of the image in file, +// if it is in cache and writes it to buf. +// +// On successful read and population of buf, the returned format is non-empty. +// Almost all errors are not interesting. Real errors will be logged. +func (ih *ImageHandler) scaledCached(buf *bytes.Buffer, file blob.Ref) (format string) { + key := cacheKey(file.String(), ih.MaxWidth, ih.MaxHeight) + br, err := ih.ThumbMeta.Get(key) + if err == errCacheMiss { + return + } + if err != nil { + log.Printf("Warning: thumbnail cachekey(%q)->meta lookup error: %v", key, err) + return + } + fr, err := ih.cached(br) + if err != nil { + if imageDebug { + log.Printf("Could not get cached image %v: %v\n", br, err) + } + return + } + defer fr.Close() + _, err = io.Copy(buf, fr) + if err != nil { + return + } + mime := magic.MIMEType(buf.Bytes()) + if format = strings.TrimPrefix(mime, "image/"); format == mime { + log.Printf("Warning: unescaped MIME type %q of %v file for thumbnail %q", mime, br, key) + return + } + return format +} + +// Gate the number of concurrent image resizes to limit RAM & CPU use. + +type formatAndImage struct { + format string + image []byte +} + +// imageConfigFromReader calls image.DecodeConfig on r. It returns an +// io.Reader that is the concatentation of the bytes read and the remaining r, +// the image configuration, and the error from image.DecodeConfig. +func imageConfigFromReader(r io.Reader) (io.Reader, image.Config, error) { + header := new(bytes.Buffer) + tr := io.TeeReader(r, header) + // We just need width & height for memory considerations, so we use the + // standard library's DecodeConfig, skipping the EXIF parsing and + // orientation correction for images.DecodeConfig. + conf, _, err := image.DecodeConfig(tr) + return io.MultiReader(header, r), conf, err +} + +func (ih *ImageHandler) newFileReader(fileRef blob.Ref) (io.ReadCloser, error) { + fi, ok := fileInfoPacked(ih.Search, ih.Fetcher, nil, fileRef) + if debugPack { + log.Printf("pkg/server/image.go: fileInfoPacked: ok=%v, %+v", ok, fi) + } + if ok { + // This would be less gross if fileInfoPacked just + // returned an io.ReadCloser, but then the download + // handler would need more invasive changes for + // ServeContent. So tolerate this for now. + return struct { + io.Reader + io.Closer + }{ + fi.rs, + types.CloseFunc(fi.close), + }, nil + } + // Default path, not going through blobpacked's fast path: + return schema.NewFileReader(ih.Fetcher, fileRef) +} + +func (ih *ImageHandler) scaleImage(fileRef blob.Ref) (*formatAndImage, error) { + fr, err := ih.newFileReader(fileRef) + if err != nil { + return nil, err + } + defer fr.Close() + + sr := types.NewStatsReader(imageBytesFetchedVar, fr) + sr, conf, err := imageConfigFromReader(sr) + if err != nil { + return nil, err + } + + // TODO(wathiede): build a size table keyed by conf.ColorModel for + // common color models for a more exact size estimate. + + // This value is an estimate of the memory required to decode an image. + // PNGs range from 1-64 bits per pixel (not all of which are supported by + // the Go standard parser). JPEGs encoded in YCbCr 4:4:4 are 3 byte/pixel. + // For all other JPEGs this is an overestimate. For GIFs it is 3x larger + // than needed. How accurate this estimate is depends on the mix of + // images being resized concurrently. + ramSize := int64(conf.Width) * int64(conf.Height) * 3 + + if err = ih.ResizeSem.Acquire(ramSize); err != nil { + return nil, err + } + defer ih.ResizeSem.Release(ramSize) + + i, imConfig, err := images.Decode(sr, &images.DecodeOpts{ + MaxWidth: ih.MaxWidth, + MaxHeight: ih.MaxHeight, + }) + if err != nil { + return nil, err + } + b := i.Bounds() + format := imConfig.Format + + isSquare := b.Dx() == b.Dy() + if ih.Square && !isSquare { + i = squareImage(i) + b = i.Bounds() + } + + // Encode as a new image + var buf bytes.Buffer + switch format { + case "png": + err = png.Encode(&buf, i) + case "cr2": + // Recompress CR2 files as JPEG + format = "jpeg" + fallthrough + default: + err = jpeg.Encode(&buf, i, &jpeg.Options{ + Quality: 90, + }) + } + if err != nil { + return nil, err + } + + return &formatAndImage{format: format, image: buf.Bytes()}, nil +} + +// singleResize prevents generating the same thumbnail at once from +// two different requests. (e.g. sending out a link to a new photo +// gallery to a big audience) +var singleResize singleflight.Group + +func (ih *ImageHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, file blob.Ref) { + if !httputil.IsGet(req) { + http.Error(rw, "Invalid method", 400) + return + } + mw, mh := ih.MaxWidth, ih.MaxHeight + if mw == 0 || mh == 0 || mw > search.MaxImageSize || mh > search.MaxImageSize { + http.Error(rw, "bogus dimensions", 400) + return + } + + key := cacheKey(file.String(), mw, mh) + etag := blob.SHA1FromString(key).String()[5:] + inm := req.Header.Get("If-None-Match") + if inm != "" { + if strings.Trim(inm, `"`) == etag { + thumbCacheHeader304.Add(1) + rw.WriteHeader(http.StatusNotModified) + return + } + } else { + if !disableThumbCache && req.Header.Get("If-Modified-Since") != "" { + thumbCacheHeader304.Add(1) + rw.WriteHeader(http.StatusNotModified) + return + } + } + + var imageData []byte + format := "" + cacheHit := false + if ih.ThumbMeta != nil && !disableThumbCache { + var buf bytes.Buffer + format = ih.scaledCached(&buf, file) + if format != "" { + cacheHit = true + imageData = buf.Bytes() + } + } + + if !cacheHit { + thumbCacheMiss.Add(1) + imi, err := singleResize.Do(key, func() (interface{}, error) { + return ih.scaleImage(file) + }) + if err != nil { + http.Error(rw, err.Error(), 500) + return + } + im := imi.(*formatAndImage) + imageData = im.image + format = im.format + if ih.ThumbMeta != nil { + err := ih.cacheScaled(imageData, key) + if err != nil { + log.Printf("image resize: %v", err) + } + } + } + + h := rw.Header() + if !disableThumbCache { + h.Set("Expires", time.Now().Add(oneYear).Format(http.TimeFormat)) + h.Set("Last-Modified", time.Now().Format(http.TimeFormat)) + h.Set("Etag", strconv.Quote(etag)) + } + h.Set("Content-Type", imageContentTypeOfFormat(format)) + size := len(imageData) + h.Set("Content-Length", fmt.Sprint(size)) + imageBytesServedVar.Add(int64(size)) + + if req.Method == "GET" { + n, err := rw.Write(imageData) + if err != nil { + if strings.Contains(err.Error(), "broken pipe") { + // boring. + return + } + // TODO: vlog this: + log.Printf("error serving thumbnail of file schema %s: %v", file, err) + return + } + if n != size { + log.Printf("error serving thumbnail of file schema %s: sent %d, expected size of %d", + file, n, size) + return + } + } +} + +func imageContentTypeOfFormat(format string) string { + if format == "jpeg" { + return "image/jpeg" + } + return "image/png" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/root.go b/vendor/github.com/camlistore/camlistore/pkg/server/root.go new file mode 100644 index 00000000..cc87ad9f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/root.go @@ -0,0 +1,276 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "sort" + "sync" + + "camlistore.org/pkg/auth" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/buildinfo" + "camlistore.org/pkg/env" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/images" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/jsonsign/signhandler" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/search" + "camlistore.org/pkg/types" + "camlistore.org/pkg/types/camtypes" +) + +// RootHandler handles serving the about/splash page. +type RootHandler struct { + // Stealth determines whether we hide from non-authenticated + // clients. + Stealth bool + + OwnerName string // for display purposes only. + Username string // default user for mobile setup. + + // URL prefixes (path or full URL) to the primary blob and + // search root. + BlobRoot string + SearchRoot string + helpRoot string + importerRoot string + statusRoot string + Prefix string // root handler's prefix + + // JSONSignRoot is the optional path or full URL to the JSON + // Signing helper. + JSONSignRoot string + + Storage blobserver.Storage // of BlobRoot, or nil + + searchInitOnce sync.Once // runs searchInit, which populates searchHandler + searchInit func() + searchHandler *search.Handler // of SearchRoot, or nil + + ui *UIHandler // or nil, if none configured + sigh *signhandler.Handler // or nil, if none configured + sync []*SyncHandler // list of configured sync handlers, for discovery. +} + +func (rh *RootHandler) SearchHandler() (h *search.Handler, ok bool) { + rh.searchInitOnce.Do(rh.searchInit) + return rh.searchHandler, rh.searchHandler != nil +} + +func init() { + blobserver.RegisterHandlerConstructor("root", newRootFromConfig) +} + +func newRootFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) { + checkType := func(key string, htype string) { + v := conf.OptionalString(key, "") + if v == "" { + return + } + ct := ld.GetHandlerType(v) + if ct == "" { + err = fmt.Errorf("root handler's %q references non-existant %q", key, v) + } else if ct != htype { + err = fmt.Errorf("root handler's %q references %q of type %q; expected type %q", key, v, ct, htype) + } + } + checkType("searchRoot", "search") + checkType("jsonSignRoot", "jsonsign") + if err != nil { + return + } + username, _ := getUserName() + root := &RootHandler{ + BlobRoot: conf.OptionalString("blobRoot", ""), + SearchRoot: conf.OptionalString("searchRoot", ""), + JSONSignRoot: conf.OptionalString("jsonSignRoot", ""), + OwnerName: conf.OptionalString("ownerName", username), + Username: osutil.Username(), + Prefix: ld.MyPrefix(), + } + root.Stealth = conf.OptionalBool("stealth", false) + root.statusRoot = conf.OptionalString("statusRoot", "") + root.helpRoot = conf.OptionalString("helpRoot", "") + if err = conf.Validate(); err != nil { + return + } + + if root.BlobRoot != "" { + bs, err := ld.GetStorage(root.BlobRoot) + if err != nil { + return nil, fmt.Errorf("Root handler's blobRoot of %q error: %v", root.BlobRoot, err) + } + root.Storage = bs + } + + if root.JSONSignRoot != "" { + h, _ := ld.GetHandler(root.JSONSignRoot) + if sigh, ok := h.(*signhandler.Handler); ok { + root.sigh = sigh + } + } + + root.searchInit = func() {} + if root.SearchRoot != "" { + prefix := root.SearchRoot + if t := ld.GetHandlerType(prefix); t != "search" { + if t == "" { + return nil, fmt.Errorf("root handler's searchRoot of %q is invalid and doesn't refer to a declared handler", prefix) + } + return nil, fmt.Errorf("root handler's searchRoot of %q is of type %q, not %q", prefix, t, "search") + } + root.searchInit = func() { + h, err := ld.GetHandler(prefix) + if err != nil { + log.Fatalf("Error fetching SearchRoot at %q: %v", prefix, err) + } + root.searchHandler = h.(*search.Handler) + root.searchInit = nil + } + } + + if pfx, _, _ := ld.FindHandlerByType("importer"); err == nil { + root.importerRoot = pfx + } + + return root, nil +} + +func (rh *RootHandler) registerUIHandler(h *UIHandler) { + rh.ui = h +} + +func (rh *RootHandler) registerSyncHandler(h *SyncHandler) { + rh.sync = append(rh.sync, h) + sort.Sort(byFromTo(rh.sync)) +} + +func (rh *RootHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if wantsDiscovery(req) { + if auth.Allowed(req, auth.OpDiscovery) { + rh.serveDiscovery(rw, req) + return + } + if !rh.Stealth { + http.Error(rw, "Unauthorized", http.StatusUnauthorized) + } + return + } + + if rh.Stealth { + return + } + if req.RequestURI == "/" && rh.ui != nil { + http.Redirect(rw, req, "/ui/", http.StatusMovedPermanently) + return + } + if req.URL.Path == "/favicon.ico" { + ServeStaticFile(rw, req, Files, "favicon.ico") + return + } + f := func(p string, a ...interface{}) { + fmt.Fprintf(rw, p, a...) + } + f("

    This is camlistored (%s), a "+ + "Camlistore server.

    ", buildinfo.Version()) + if auth.IsLocalhost(req) && !env.IsDev() { + f("

    If you're coming from localhost, configure your Camlistore server at /setup.

    ") + } + if rh.ui != nil { + f("

    To manage your content, access the %s.

    ", rh.ui.prefix, rh.ui.prefix) + } + if rh.statusRoot != "" { + f("

    To view status, see %s.

    ", rh.statusRoot, rh.statusRoot) + } + if rh.helpRoot != "" { + f("

    To view more information on accessing the server, see %s.

    ", rh.helpRoot, rh.helpRoot) + } + fmt.Fprintf(rw, "") +} + +type byFromTo []*SyncHandler + +func (b byFromTo) Len() int { return len(b) } +func (b byFromTo) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b byFromTo) Less(i, j int) bool { + if b[i].fromName < b[j].fromName { + return true + } + return b[i].fromName == b[j].fromName && b[i].toName < b[j].toName +} + +func (rh *RootHandler) serveDiscovery(rw http.ResponseWriter, req *http.Request) { + d := &camtypes.Discovery{ + BlobRoot: rh.BlobRoot, + JSONSignRoot: rh.JSONSignRoot, + HelpRoot: rh.helpRoot, + ImporterRoot: rh.importerRoot, + SearchRoot: rh.SearchRoot, + StatusRoot: rh.statusRoot, + OwnerName: rh.OwnerName, + UserName: rh.Username, + WSAuthToken: auth.ProcessRandom(), + ThumbVersion: images.ThumbnailVersion(), + } + if gener, ok := rh.Storage.(blobserver.Generationer); ok { + initTime, gen, err := gener.StorageGeneration() + if err != nil { + d.StorageGenerationError = err.Error() + } else { + d.StorageInitTime = types.Time3339(initTime) + d.StorageGeneration = gen + } + } else { + log.Printf("Storage type %T is not a blobserver.Generationer; not sending storageGeneration", rh.Storage) + } + if rh.ui != nil { + d.UIDiscovery = rh.ui.discovery() + } + if rh.sigh != nil { + d.Signing = rh.sigh.Discovery(rh.JSONSignRoot) + } + if len(rh.sync) > 0 { + syncHandlers := make([]camtypes.SyncHandlerDiscovery, 0, len(rh.sync)) + for _, sh := range rh.sync { + syncHandlers = append(syncHandlers, sh.discovery()) + } + d.SyncHandlers = syncHandlers + } + discoveryHelper(rw, req, d) +} + +func discoveryHelper(rw http.ResponseWriter, req *http.Request, dr *camtypes.Discovery) { + rw.Header().Set("Content-Type", "text/javascript") + if cb := req.FormValue("cb"); identOrDotPattern.MatchString(cb) { + fmt.Fprintf(rw, "%s(", cb) + defer rw.Write([]byte(");\n")) + } else if v := req.FormValue("var"); identOrDotPattern.MatchString(v) { + fmt.Fprintf(rw, "%s = ", v) + defer rw.Write([]byte(";\n")) + } + bytes, err := json.MarshalIndent(dr, "", " ") + if err != nil { + httputil.ServeJSONError(rw, httputil.ServerError("encoding discovery information: "+err.Error())) + return + } + rw.Write(bytes) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/root_appengine.go b/vendor/github.com/camlistore/camlistore/pkg/server/root_appengine.go new file mode 100644 index 00000000..8de71211 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/root_appengine.go @@ -0,0 +1,24 @@ +// +build appengine + +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +func getUserName() (string, error) { + // TODO(mpl): use appengine specific stuff to do that + return "unknown on Appengine", nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/root_normal.go b/vendor/github.com/camlistore/camlistore/pkg/server/root_normal.go new file mode 100644 index 00000000..eca54c27 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/root_normal.go @@ -0,0 +1,36 @@ +// +build !appengine + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "os/user" + + "camlistore.org/pkg/osutil" +) + +func getUserName() (string, error) { + u, err := user.Current() + if err != nil { + if v := osutil.Username(); v != "" { + return v, nil + } + return "", err + } + return u.Name, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/share.go b/vendor/github.com/camlistore/camlistore/pkg/server/share.go new file mode 100644 index 00000000..ea58cce0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/share.go @@ -0,0 +1,250 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "strconv" + "strings" + "time" + + "camlistore.org/pkg/auth" + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/blobserver/gethandler" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/schema" +) + +type responseType int + +const ( + badRequest responseType = iota + unauthorizedRequest +) + +type errorCode int + +const ( + noError errorCode = iota + assembleNonTransitive + invalidMethod + invalidURL + invalidVia + shareBlobInvalid + shareBlobTooLarge + shareExpired + shareFetchFailed + shareReadFailed + shareTargetInvalid + shareNotTransitive + viaChainFetchFailed + viaChainInvalidLink + viaChainReadFailed +) + +type shareError struct { + code errorCode + response responseType + message string +} + +func (e *shareError) Error() string { + return e.message +} + +func unauthorized(code errorCode, format string, args ...interface{}) *shareError { + return &shareError{ + code: code, response: unauthorizedRequest, message: fmt.Sprintf(format, args...), + } +} + +const fetchFailureDelay = 200 * time.Millisecond + +// ShareHandler handles the requests for "share" (and shared) blobs. +type shareHandler struct { + fetcher blob.Fetcher +} + +func init() { + blobserver.RegisterHandlerConstructor("share", newShareFromConfig) +} + +func newShareFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) { + blobRoot := conf.RequiredString("blobRoot") + if blobRoot == "" { + return nil, errors.New("No blobRoot defined for share handler") + } + if err = conf.Validate(); err != nil { + return + } + + share := &shareHandler{} + bs, err := ld.GetStorage(blobRoot) + if err != nil { + return nil, fmt.Errorf("Share handler's blobRoot of %q error: %v", blobRoot, err) + } + fetcher, ok := bs.(blob.Fetcher) + if !ok { + return nil, errors.New("Share handler's storage not a Fetcher.") + } + share.fetcher = fetcher + return share, nil +} + +// Unauthenticated user. Be paranoid. +func handleGetViaSharing(conn http.ResponseWriter, req *http.Request, + blobRef blob.Ref, fetcher blob.Fetcher) error { + if !httputil.IsGet(req) { + return &shareError{code: invalidMethod, response: badRequest, message: "Invalid method"} + } + + conn.Header().Set("Access-Control-Allow-Origin", "*") + + viaPathOkay := false + startTime := time.Now() + defer func() { + if !viaPathOkay { + // Insert a delay, to hide timing attacks probing + // for the existence of blobs. + sleep := fetchFailureDelay - (time.Now().Sub(startTime)) + time.Sleep(sleep) + } + }() + viaBlobs := make([]blob.Ref, 0) + if via := req.FormValue("via"); via != "" { + for _, vs := range strings.Split(via, ",") { + if br, ok := blob.Parse(vs); ok { + viaBlobs = append(viaBlobs, br) + } else { + return &shareError{code: invalidVia, response: badRequest, message: "Malformed blobref in via param"} + } + } + } + + fetchChain := make([]blob.Ref, 0) + fetchChain = append(fetchChain, viaBlobs...) + fetchChain = append(fetchChain, blobRef) + isTransitive := false + for i, br := range fetchChain { + switch i { + case 0: + file, size, err := fetcher.Fetch(br) + if err != nil { + return unauthorized(shareFetchFailed, "Fetch chain 0 of %s failed: %v", br, err) + } + defer file.Close() + if size > schema.MaxSchemaBlobSize { + return unauthorized(shareBlobTooLarge, "Fetch chain 0 of %s too large", br) + } + blob, err := schema.BlobFromReader(br, file) + if err != nil { + return unauthorized(shareReadFailed, "Can't create a blob from %v: %v", br, err) + } + share, ok := blob.AsShare() + if !ok { + return unauthorized(shareBlobInvalid, "Fetch chain 0 of %s wasn't a valid Share", br) + } + if share.IsExpired() { + return unauthorized(shareExpired, "Share is expired") + } + if len(fetchChain) > 1 && fetchChain[1].String() != share.Target().String() { + return unauthorized(shareTargetInvalid, + "Fetch chain 0->1 (%s -> %q) unauthorized, expected hop to %q", + br, fetchChain[1], share.Target()) + } + isTransitive = share.IsTransitive() + if len(fetchChain) > 2 && !isTransitive { + return unauthorized(shareNotTransitive, "Share is not transitive") + } + case len(fetchChain) - 1: + // Last one is fine (as long as its path up to here has been proven, and it's + // not the first thing in the chain) + continue + default: + file, _, err := fetcher.Fetch(br) + if err != nil { + return unauthorized(viaChainFetchFailed, "Fetch chain %d of %s failed: %v", i, br, err) + } + defer file.Close() + lr := io.LimitReader(file, schema.MaxSchemaBlobSize) + slurpBytes, err := ioutil.ReadAll(lr) + if err != nil { + return unauthorized(viaChainReadFailed, + "Fetch chain %d of %s failed in slurp: %v", i, br, err) + } + saught := fetchChain[i+1].String() + if bytes.Index(slurpBytes, []byte(saught)) == -1 { + return unauthorized(viaChainInvalidLink, + "Fetch chain %d of %s failed; no reference to %s", i, br, saught) + } + } + } + + if assemble, _ := strconv.ParseBool(req.FormValue("assemble")); assemble { + if !isTransitive { + return unauthorized(assembleNonTransitive, "Cannot assemble non-transitive share") + } + dh := &DownloadHandler{ + Fetcher: fetcher, + // TODO(aa): It would be nice to specify a local cache here, as the UI handler does. + } + dh.ServeHTTP(conn, req, blobRef) + } else { + gethandler.ServeBlobRef(conn, req, blobRef, fetcher) + } + viaPathOkay = true + return nil +} + +func (h *shareHandler) serveHTTP(rw http.ResponseWriter, req *http.Request) error { + var err error + pathSuffix := httputil.PathSuffix(req) + if len(pathSuffix) == 0 { + // This happens during testing because we don't go through PrefixHandler + pathSuffix = strings.TrimLeft(req.URL.Path, "/") + } + pathParts := strings.SplitN(pathSuffix, "/", 2) + blobRef, ok := blob.Parse(pathParts[0]) + if !ok { + err = &shareError{code: invalidURL, response: badRequest, + message: fmt.Sprintf("Malformed share pathSuffix: %s", pathSuffix)} + } else { + err = handleGetViaSharing(rw, req, blobRef, h.fetcher) + } + if se, ok := err.(*shareError); ok { + switch se.response { + case badRequest: + httputil.BadRequestError(rw, err.Error()) + case unauthorizedRequest: + log.Print(err) + auth.SendUnauthorized(rw, req) + } + } + return err +} + +func (h *shareHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + h.serveHTTP(rw, req) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/share_test.go b/vendor/github.com/camlistore/camlistore/pkg/server/share_test.go new file mode 100644 index 00000000..1f31e602 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/share_test.go @@ -0,0 +1,116 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/test" +) + +func TestHandleGetViaSharing(t *testing.T) { + sto := &test.Fetcher{} + handler := &shareHandler{fetcher: sto} + var wr *httptest.ResponseRecorder + + putRaw := func(ref blob.Ref, data string) { + if _, err := blobserver.Receive(sto, ref, strings.NewReader(data)); err != nil { + t.Fatal(err) + } + } + + put := func(blob *schema.Blob) { + putRaw(blob.BlobRef(), blob.JSON()) + } + + get := func(path string) *shareError { + wr = httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://unused/"+path, nil) + err := handler.serveHTTP(wr, req) + if err != nil { + return err.(*shareError) + } + return nil + } + + testGet := func(path string, expectedError errorCode) { + err := get(path) + if expectedError != noError { + if err == nil || err.code != expectedError { + t.Errorf("Fetching %s, expected error %#v, but got %#v", path, expectedError, err) + } + } else { + if err != nil { + t.Errorf("Fetching %s, expected success but got %#v", path, err) + } + } + + if wr.HeaderMap.Get("Access-Control-Allow-Origin") != "*" { + t.Errorf("Fetching %s, share response did not contain expected CORS header", path) + } + } + + content := "monkey" + contentRef := blob.SHA1FromString(content) + + // For the purposes of following the via chain, the only thing that + // matters is that the content of each link contains the name of the + // next link. + link := contentRef.String() + linkRef := blob.SHA1FromString(link) + + share := schema.NewShareRef(schema.ShareHaveRef, false). + SetShareTarget(linkRef). + SetSigner(blob.SHA1FromString("irrelevant")). + SetRawStringField("camliSig", "alsounused") + + testGet(share.Blob().BlobRef().String(), shareFetchFailed) + + put(share.Blob()) + testGet(fmt.Sprintf("%s?via=%s", contentRef, share.Blob().BlobRef()), shareTargetInvalid) + + putRaw(linkRef, link) + testGet(linkRef.String(), shareReadFailed) + testGet(share.Blob().BlobRef().String(), noError) + testGet(fmt.Sprintf("%s?via=%s", linkRef, share.Blob().BlobRef()), noError) + testGet(fmt.Sprintf("%s?via=%s,%s", contentRef, share.Blob().BlobRef(), linkRef), shareNotTransitive) + + share.SetShareIsTransitive(true) + put(share.Blob()) + testGet(fmt.Sprintf("%s?via=%s,%s", linkRef, share.Blob().BlobRef(), linkRef), viaChainInvalidLink) + + putRaw(contentRef, content) + testGet(fmt.Sprintf("%s?via=%s,%s", contentRef, share.Blob().BlobRef(), linkRef), noError) + + share.SetShareExpiration(time.Now().Add(-time.Duration(10) * time.Minute)) + put(share.Blob()) + testGet(fmt.Sprintf("%s?via=%s,%s", contentRef, share.Blob().BlobRef(), linkRef), shareExpired) + + share.SetShareExpiration(time.Now().Add(time.Duration(10) * time.Minute)) + put(share.Blob()) + testGet(fmt.Sprintf("%s?via=%s,%s", contentRef, share.Blob().BlobRef(), linkRef), noError) + + // TODO(aa): assemble +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/status.go b/vendor/github.com/camlistore/camlistore/pkg/server/status.go new file mode 100644 index 00000000..978089c2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/status.go @@ -0,0 +1,297 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "encoding/json" + "fmt" + "html" + "io" + "log" + "net/http" + "os" + "reflect" + "regexp" + "runtime" + "strings" + "time" + + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/buildinfo" + "camlistore.org/pkg/env" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/index" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/search" + "camlistore.org/pkg/server/app" + "camlistore.org/pkg/types/camtypes" +) + +// StatusHandler publishes server status information. +type StatusHandler struct { + prefix string + handlerFinder blobserver.FindHandlerByTyper +} + +func init() { + blobserver.RegisterHandlerConstructor("status", newStatusFromConfig) +} + +var _ blobserver.HandlerIniter = (*StatusHandler)(nil) + +func newStatusFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) { + if err := conf.Validate(); err != nil { + return nil, err + } + return &StatusHandler{ + prefix: ld.MyPrefix(), + handlerFinder: ld, + }, nil +} + +func (sh *StatusHandler) InitHandler(hl blobserver.FindHandlerByTyper) error { + _, h, err := hl.FindHandlerByType("search") + if err == blobserver.ErrHandlerTypeNotFound { + return nil + } + if err != nil { + return err + } + go func() { + var lastSend *status + for { + cur := sh.currentStatus() + if reflect.DeepEqual(cur, lastSend) { + // TODO: something better. get notified on interesting events. + time.Sleep(10 * time.Second) + continue + } + lastSend = cur + js, _ := json.MarshalIndent(cur, "", " ") + h.(*search.Handler).SendStatusUpdate(js) + } + }() + return nil +} + +func (sh *StatusHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + suffix := httputil.PathSuffix(req) + if suffix == "restart" { + sh.serveRestart(rw, req) + return + } + if !httputil.IsGet(req) { + http.Error(rw, "Illegal status method.", http.StatusMethodNotAllowed) + return + } + switch suffix { + case "status.json": + sh.serveStatusJSON(rw, req) + case "": + sh.serveStatusHTML(rw, req) + default: + http.Error(rw, "Illegal status path.", http.StatusNotFound) + } +} + +type status struct { + Version string `json:"version"` + Errors []camtypes.StatusError `json:"errors,omitempty"` + Sync map[string]syncStatus `json:"sync"` + Storage map[string]storageStatus `json:"storage"` + importerRoot string + rootPrefix string + + ImporterAccounts interface{} `json:"importerAccounts"` +} + +func (st *status) addError(msg, url string) { + st.Errors = append(st.Errors, camtypes.StatusError{ + Error: msg, + URL: url, + }) +} + +func (st *status) isHandler(pfx string) bool { + if pfx == st.importerRoot { + return true + } + if _, ok := st.Sync[pfx]; ok { + return true + } + if _, ok := st.Storage[pfx]; ok { + return true + } + return false +} + +type storageStatus struct { + Primary bool `json:"primary,omitempty"` + IsIndex bool `json:"isIndex,omitempty"` + Type string `json:"type"` + ApproxBlobs int `json:"approxBlobs,omitempty"` + ApproxBytes int `json:"approxBytes,omitempty"` + ImplStatus interface{} `json:"implStatus,omitempty"` +} + +func (sh *StatusHandler) currentStatus() *status { + res := &status{ + Version: buildinfo.Version(), + Storage: make(map[string]storageStatus), + Sync: make(map[string]syncStatus), + } + if v := os.Getenv("CAMLI_FAKE_STATUS_ERROR"); v != "" { + res.addError(v, "/status/#fakeerror") + } + _, hi, err := sh.handlerFinder.FindHandlerByType("root") + if err != nil { + res.addError(fmt.Sprintf("Error finding root handler: %v", err), "") + return res + } + rh := hi.(*RootHandler) + res.rootPrefix = rh.Prefix + + if pfx, h, err := sh.handlerFinder.FindHandlerByType("importer"); err == nil { + res.importerRoot = pfx + as := h.(interface { + AccountsStatus() (interface{}, []camtypes.StatusError) + }) + var errs []camtypes.StatusError + res.ImporterAccounts, errs = as.AccountsStatus() + res.Errors = append(res.Errors, errs...) + } + + types, handlers := sh.handlerFinder.AllHandlers() + + // Sync + for pfx, h := range handlers { + sh, ok := h.(*SyncHandler) + if !ok { + continue + } + res.Sync[pfx] = sh.currentStatus() + } + + // Storage + for pfx, typ := range types { + if !strings.HasPrefix(typ, "storage-") { + continue + } + h := handlers[pfx] + _, isIndex := h.(*index.Index) + res.Storage[pfx] = storageStatus{ + Type: strings.TrimPrefix(typ, "storage-"), + Primary: pfx == rh.BlobRoot, + IsIndex: isIndex, + } + } + + return res +} + +func (sh *StatusHandler) serveStatusJSON(rw http.ResponseWriter, req *http.Request) { + httputil.ReturnJSON(rw, sh.currentStatus()) +} + +var quotedPrefix = regexp.MustCompile(`[;"]/(\S+?/)[&"]`) + +func (sh *StatusHandler) serveStatusHTML(rw http.ResponseWriter, req *http.Request) { + st := sh.currentStatus() + f := func(p string, a ...interface{}) { + if len(a) == 0 { + io.WriteString(rw, p) + } else { + fmt.Fprintf(rw, p, a...) + } + } + f("camlistored status") + f("") + + f("

    camlistored status

    ") + + f("

    Versions

      ") + var envStr string + if env.OnGCE() { + envStr = " (on GCE)" + } + f("
    • Camlistore: %s%s
    • ", html.EscapeString(buildinfo.Version()), envStr) + f("
    • Go: %s/%s %s, cgo=%v
    • ", runtime.GOOS, runtime.GOARCH, runtime.Version(), cgoEnabled) + f("
    • djpeg: %s", html.EscapeString(buildinfo.DjpegStatus())) + f("
    ") + + f("

    Logs

    ") + + f("

    Admin

    ") + f("
    ") + + f("

    Handlers

    ") + f("

    As JSON: status.json; and the discovery JSON.

    ", st.rootPrefix) + f("

    Not yet pretty HTML UI:

    ") + js, err := json.MarshalIndent(st, "", " ") + if err != nil { + log.Printf("JSON marshal error: %v", err) + } + jsh := html.EscapeString(string(js)) + jsh = quotedPrefix.ReplaceAllStringFunc(jsh, func(in string) string { + pfx := in[1 : len(in)-1] + if st.isHandler(pfx) { + return fmt.Sprintf("%s%s%s", in[:1], pfx, pfx, in[len(in)-1:]) + } + return in + }) + f("
    %s
    ", jsh) +} + +func (sh *StatusHandler) serveRestart(rw http.ResponseWriter, req *http.Request) { + if req.Method != "POST" { + http.Error(rw, "POST to restart", http.StatusMethodNotAllowed) + return + } + + _, handlers := sh.handlerFinder.AllHandlers() + for _, h := range handlers { + ah, ok := h.(*app.Handler) + if !ok { + continue + } + log.Printf("Sending SIGINT to %s", ah.ProgramName()) + err := ah.Quit() + if err != nil { + msg := fmt.Sprintf("Not restarting: couldn't interrupt app %s: %v", ah.ProgramName(), err) + log.Printf(msg) + http.Error(rw, msg, http.StatusInternalServerError) + return + } + } + + log.Println("Restarting camlistored") + rw.Header().Set("Connection", "close") + http.Redirect(rw, req, sh.prefix, http.StatusFound) + if f, ok := rw.(http.Flusher); ok { + f.Flush() + } + osutil.RestartProcess() +} + +var cgoEnabled bool diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/sync.go b/vendor/github.com/camlistore/camlistore/pkg/server/sync.go new file mode 100644 index 00000000..4241ff62 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/sync.go @@ -0,0 +1,1013 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "bytes" + "errors" + "fmt" + "html" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "sort" + "strconv" + "strings" + "sync" + "time" + + "camlistore.org/pkg/auth" + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/constants" + "camlistore.org/pkg/context" + "camlistore.org/pkg/index" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/syncutil" + "camlistore.org/pkg/types/camtypes" + "camlistore.org/third_party/code.google.com/p/xsrftoken" +) + +const ( + maxRecentErrors = 20 + queueSyncInterval = 5 * time.Second +) + +type blobReceiverEnumerator interface { + blobserver.BlobReceiver + blobserver.BlobEnumerator +} + +// The SyncHandler handles async replication in one direction between +// a pair storage targets, a source and target. +// +// SyncHandler is a BlobReceiver but doesn't actually store incoming +// blobs; instead, it records blobs it has received and queues them +// for async replication soon, or whenever it can. +type SyncHandler struct { + // TODO: rate control tunables + fromName, toName string + from blobserver.Storage + to blobReceiverEnumerator + queue sorted.KeyValue + toIndex bool // whether this sync is from a blob storage to an index + idle bool // if true, the handler does nothing other than providing the discovery. + copierPoolSize int + + // wakec wakes up the blob syncer loop when a blob is received. + wakec chan bool + + mu sync.Mutex // protects following + status string + copying map[blob.Ref]*copyStatus // to start time + needCopy map[blob.Ref]uint32 // blobs needing to be copied. some might be in lastFail too. + lastFail map[blob.Ref]failDetail // subset of needCopy that previously failed, and why + bytesRemain int64 // sum of needCopy values + recentErrors []blob.Ref // up to maxRecentErrors, recent first. valid if still in lastFail. + recentCopyTime time.Time + totalCopies int64 + totalCopyBytes int64 + totalErrors int64 + vshards []string // validation shards. if 0, validation not running + vshardDone int // shards validated + vshardErrs []string + vmissing int64 // missing blobs found during validat + vdestCount int // number of blobs seen on dest during validate + vdestBytes int64 // number of blob bytes seen on dest during validate + vsrcCount int // number of blobs seen on src during validate + vsrcBytes int64 // number of blob bytes seen on src during validate +} + +var ( + _ blobserver.Storage = (*SyncHandler)(nil) + _ blobserver.HandlerIniter = (*SyncHandler)(nil) +) + +func (sh *SyncHandler) String() string { + return fmt.Sprintf("[SyncHandler %v -> %v]", sh.fromName, sh.toName) +} + +func (sh *SyncHandler) logf(format string, args ...interface{}) { + log.Printf(sh.String()+" "+format, args...) +} + +func init() { + blobserver.RegisterHandlerConstructor("sync", newSyncFromConfig) +} + +// TODO: this is is temporary. should delete, or decide when it's on by default (probably always). +// Then need genconfig option to disable it. +var validateOnStartDefault, _ = strconv.ParseBool(os.Getenv("CAMLI_SYNC_VALIDATE")) + +func newSyncFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Handler, error) { + var ( + from = conf.RequiredString("from") + to = conf.RequiredString("to") + fullSync = conf.OptionalBool("fullSyncOnStart", false) + blockFullSync = conf.OptionalBool("blockingFullSyncOnStart", false) + idle = conf.OptionalBool("idle", false) + queueConf = conf.OptionalObject("queue") + copierPoolSize = conf.OptionalInt("copierPoolSize", 5) + validate = conf.OptionalBool("validateOnStart", validateOnStartDefault) + ) + if err := conf.Validate(); err != nil { + return nil, err + } + if idle { + return newIdleSyncHandler(from, to), nil + } + if len(queueConf) == 0 { + return nil, errors.New(`Missing required "queue" object`) + } + q, err := sorted.NewKeyValue(queueConf) + if err != nil { + return nil, err + } + + isToIndex := false + fromBs, err := ld.GetStorage(from) + if err != nil { + return nil, err + } + toBs, err := ld.GetStorage(to) + if err != nil { + return nil, err + } + if _, ok := fromBs.(*index.Index); !ok { + if _, ok := toBs.(*index.Index); ok { + isToIndex = true + } + } + + sh := newSyncHandler(from, to, fromBs, toBs, q) + sh.toIndex = isToIndex + sh.copierPoolSize = copierPoolSize + if err := sh.readQueueToMemory(); err != nil { + return nil, fmt.Errorf("Error reading sync queue to memory: %v", err) + } + + if fullSync || blockFullSync { + sh.logf("Doing full sync") + didFullSync := make(chan bool, 1) + go func() { + for { + n := sh.runSync("queue", sh.enumeratePendingBlobs) + if n > 0 { + sh.logf("Queue sync copied %d blobs", n) + continue + } + break + } + n := sh.runSync("full", blobserverEnumerator(context.TODO(), fromBs)) + sh.logf("Full sync copied %d blobs", n) + didFullSync <- true + sh.syncLoop() + }() + if blockFullSync { + sh.logf("Blocking startup, waiting for full sync from %q to %q", from, to) + <-didFullSync + sh.logf("Full sync complete.") + } + } else { + go sh.syncLoop() + } + + if validate { + go sh.startFullValidation() + } + + blobserver.GetHub(fromBs).AddReceiveHook(sh.enqueue) + return sh, nil +} + +func (sh *SyncHandler) InitHandler(hl blobserver.FindHandlerByTyper) error { + _, h, err := hl.FindHandlerByType("root") + if err == blobserver.ErrHandlerTypeNotFound { + // It's optional. We register ourselves if it's there. + return nil + } + if err != nil { + return err + } + h.(*RootHandler).registerSyncHandler(sh) + return nil +} + +func newSyncHandler(fromName, toName string, + from blobserver.Storage, to blobReceiverEnumerator, + queue sorted.KeyValue) *SyncHandler { + return &SyncHandler{ + copierPoolSize: 2, + from: from, + to: to, + fromName: fromName, + toName: toName, + queue: queue, + wakec: make(chan bool), + status: "not started", + needCopy: make(map[blob.Ref]uint32), + lastFail: make(map[blob.Ref]failDetail), + copying: make(map[blob.Ref]*copyStatus), + } +} + +func newIdleSyncHandler(fromName, toName string) *SyncHandler { + return &SyncHandler{ + fromName: fromName, + toName: toName, + idle: true, + status: "disabled", + } +} + +func (sh *SyncHandler) discovery() camtypes.SyncHandlerDiscovery { + return camtypes.SyncHandlerDiscovery{ + From: sh.fromName, + To: sh.toName, + ToIndex: sh.toIndex, + } +} + +// syncStatus is a snapshot of the current status, for display by the +// status handler (status.go) in both JSON and HTML forms. +type syncStatus struct { + sh *SyncHandler + + From string `json:"from"` + FromDesc string `json:"fromDesc"` + To string `json:"to"` + ToDesc string `json:"toDesc"` + DestIsIndex bool `json:"destIsIndex,omitempty"` + BlobsToCopy int `json:"blobsToCopy"` + BytesToCopy int64 `json:"bytesToCopy"` + LastCopySecAgo int `json:"lastCopySecondsAgo,omitempty"` +} + +func (sh *SyncHandler) currentStatus() syncStatus { + sh.mu.Lock() + defer sh.mu.Unlock() + ago := 0 + if !sh.recentCopyTime.IsZero() { + ago = int(time.Now().Sub(sh.recentCopyTime).Seconds()) + } + return syncStatus{ + sh: sh, + From: sh.fromName, + FromDesc: storageDesc(sh.from), + To: sh.toName, + ToDesc: storageDesc(sh.to), + DestIsIndex: sh.toIndex, + BlobsToCopy: len(sh.needCopy), + BytesToCopy: sh.bytesRemain, + LastCopySecAgo: ago, + } +} + +// readQueueToMemory slurps in the pending queue from disk (or +// wherever) to memory. Even with millions of blobs, it's not much +// memory. The point of the persistent queue is to survive restarts if +// the "fullSyncOnStart" option is off. With "fullSyncOnStart" set to +// true, this is a little pointless (we'd figure out what's missing +// eventually), but this might save us a few minutes (let us start +// syncing missing blobs a few minutes earlier) since we won't have to +// wait to figure out what the destination is missing. +func (sh *SyncHandler) readQueueToMemory() error { + errc := make(chan error, 1) + blobs := make(chan blob.SizedRef, 16) + intr := make(chan struct{}) + defer close(intr) + go func() { + errc <- sh.enumerateQueuedBlobs(blobs, intr) + }() + n := 0 + for sb := range blobs { + sh.addBlobToCopy(sb) + n++ + } + sh.logf("Added %d pending blobs from sync queue to pending list", n) + return <-errc +} + +func (sh *SyncHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if req.Method == "POST" { + if req.FormValue("mode") == "validate" { + token := req.FormValue("token") + if xsrftoken.Valid(token, auth.ProcessRandom(), "user", "runFullValidate") { + sh.startFullValidation() + http.Redirect(rw, req, "./", http.StatusFound) + return + } + } + http.Error(rw, "Bad POST request", http.StatusBadRequest) + return + } + + // TODO: remove this lock and instead just call currentStatus, + // and transition to using that here. + sh.mu.Lock() + defer sh.mu.Unlock() + f := func(p string, a ...interface{}) { + fmt.Fprintf(rw, p, a...) + } + now := time.Now() + f("

    Sync Status (for %s to %s)

    ", sh.fromName, sh.toName) + f("

    Current status: %s

    ", html.EscapeString(sh.status)) + if sh.idle { + return + } + + f("

    Stats:

      ") + f("
    • Source: %s
    • ", html.EscapeString(storageDesc(sh.from))) + f("
    • Target: %s
    • ", html.EscapeString(storageDesc(sh.to))) + f("
    • Blobs synced: %d
    • ", sh.totalCopies) + f("
    • Bytes synced: %d
    • ", sh.totalCopyBytes) + f("
    • Blobs yet to copy: %d
    • ", len(sh.needCopy)) + f("
    • Bytes yet to copy: %d
    • ", sh.bytesRemain) + if !sh.recentCopyTime.IsZero() { + f("
    • Most recent copy: %s (%v ago)
    • ", sh.recentCopyTime.Format(time.RFC3339), now.Sub(sh.recentCopyTime)) + } + clarification := "" + if len(sh.needCopy) == 0 && sh.totalErrors > 0 { + clarification = "(all since resolved)" + } + f("
    • Previous copy errors: %d %s
    • ", sh.totalErrors, clarification) + f("
    ") + + f("

    Validation

    ") + if len(sh.vshards) == 0 { + f("Validation disabled") + token := xsrftoken.Generate(auth.ProcessRandom(), "user", "runFullValidate") + f("
    ", token) + } else { + f("

    Background scan of source and destination to ensure that the destination has everything the source does, or is at least enqueued to sync.

    ") + f("
      ") + f("
    • Shards complete: %d/%d (%.1f%%)
    • ", + sh.vshardDone, + len(sh.vshards), + 100*float64(sh.vshardDone)/float64(len(sh.vshards))) + f("
    • Source blobs seen: %d
    • ", sh.vsrcCount) + f("
    • Source bytes seen: %d
    • ", sh.vsrcBytes) + f("
    • Dest blobs seen: %d
    • ", sh.vdestCount) + f("
    • Dest bytes seen: %d
    • ", sh.vdestBytes) + f("
    • Blobs found missing & enqueued: %d
    • ", sh.vmissing) + if len(sh.vshardErrs) > 0 { + f("
    • Validation errors: %s
    • ", sh.vshardErrs) + } + f("
    ") + } + + if len(sh.copying) > 0 { + f("

    Currently Copying

      ") + copying := make([]blob.Ref, 0, len(sh.copying)) + for br := range sh.copying { + copying = append(copying, br) + } + sort.Sort(blob.ByRef(copying)) + for _, br := range copying { + f("
    • %s
    • \n", sh.copying[br]) + } + f("
    ") + } + + recentErrors := make([]blob.Ref, 0, len(sh.recentErrors)) + for _, br := range sh.recentErrors { + if _, ok := sh.needCopy[br]; ok { + // Only show it in the web UI if it's still a problem. Blobs that + // have since succeeded just confused people. + recentErrors = append(recentErrors, br) + } + } + if len(recentErrors) > 0 { + f("

    Recent Errors

    Blobs that haven't successfully copied over yet, and their last errors:

      ") + for _, br := range recentErrors { + fail := sh.lastFail[br] + f("
    • %s: %s: %s
    • \n", + br, + fail.when.Format(time.RFC3339), + html.EscapeString(fail.err.Error())) + } + f("
    ") + } +} + +func (sh *SyncHandler) setStatusf(s string, args ...interface{}) { + s = time.Now().UTC().Format(time.RFC3339) + ": " + fmt.Sprintf(s, args...) + sh.mu.Lock() + defer sh.mu.Unlock() + sh.status = s +} + +type copyResult struct { + sb blob.SizedRef + err error +} + +func blobserverEnumerator(ctx *context.Context, src blobserver.BlobEnumerator) func(chan<- blob.SizedRef, <-chan struct{}) error { + return func(dst chan<- blob.SizedRef, intr <-chan struct{}) error { + return blobserver.EnumerateAll(ctx, src, func(sb blob.SizedRef) error { + select { + case dst <- sb: + case <-intr: + return errors.New("interrupted") + } + return nil + }) + } +} + +// enumeratePendingBlobs yields blobs from the in-memory pending list (needCopy). +// This differs from enumerateQueuedBlobs, which pulls in the on-disk sorted.KeyValue store. +func (sh *SyncHandler) enumeratePendingBlobs(dst chan<- blob.SizedRef, intr <-chan struct{}) error { + defer close(dst) + sh.mu.Lock() + var toSend []blob.SizedRef + { + n := len(sh.needCopy) + const maxBatch = 1000 + if n > maxBatch { + n = maxBatch + } + toSend = make([]blob.SizedRef, 0, n) + for br, size := range sh.needCopy { + toSend = append(toSend, blob.SizedRef{br, size}) + if len(toSend) == n { + break + } + } + } + sh.mu.Unlock() + for _, sb := range toSend { + select { + case dst <- sb: + case <-intr: + return nil + } + } + return nil +} + +// enumerateQueuedBlobs yields blobs from the on-disk sorted.KeyValue store. +// This differs from enumeratePendingBlobs, which sends from the in-memory pending list. +func (sh *SyncHandler) enumerateQueuedBlobs(dst chan<- blob.SizedRef, intr <-chan struct{}) error { + defer close(dst) + it := sh.queue.Find("", "") + for it.Next() { + br, ok := blob.Parse(it.Key()) + size, err := strconv.ParseUint(it.Value(), 10, 32) + if !ok || err != nil { + sh.logf("ERROR: bogus sync queue entry: %q => %q", it.Key(), it.Value()) + continue + } + select { + case dst <- blob.SizedRef{br, uint32(size)}: + case <-intr: + return it.Close() + } + } + return it.Close() +} + +func (sh *SyncHandler) runSync(srcName string, enumSrc func(chan<- blob.SizedRef, <-chan struct{}) error) int { + enumch := make(chan blob.SizedRef, 8) + errch := make(chan error, 1) + intr := make(chan struct{}) + defer close(intr) + go func() { errch <- enumSrc(enumch, intr) }() + + nCopied := 0 + toCopy := 0 + + workch := make(chan blob.SizedRef, 1000) + resch := make(chan copyResult, 8) +FeedWork: + for sb := range enumch { + if toCopy < sh.copierPoolSize { + go sh.copyWorker(resch, workch) + } + select { + case workch <- sb: + toCopy++ + default: + // Buffer full. Enough for this batch. Will get it later. + break FeedWork + } + } + close(workch) + for i := 0; i < toCopy; i++ { + sh.setStatusf("Copying blobs") + res := <-resch + if res.err == nil { + nCopied++ + } + } + + if err := <-errch; err != nil { + sh.logf("error enumerating from source: %v", err) + } + return nCopied +} + +func (sh *SyncHandler) syncLoop() { + for { + t0 := time.Now() + + for sh.runSync(sh.fromName, sh.enumeratePendingBlobs) > 0 { + // Loop, before sleeping. + } + sh.setStatusf("Sleeping briefly before next long poll.") + + d := queueSyncInterval - time.Since(t0) + select { + case <-time.After(d): + case <-sh.wakec: + } + } +} + +func (sh *SyncHandler) copyWorker(res chan<- copyResult, work <-chan blob.SizedRef) { + for sb := range work { + res <- copyResult{sb, sh.copyBlob(sb)} + } +} + +func (sh *SyncHandler) copyBlob(sb blob.SizedRef) (err error) { + cs := sh.newCopyStatus(sb) + defer func() { cs.setError(err) }() + br := sb.Ref + + sh.mu.Lock() + sh.copying[br] = cs + sh.mu.Unlock() + + if sb.Size > constants.MaxBlobSize { + return fmt.Errorf("blob size %d too large; max blob size is %d", sb.Size, constants.MaxBlobSize) + } + + cs.setStatus(statusFetching) + rc, fromSize, err := sh.from.Fetch(br) + if err != nil { + return fmt.Errorf("source fetch: %v", err) + } + if fromSize != sb.Size { + rc.Close() + return fmt.Errorf("source fetch size mismatch: get=%d, enumerate=%d", fromSize, sb.Size) + } + + buf := make([]byte, fromSize) + hash := br.Hash() + cs.setStatus(statusReading) + n, err := io.ReadFull(io.TeeReader(rc, + io.MultiWriter( + incrWriter{cs, &cs.nread}, + hash, + )), buf) + rc.Close() + if err != nil { + return fmt.Errorf("Read error after %d/%d bytes: %v", n, fromSize, err) + } + if !br.HashMatches(hash) { + return fmt.Errorf("Read data has unexpected digest %x", hash.Sum(nil)) + } + + cs.setStatus(statusWriting) + newsb, err := sh.to.ReceiveBlob(br, io.TeeReader(bytes.NewReader(buf), incrWriter{cs, &cs.nwrite})) + if err != nil { + return fmt.Errorf("dest write: %v", err) + } + if newsb.Size != sb.Size { + return fmt.Errorf("write size mismatch: source_read=%d but dest_write=%d", sb.Size, newsb.Size) + } + return nil +} + +func (sh *SyncHandler) ReceiveBlob(br blob.Ref, r io.Reader) (sb blob.SizedRef, err error) { + n, err := io.Copy(ioutil.Discard, r) + if err != nil { + return + } + sb = blob.SizedRef{br, uint32(n)} + return sb, sh.enqueue(sb) +} + +// addBlobToCopy adds a blob to copy to memory (not to disk: that's enqueue). +// It returns true if it was added, or false if it was a duplicate. +func (sh *SyncHandler) addBlobToCopy(sb blob.SizedRef) bool { + sh.mu.Lock() + defer sh.mu.Unlock() + if _, dup := sh.needCopy[sb.Ref]; dup { + return false + } + + sh.needCopy[sb.Ref] = sb.Size + sh.bytesRemain += int64(sb.Size) + + // Non-blocking send to wake up looping goroutine if it's + // sleeping... + select { + case sh.wakec <- true: + default: + } + return true +} + +func (sh *SyncHandler) enqueue(sb blob.SizedRef) error { + if !sh.addBlobToCopy(sb) { + // Dup + return nil + } + // TODO: include current time in encoded value, to attempt to + // do in-order delivery to remote side later? Possible + // friendly optimization later. Might help peer's indexer have + // less missing deps. + if err := sh.queue.Set(sb.Ref.String(), fmt.Sprint(sb.Size)); err != nil { + return err + } + return nil +} + +func (sh *SyncHandler) startFullValidation() { + sh.mu.Lock() + if len(sh.vshards) != 0 { + sh.mu.Unlock() + return + } + sh.mu.Unlock() + + sh.logf("Running full validation; determining validation shards...") + shards := sh.shardPrefixes() + + sh.mu.Lock() + if len(sh.vshards) != 0 { + sh.mu.Unlock() + return + } + sh.vshards = shards + sh.mu.Unlock() + + go sh.runFullValidation() +} + +func (sh *SyncHandler) runFullValidation() { + var wg sync.WaitGroup + + sh.mu.Lock() + shards := sh.vshards + wg.Add(len(shards)) + sh.mu.Unlock() + + sh.logf("full validation beginning with %d shards", len(shards)) + + const maxShardWorkers = 30 // arbitrary + gate := syncutil.NewGate(maxShardWorkers) + + for _, pfx := range shards { + pfx := pfx + gate.Start() + go func() { + wg.Done() + defer gate.Done() + sh.validateShardPrefix(pfx) + }() + } + wg.Wait() + sh.logf("Validation complete") +} + +func (sh *SyncHandler) validateShardPrefix(pfx string) (err error) { + defer func() { + sh.mu.Lock() + if err != nil { + errs := fmt.Sprintf("Failed to validate prefix %s: %v", pfx, err) + sh.logf("%s", errs) + sh.vshardErrs = append(sh.vshardErrs, errs) + } else { + sh.vshardDone++ + } + sh.mu.Unlock() + }() + ctx := context.New() + defer ctx.Cancel() + src, serrc := sh.startValidatePrefix(ctx, pfx, false) + dst, derrc := sh.startValidatePrefix(ctx, pfx, true) + srcErr := &chanError{ + C: serrc, + Wrap: func(err error) error { + return fmt.Errorf("Error enumerating source %s for validating shard %s: %v", sh.fromName, pfx, err) + }, + } + dstErr := &chanError{ + C: derrc, + Wrap: func(err error) error { + return fmt.Errorf("Error enumerating target %s for validating shard %s: %v", sh.toName, pfx, err) + }, + } + + missingc := make(chan blob.SizedRef, 8) + go blobserver.ListMissingDestinationBlobs(missingc, func(blob.Ref) {}, src, dst) + + var missing []blob.SizedRef + for sb := range missingc { + missing = append(missing, sb) + } + + if err := srcErr.Get(); err != nil { + return err + } + if err := dstErr.Get(); err != nil { + return err + } + + for _, sb := range missing { + if enqErr := sh.enqueue(sb); enqErr != nil { + if err == nil { + err = enqErr + } + } else { + sh.mu.Lock() + sh.vmissing += 1 + sh.mu.Unlock() + } + } + return err +} + +var errNotPrefix = errors.New("sentinel error: hit blob into the next shard") + +// doDest is false for source and true for dest. +func (sh *SyncHandler) startValidatePrefix(ctx *context.Context, pfx string, doDest bool) (<-chan blob.SizedRef, <-chan error) { + var e blobserver.BlobEnumerator + if doDest { + e = sh.to + } else { + e = sh.from + } + c := make(chan blob.SizedRef, 64) + errc := make(chan error, 1) + go func() { + defer close(c) + var last string // last blobref seen; to double check storage's enumeration works correctly. + err := blobserver.EnumerateAllFrom(ctx, e, pfx, func(sb blob.SizedRef) error { + // Just double-check that the storage target is returning sorted results correctly. + brStr := sb.Ref.String() + if brStr < pfx { + log.Fatalf("Storage target %T enumerate not behaving: %q < requested prefix %q", e, brStr, pfx) + } + if last != "" && last >= brStr { + log.Fatalf("Storage target %T enumerate not behaving: previous %q >= current %q", e, last, brStr) + } + last = brStr + + // TODO: could add a more efficient method on blob.Ref to do this, + // that doesn't involve call String(). + if !strings.HasPrefix(brStr, pfx) { + return errNotPrefix + } + select { + case c <- sb: + sh.mu.Lock() + if doDest { + sh.vdestCount++ + sh.vdestBytes += int64(sb.Size) + } else { + sh.vsrcCount++ + sh.vsrcBytes += int64(sb.Size) + } + sh.mu.Unlock() + return nil + case <-ctx.Done(): + return context.ErrCanceled + } + }) + if err == errNotPrefix { + err = nil + } + if err != nil { + // Send a zero value to shut down ListMissingDestinationBlobs. + c <- blob.SizedRef{} + } + errc <- err + }() + return c, errc +} + +func (sh *SyncHandler) shardPrefixes() []string { + var pfx []string + // TODO(bradfitz): do limit=1 enumerates against sh.from and sh.to with varying + // "after" values to determine all the blobref types on both sides. + // For now, be lazy and assume only sha1: + for i := 0; i < 256; i++ { + pfx = append(pfx, fmt.Sprintf("sha1-%02x", i)) + } + return pfx +} + +func (sh *SyncHandler) newCopyStatus(sb blob.SizedRef) *copyStatus { + now := time.Now() + return ©Status{ + sh: sh, + sb: sb, + state: statusStarting, + start: now, + t: now, + } +} + +// copyStatus is an in-progress copy. +type copyStatus struct { + sh *SyncHandler + sb blob.SizedRef + start time.Time + + mu sync.Mutex + state string // one of statusFoo, below + t time.Time // last status update time + nread uint32 + nwrite uint32 +} + +const ( + statusStarting = "starting" + statusFetching = "fetching source" + statusReading = "reading" + statusWriting = "writing" +) + +func (cs *copyStatus) setStatus(s string) { + now := time.Now() + cs.mu.Lock() + defer cs.mu.Unlock() + cs.state = s + cs.t = now +} + +func (cs *copyStatus) setError(err error) { + now := time.Now() + sh := cs.sh + br := cs.sb.Ref + if err == nil { + // This is somewhat slow, so do it before we acquire the lock. + // The queue is thread-safe. + if derr := sh.queue.Delete(br.String()); derr != nil { + sh.logf("queue delete of %v error: %v", cs.sb.Ref, derr) + } + } + + sh.mu.Lock() + defer sh.mu.Unlock() + if _, needCopy := sh.needCopy[br]; !needCopy { + sh.logf("IGNORING DUPLICATE UPLOAD of %v = %v", br, err) + return + } + delete(sh.copying, br) + if err == nil { + delete(sh.needCopy, br) + delete(sh.lastFail, br) + sh.recentCopyTime = now + sh.totalCopies++ + sh.totalCopyBytes += int64(cs.sb.Size) + sh.bytesRemain -= int64(cs.sb.Size) + return + } + + sh.totalErrors++ + sh.logf("error copying %v: %v", br, err) + sh.lastFail[br] = failDetail{ + when: now, + err: err, + } + + // Kinda lame. TODO: use a ring buffer or container/list instead. + if len(sh.recentErrors) == maxRecentErrors { + copy(sh.recentErrors[1:], sh.recentErrors) + sh.recentErrors = sh.recentErrors[:maxRecentErrors-1] + } + sh.recentErrors = append(sh.recentErrors, br) +} + +func (cs *copyStatus) String() string { + var buf bytes.Buffer + now := time.Now() + buf.WriteString(cs.sb.Ref.String()) + buf.WriteString(": ") + + cs.mu.Lock() + defer cs.mu.Unlock() + sinceStart := now.Sub(cs.start) + sinceLast := now.Sub(cs.t) + + switch cs.state { + case statusReading: + buf.WriteString(cs.state) + fmt.Fprintf(&buf, " (%d/%dB)", cs.nread, cs.sb.Size) + case statusWriting: + if cs.nwrite == cs.sb.Size { + buf.WriteString("wrote all, waiting ack") + } else { + buf.WriteString(cs.state) + fmt.Fprintf(&buf, " (%d/%dB)", cs.nwrite, cs.sb.Size) + } + default: + buf.WriteString(cs.state) + + } + if sinceLast > 5*time.Second { + fmt.Fprintf(&buf, ", last change %v ago (total elapsed %v)", sinceLast, sinceStart) + } + return buf.String() +} + +type failDetail struct { + when time.Time + err error +} + +// incrWriter is an io.Writer that locks mu and increments *n. +type incrWriter struct { + cs *copyStatus + n *uint32 +} + +func (w incrWriter) Write(p []byte) (n int, err error) { + w.cs.mu.Lock() + *w.n += uint32(len(p)) + w.cs.t = time.Now() + w.cs.mu.Unlock() + return len(p), nil +} + +func storageDesc(v interface{}) string { + if s, ok := v.(fmt.Stringer); ok { + return s.String() + } + return fmt.Sprintf("%T", v) +} + +// TODO(bradfitz): implement these? what do they mean? possibilities: +// a) proxy to sh.from +// b) proxy to sh.to +// c) merge intersection of sh.from, sh.to, and sh.queue: that is, a blob this pair +// currently or eventually will have. The only missing blob would be one that +// sh.from has, sh.to doesn't have, and isn't in the queue to be replicated. +// +// For now, don't implement them. Wait until we need them. + +func (sh *SyncHandler) Fetch(blob.Ref) (file io.ReadCloser, size uint32, err error) { + panic("Unimplemeted blobserver.Fetch called") +} + +func (sh *SyncHandler) StatBlobs(dest chan<- blob.SizedRef, blobs []blob.Ref) error { + sh.logf("Unexpected StatBlobs call") + return nil +} + +func (sh *SyncHandler) EnumerateBlobs(ctx *context.Context, dest chan<- blob.SizedRef, after string, limit int) error { + defer close(dest) + sh.logf("Unexpected EnumerateBlobs call") + return nil +} + +func (sh *SyncHandler) RemoveBlobs(blobs []blob.Ref) error { + panic("Unimplemeted RemoveBlobs") +} + +// chanError is a Future around an incoming error channel of one item. +// It can also wrap its error in something more descriptive. +type chanError struct { + C <-chan error + Wrap func(error) error // optional + err error + received bool +} + +func (ce *chanError) Set(err error) { + if ce.Wrap != nil && err != nil { + err = ce.Wrap(err) + } + ce.err = err + ce.received = true +} + +func (ce *chanError) Get() error { + if ce.received { + return ce.err + } + ce.Set(<-ce.C) + return ce.err +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/thumbcache.go b/vendor/github.com/camlistore/camlistore/pkg/server/thumbcache.go new file mode 100644 index 00000000..925618f7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/thumbcache.go @@ -0,0 +1,84 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "errors" + "fmt" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/lru" + "camlistore.org/pkg/sorted" +) + +const memLRUSize = 1024 // arbitrary + +var errCacheMiss = errors.New("not in cache") + +// ThumbMeta is a mapping from an image's scaling parameters (encoding +// as an opaque "key" string) and the blobref of the thumbnail +// (currently its file schema blob). +// ThumbMeta is safe for concurrent use by multiple goroutines. +// +// The key will be some string containing the original full-sized image's +// blobref, its target dimensions, and any possible transformations on +// it (e.g. cropping it to square). +type ThumbMeta struct { + mem *lru.Cache // key -> blob.Ref + kv sorted.KeyValue // optional +} + +// NewThumbMeta returns a new in-memory ThumbMeta, backed with the +// optional kv. +// If kv is nil, key/value pairs are stored in memory only. +func NewThumbMeta(kv sorted.KeyValue) *ThumbMeta { + return &ThumbMeta{ + mem: lru.New(memLRUSize), + kv: kv, + } +} + +func (m *ThumbMeta) Get(key string) (blob.Ref, error) { + var br blob.Ref + if v, ok := m.mem.Get(key); ok { + return v.(blob.Ref), nil + } + if m.kv != nil { + v, err := m.kv.Get(key) + if err == sorted.ErrNotFound { + return br, errCacheMiss + } + if err != nil { + return br, err + } + br, ok := blob.Parse(v) + if !ok { + return br, fmt.Errorf("Invalid blobref %q found for key %q in thumbnail mea", v, key) + } + m.mem.Add(key, br) + return br, nil + } + return br, errCacheMiss +} + +func (m *ThumbMeta) Put(key string, br blob.Ref) error { + m.mem.Add(key, br) + if m.kv != nil { + return m.kv.Set(key, br.String()) + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/ui.go b/vendor/github.com/camlistore/camlistore/pkg/server/ui.go new file mode 100644 index 00000000..69b35797 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/ui.go @@ -0,0 +1,658 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "errors" + "fmt" + "log" + "net/http" + "os" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/constants" + "camlistore.org/pkg/fileembed" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/misc/closure" + "camlistore.org/pkg/search" + "camlistore.org/pkg/server/app" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/syncutil" + "camlistore.org/pkg/types/camtypes" + uistatic "camlistore.org/server/camlistored/ui" + closurestatic "camlistore.org/server/camlistored/ui/closure" + "camlistore.org/third_party/code.google.com/p/rsc/qr" + fontawesomestatic "camlistore.org/third_party/fontawesome" + glitchstatic "camlistore.org/third_party/glitch" + lessstatic "camlistore.org/third_party/less" + reactstatic "camlistore.org/third_party/react" +) + +var ( + staticFilePattern = regexp.MustCompile(`^([a-zA-Z0-9\-\_\.]+\.(html|js|css|png|jpg|gif|svg))$`) + identOrDotPattern = regexp.MustCompile(`^[a-zA-Z\_]+(\.[a-zA-Z\_]+)*$`) + + // Download URL suffix: + // $1: blobref (checked in download handler) + // $2: optional "/filename" to be sent as recommended download name, + // if sane looking + downloadPattern = regexp.MustCompile(`^download/([^/]+)(/.*)?$`) + + thumbnailPattern = regexp.MustCompile(`^thumbnail/([^/]+)(/.*)?$`) + treePattern = regexp.MustCompile(`^tree/([^/]+)(/.*)?$`) + closurePattern = regexp.MustCompile(`^closure/(([^/]+)(/.*)?)$`) + lessPattern = regexp.MustCompile(`^less/(.+)$`) + reactPattern = regexp.MustCompile(`^react/(.+)$`) + fontawesomePattern = regexp.MustCompile(`^fontawesome/(.+)$`) + glitchPattern = regexp.MustCompile(`^glitch/(.+)$`) + + disableThumbCache, _ = strconv.ParseBool(os.Getenv("CAMLI_DISABLE_THUMB_CACHE")) +) + +// UIHandler handles serving the UI and discovery JSON. +type UIHandler struct { + publishRoots map[string]*publishRoot + + prefix string // of the UI handler itself + root *RootHandler + search *search.Handler + + // Cache optionally specifies a cache blob server, used for + // caching image thumbnails and other emphemeral data. + Cache blobserver.Storage // or nil + + // Limit peak RAM used by concurrent image thumbnail calls. + resizeSem *syncutil.Sem + thumbMeta *ThumbMeta // optional thumbnail key->blob.Ref cache + + // sourceRoot optionally specifies the path to root of Camlistore's + // source. If empty, the UI files must be compiled in to the + // binary (with go run make.go). This comes from the "sourceRoot" + // ui handler config option. + sourceRoot string + + uiDir string // if sourceRoot != "", this is sourceRoot+"/server/camlistored/ui" + + closureHandler http.Handler + fileLessHandler http.Handler + fileReactHandler http.Handler + fileFontawesomeHandler http.Handler + fileGlitchHandler http.Handler +} + +func init() { + blobserver.RegisterHandlerConstructor("ui", uiFromConfig) +} + +// newKVOrNil wraps sorted.NewKeyValue and adds the ability +// to pass a nil conf to get a (nil, nil) response. +func newKVOrNil(conf jsonconfig.Obj) (sorted.KeyValue, error) { + if len(conf) == 0 { + return nil, nil + } + return sorted.NewKeyValue(conf) +} + +func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) { + ui := &UIHandler{ + prefix: ld.MyPrefix(), + sourceRoot: conf.OptionalString("sourceRoot", ""), + resizeSem: syncutil.NewSem(int64(conf.OptionalInt("maxResizeBytes", + constants.DefaultMaxResizeMem))), + } + cachePrefix := conf.OptionalString("cache", "") + scaledImageConf := conf.OptionalObject("scaledImage") + if err = conf.Validate(); err != nil { + return + } + + scaledImageKV, err := newKVOrNil(scaledImageConf) + if err != nil { + return nil, fmt.Errorf("in UI handler's scaledImage: %v", err) + } + if scaledImageKV != nil && cachePrefix == "" { + return nil, fmt.Errorf("in UI handler, can't specify scaledImage without cache") + } + if cachePrefix != "" { + bs, err := ld.GetStorage(cachePrefix) + if err != nil { + return nil, fmt.Errorf("UI handler's cache of %q error: %v", cachePrefix, err) + } + ui.Cache = bs + ui.thumbMeta = NewThumbMeta(scaledImageKV) + } + + if ui.sourceRoot == "" { + ui.sourceRoot = os.Getenv("CAMLI_DEV_CAMLI_ROOT") + if uistatic.IsAppEngine { + if _, err = os.Stat(filepath.Join(uistatic.GaeSourceRoot, + filepath.FromSlash("server/camlistored/ui/index.html"))); err != nil { + hint := fmt.Sprintf("\"sourceRoot\" was not specified in the config,"+ + " and the default sourceRoot dir %v does not exist or does not contain"+ + " \"server/camlistored/ui/index.html\". devcam appengine can do that for you.", + uistatic.GaeSourceRoot) + log.Print(hint) + return nil, errors.New("No sourceRoot found; UI not available.") + } + log.Printf("Using the default \"%v\" as the sourceRoot for AppEngine", uistatic.GaeSourceRoot) + ui.sourceRoot = uistatic.GaeSourceRoot + } + } + if ui.sourceRoot != "" { + ui.uiDir = filepath.Join(ui.sourceRoot, filepath.FromSlash("server/camlistored/ui")) + // Ignore any fileembed files: + Files = &fileembed.Files{ + DirFallback: filepath.Join(ui.sourceRoot, filepath.FromSlash("pkg/server")), + } + uistatic.Files = &fileembed.Files{ + DirFallback: ui.uiDir, + Listable: true, + // In dev_appserver, allow edit-and-reload without + // restarting. In production, though, it's faster to just + // slurp it in. + SlurpToMemory: uistatic.IsProdAppEngine, + } + } + + ui.closureHandler, err = ui.makeClosureHandler(ui.sourceRoot) + if err != nil { + return nil, fmt.Errorf(`Invalid "sourceRoot" value of %q: %v"`, ui.sourceRoot, err) + } + + if ui.sourceRoot != "" { + ui.fileReactHandler, err = makeFileServer(ui.sourceRoot, filepath.Join("third_party", "react"), "react.js") + if err != nil { + return nil, fmt.Errorf("Could not make react handler: %s", err) + } + ui.fileGlitchHandler, err = makeFileServer(ui.sourceRoot, filepath.Join("third_party", "glitch"), "npc_piggy__x1_walk_png_1354829432.png") + if err != nil { + return nil, fmt.Errorf("Could not make glitch handler: %s", err) + } + ui.fileFontawesomeHandler, err = makeFileServer(ui.sourceRoot, filepath.Join("third_party", "fontawesome"), "css/font-awesome.css") + if err != nil { + return nil, fmt.Errorf("Could not make fontawesome handler: %s", err) + } + ui.fileLessHandler, err = makeFileServer(ui.sourceRoot, filepath.Join("third_party", "less"), "less.js") + if err != nil { + return nil, fmt.Errorf("Could not make less handler: %s", err) + } + } + + rootPrefix, _, err := ld.FindHandlerByType("root") + if err != nil { + return nil, errors.New("No root handler configured, which is necessary for the ui handler") + } + if h, err := ld.GetHandler(rootPrefix); err == nil { + ui.root = h.(*RootHandler) + ui.root.registerUIHandler(ui) + } else { + return nil, errors.New("failed to find the 'root' handler") + } + + return ui, nil +} + +type publishRoot struct { + Name string + Permanode blob.Ref + Prefix string +} + +// InitHandler goes through all the other configured handlers to discover +// the publisher ones, and uses them to populate ui.publishRoots. +func (ui *UIHandler) InitHandler(hl blobserver.FindHandlerByTyper) error { + // InitHandler is called after all handlers have been setup, so the bootstrap + // of the camliRoot node for publishers in dev-mode is already done. + searchPrefix, _, err := hl.FindHandlerByType("search") + if err != nil { + return errors.New("No search handler configured, which is necessary for the ui handler") + } + var sh *search.Handler + htype, hi := hl.AllHandlers() + if h, ok := hi[searchPrefix]; !ok { + return errors.New("failed to find the \"search\" handler") + } else { + sh = h.(*search.Handler) + ui.search = sh + } + camliRootQuery := func(camliRoot string) (*search.SearchResult, error) { + return sh.Query(&search.SearchQuery{ + Limit: 1, + Constraint: &search.Constraint{ + Permanode: &search.PermanodeConstraint{ + Attr: "camliRoot", + Value: camliRoot, + }, + }, + }) + } + for prefix, typ := range htype { + if typ != "app" { + continue + } + ah, ok := hi[prefix].(*app.Handler) + if !ok { + panic(fmt.Sprintf("UI: handler for %v has type \"app\" but is not app.Handler", prefix)) + } + if ah.ProgramName() != "publisher" { + continue + } + appConfig := ah.AppConfig() + if appConfig == nil { + log.Printf("UI: app handler for %v has no appConfig", prefix) + continue + } + camliRoot, ok := appConfig["camliRoot"].(string) + if !ok { + log.Printf("UI: camliRoot in appConfig is %T, want string", appConfig["camliRoot"]) + continue + } + result, err := camliRootQuery(camliRoot) + if err != nil { + log.Printf("UI: could not find permanode for camliRoot %v: %v", camliRoot, err) + continue + } + if len(result.Blobs) == 0 || !result.Blobs[0].Blob.Valid() { + log.Printf("UI: no valid permanode for camliRoot %v", camliRoot) + continue + } + if ui.publishRoots == nil { + ui.publishRoots = make(map[string]*publishRoot) + } + ui.publishRoots[prefix] = &publishRoot{ + Name: camliRoot, + Prefix: prefix, + Permanode: result.Blobs[0].Blob, + } + } + return nil +} + +func (ui *UIHandler) makeClosureHandler(root string) (http.Handler, error) { + return makeClosureHandler(root, "ui") +} + +// makeClosureHandler returns a handler to serve Closure files. +// root is either: +// 1) empty: use the Closure files compiled in to the binary (if +// available), else redirect to the Internet. +// 2) a URL prefix: base of Camlistore to get Closure to redirect to +// 3) a path on disk to the root of camlistore's source (which +// contains the necessary subset of Closure files) +func makeClosureHandler(root, handlerName string) (http.Handler, error) { + // devcam server environment variable takes precedence: + if d := os.Getenv("CAMLI_DEV_CLOSURE_DIR"); d != "" { + log.Printf("%v: serving Closure from devcam server's $CAMLI_DEV_CLOSURE_DIR: %v", handlerName, d) + return http.FileServer(http.Dir(d)), nil + } + if root == "" { + fs, err := closurestatic.FileSystem() + if err == os.ErrNotExist { + log.Printf("%v: no configured setting or embedded resources; serving Closure via %v", handlerName, closureBaseURL) + return closureBaseURL, nil + } + if err != nil { + return nil, fmt.Errorf("error loading embedded Closure zip file: %v", err) + } + log.Printf("%v: serving Closure from embedded resources", handlerName) + return http.FileServer(fs), nil + } + if strings.HasPrefix(root, "http") { + log.Printf("%v: serving Closure using redirects to %v", handlerName, root) + return closureRedirector(root), nil + } + + path := filepath.Join("third_party", "closure", "lib", "closure") + return makeFileServer(root, path, filepath.Join("goog", "base.js")) +} + +func makeFileServer(sourceRoot string, pathToServe string, expectedContentPath string) (http.Handler, error) { + fi, err := os.Stat(sourceRoot) + if err != nil { + return nil, err + } + if !fi.IsDir() { + return nil, errors.New("not a directory") + } + dirToServe := filepath.Join(sourceRoot, pathToServe) + _, err = os.Stat(filepath.Join(dirToServe, expectedContentPath)) + if err != nil { + return nil, fmt.Errorf("directory doesn't contain %s; wrong directory?", expectedContentPath) + } + return http.FileServer(http.Dir(dirToServe)), nil +} + +const closureBaseURL closureRedirector = "https://closure-library.googlecode.com/git" + +// closureRedirector is a hack to redirect requests for Closure's million *.js files +// to https://closure-library.googlecode.com/git. +// TODO: this doesn't work when offline. We need to run genjsdeps over all of the Camlistore +// UI to figure out which Closure *.js files to fileembed and generate zembed. Then this +// type can be deleted. +type closureRedirector string + +func (base closureRedirector) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + newURL := string(base) + "/" + path.Clean(httputil.PathSuffix(req)) + http.Redirect(rw, req, newURL, http.StatusTemporaryRedirect) +} + +func camliMode(req *http.Request) string { + return req.URL.Query().Get("camli.mode") +} + +func wantsBlobRef(req *http.Request) bool { + _, ok := blob.ParseKnown(httputil.PathSuffix(req)) + return ok +} + +func wantsDiscovery(req *http.Request) bool { + return httputil.IsGet(req) && + (req.Header.Get("Accept") == "text/x-camli-configuration" || + camliMode(req) == "config") +} + +func wantsUploadHelper(req *http.Request) bool { + return req.Method == "POST" && camliMode(req) == "uploadhelper" +} + +func wantsPermanode(req *http.Request) bool { + return httputil.IsGet(req) && blob.ValidRefString(req.FormValue("p")) +} + +func wantsBlobInfo(req *http.Request) bool { + return httputil.IsGet(req) && blob.ValidRefString(req.FormValue("b")) +} + +func wantsFileTreePage(req *http.Request) bool { + return httputil.IsGet(req) && blob.ValidRefString(req.FormValue("d")) +} + +func getSuffixMatches(req *http.Request, pattern *regexp.Regexp) bool { + if httputil.IsGet(req) { + suffix := httputil.PathSuffix(req) + return pattern.MatchString(suffix) + } + return false +} + +func (ui *UIHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + suffix := httputil.PathSuffix(req) + + rw.Header().Set("Vary", "Accept") + switch { + case wantsDiscovery(req): + ui.root.serveDiscovery(rw, req) + case wantsUploadHelper(req): + ui.serveUploadHelper(rw, req) + case strings.HasPrefix(suffix, "download/"): + ui.serveDownload(rw, req) + case strings.HasPrefix(suffix, "thumbnail/"): + ui.serveThumbnail(rw, req) + case strings.HasPrefix(suffix, "tree/"): + ui.serveFileTree(rw, req) + case strings.HasPrefix(suffix, "qr/"): + ui.serveQR(rw, req) + case getSuffixMatches(req, closurePattern): + ui.serveClosure(rw, req) + case getSuffixMatches(req, lessPattern): + ui.serveFromDiskOrStatic(rw, req, lessPattern, ui.fileLessHandler, lessstatic.Files) + case getSuffixMatches(req, reactPattern): + ui.serveFromDiskOrStatic(rw, req, reactPattern, ui.fileReactHandler, reactstatic.Files) + case getSuffixMatches(req, glitchPattern): + ui.serveFromDiskOrStatic(rw, req, glitchPattern, ui.fileGlitchHandler, glitchstatic.Files) + case getSuffixMatches(req, fontawesomePattern): + ui.serveFromDiskOrStatic(rw, req, fontawesomePattern, ui.fileFontawesomeHandler, fontawesomestatic.Files) + default: + file := "" + if m := staticFilePattern.FindStringSubmatch(suffix); m != nil { + file = m[1] + } else { + switch { + case wantsBlobRef(req): + file = "index.html" + case wantsPermanode(req): + file = "permanode.html" + case wantsBlobInfo(req): + file = "blobinfo.html" + case wantsFileTreePage(req): + file = "filetree.html" + case req.URL.Path == httputil.PathBase(req): + file = "index.html" + default: + http.Error(rw, "Illegal URL.", http.StatusNotFound) + return + } + } + if file == "deps.js" { + serveDepsJS(rw, req, ui.uiDir) + return + } + ServeStaticFile(rw, req, uistatic.Files, file) + } +} + +// ServeStaticFile serves file from the root virtual filesystem. +func ServeStaticFile(rw http.ResponseWriter, req *http.Request, root http.FileSystem, file string) { + f, err := root.Open("/" + file) + if err != nil { + http.NotFound(rw, req) + log.Printf("Failed to open file %q from embedded resources: %v", file, err) + return + } + defer f.Close() + var modTime time.Time + if fi, err := f.Stat(); err == nil { + modTime = fi.ModTime() + } + // TODO(wathiede): should pkg/magic be leveraged here somehow? It has a + // slightly different purpose. + if strings.HasSuffix(file, ".svg") { + rw.Header().Set("Content-Type", "image/svg+xml") + } + http.ServeContent(rw, req, file, modTime, f) +} + +func (ui *UIHandler) discovery() *camtypes.UIDiscovery { + pubRoots := map[string]*camtypes.PublishRootDiscovery{} + for _, v := range ui.publishRoots { + rd := &camtypes.PublishRootDiscovery{ + Name: v.Name, + Prefix: []string{v.Prefix}, + CurrentPermanode: v.Permanode, + } + pubRoots[v.Name] = rd + } + + uiDisco := &camtypes.UIDiscovery{ + UIRoot: ui.prefix, + UploadHelper: ui.prefix + "?camli.mode=uploadhelper", + DownloadHelper: path.Join(ui.prefix, "download") + "/", + DirectoryHelper: path.Join(ui.prefix, "tree") + "/", + PublishRoots: pubRoots, + } + return uiDisco +} + +func (ui *UIHandler) serveDownload(rw http.ResponseWriter, req *http.Request) { + if ui.root.Storage == nil { + http.Error(rw, "No BlobRoot configured", 500) + return + } + + suffix := httputil.PathSuffix(req) + m := downloadPattern.FindStringSubmatch(suffix) + if m == nil { + httputil.ErrorRouting(rw, req) + return + } + + fbr, ok := blob.Parse(m[1]) + if !ok { + http.Error(rw, "Invalid blobref", 400) + return + } + + dh := &DownloadHandler{ + Fetcher: ui.root.Storage, + Search: ui.search, + Cache: ui.Cache, + } + dh.ServeHTTP(rw, req, fbr) +} + +func (ui *UIHandler) serveThumbnail(rw http.ResponseWriter, req *http.Request) { + if ui.root.Storage == nil { + http.Error(rw, "No BlobRoot configured", 500) + return + } + + suffix := httputil.PathSuffix(req) + m := thumbnailPattern.FindStringSubmatch(suffix) + if m == nil { + httputil.ErrorRouting(rw, req) + return + } + + query := req.URL.Query() + width, _ := strconv.Atoi(query.Get("mw")) + height, _ := strconv.Atoi(query.Get("mh")) + blobref, ok := blob.Parse(m[1]) + if !ok { + http.Error(rw, "Invalid blobref", 400) + return + } + + if width == 0 { + width = search.MaxImageSize + } + if height == 0 { + height = search.MaxImageSize + } + + th := &ImageHandler{ + Fetcher: ui.root.Storage, + Cache: ui.Cache, + MaxWidth: width, + MaxHeight: height, + ThumbMeta: ui.thumbMeta, + ResizeSem: ui.resizeSem, + Search: ui.search, + } + th.ServeHTTP(rw, req, blobref) +} + +func (ui *UIHandler) serveFileTree(rw http.ResponseWriter, req *http.Request) { + if ui.root.Storage == nil { + http.Error(rw, "No BlobRoot configured", 500) + return + } + + suffix := httputil.PathSuffix(req) + m := treePattern.FindStringSubmatch(suffix) + if m == nil { + httputil.ErrorRouting(rw, req) + return + } + + blobref, ok := blob.Parse(m[1]) + if !ok { + http.Error(rw, "Invalid blobref", 400) + return + } + + fth := &FileTreeHandler{ + Fetcher: ui.root.Storage, + file: blobref, + } + fth.ServeHTTP(rw, req) +} + +func (ui *UIHandler) serveClosure(rw http.ResponseWriter, req *http.Request) { + suffix := httputil.PathSuffix(req) + if ui.closureHandler == nil { + log.Printf("%v not served: closure handler is nil", suffix) + http.NotFound(rw, req) + return + } + m := closurePattern.FindStringSubmatch(suffix) + if m == nil { + httputil.ErrorRouting(rw, req) + return + } + req.URL.Path = "/" + m[1] + ui.closureHandler.ServeHTTP(rw, req) +} + +// serveFromDiskOrStatic matches rx against req's path and serves the match either from disk (if non-nil) or from static (embedded in the binary). +func (ui *UIHandler) serveFromDiskOrStatic(rw http.ResponseWriter, req *http.Request, rx *regexp.Regexp, disk http.Handler, static *fileembed.Files) { + suffix := httputil.PathSuffix(req) + m := rx.FindStringSubmatch(suffix) + if m == nil { + panic("Caller should verify that rx matches") + } + file := m[1] + if disk != nil { + req.URL.Path = "/" + file + disk.ServeHTTP(rw, req) + } else { + ServeStaticFile(rw, req, static, file) + } + +} + +func (ui *UIHandler) serveQR(rw http.ResponseWriter, req *http.Request) { + url := req.URL.Query().Get("url") + if url == "" { + http.Error(rw, "Missing url parameter.", http.StatusBadRequest) + return + } + code, err := qr.Encode(url, qr.L) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + rw.Header().Set("Content-Type", "image/png") + rw.Write(code.PNG()) +} + +// serveDepsJS serves an auto-generated Closure deps.js file. +func serveDepsJS(rw http.ResponseWriter, req *http.Request, dir string) { + var root http.FileSystem + if dir == "" { + root = uistatic.Files + } else { + root = http.Dir(dir) + } + + b, err := closure.GenDeps(root) + if err != nil { + log.Print(err) + http.Error(rw, "Server error", 500) + return + } + rw.Header().Set("Content-Type", "text/javascript; charset=utf-8") + rw.Write([]byte("// auto-generated from camlistored\n")) + rw.Write(b) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/uploadhelper.go b/vendor/github.com/camlistore/camlistore/pkg/server/uploadhelper.go new file mode 100644 index 00000000..a9fec2e3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/uploadhelper.go @@ -0,0 +1,93 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "io" + "io/ioutil" + "log" + "net/http" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/types" +) + +// uploadHelperResponse is the response from serveUploadHelper. +type uploadHelperResponse struct { + Got []*uploadHelperGotItem `json:"got"` +} + +type uploadHelperGotItem struct { + FileName string `json:"filename"` + ModTime types.Time3339 `json:"modtime"` + FormName string `json:"formname"` + FileRef blob.Ref `json:"fileref"` +} + +func (ui *UIHandler) serveUploadHelper(rw http.ResponseWriter, req *http.Request) { + if ui.root.Storage == nil { + httputil.ServeJSONError(rw, httputil.ServerError("No BlobRoot configured")) + return + } + + mr, err := req.MultipartReader() + if err != nil { + httputil.ServeJSONError(rw, httputil.ServerError("reading body: "+err.Error())) + return + } + + var got []*uploadHelperGotItem + var modTime types.Time3339 + for { + part, err := mr.NextPart() + if err == io.EOF { + break + } + if err != nil { + httputil.ServeJSONError(rw, httputil.ServerError("reading body: "+err.Error())) + break + } + if part.FormName() == "modtime" { + payload, err := ioutil.ReadAll(part) + if err != nil { + log.Printf("ui uploadhelper: unable to read part for modtime: %v", err) + continue + } + modTime = types.ParseTime3339OrZero(string(payload)) + continue + } + fileName := part.FileName() + if fileName == "" { + continue + } + br, err := schema.WriteFileFromReaderWithModTime(ui.root.Storage, fileName, modTime.Time(), part) + if err != nil { + httputil.ServeJSONError(rw, httputil.ServerError("writing to blobserver: "+err.Error())) + return + } + got = append(got, &uploadHelperGotItem{ + FileName: part.FileName(), + ModTime: modTime, + FormName: part.FormName(), + FileRef: br, + }) + } + + httputil.ReturnJSON(rw, &uploadHelperResponse{Got: got}) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/wizard-html.go b/vendor/github.com/camlistore/camlistore/pkg/server/wizard-html.go new file mode 100644 index 00000000..5d467307 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/wizard-html.go @@ -0,0 +1,39 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +// TODO: this file and the code in wizard.go is outdated. Anyone interested enough +// can take care of updating it as something nicer which would fit better with the +// react UI. But in the meantime we don't link to it anymore. + +const topWizard = ` + + + + Camlistore setup + + +

    [Back]

    +

    Setup Wizard

    +

    See Server Configuration for information on configuring the values below.

    +
    +` + +const bottomWizard = ` + + +` diff --git a/vendor/github.com/camlistore/camlistore/pkg/server/wizard.go b/vendor/github.com/camlistore/camlistore/pkg/server/wizard.go new file mode 100644 index 00000000..54718a49 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/server/wizard.go @@ -0,0 +1,287 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "os" + "reflect" + "strconv" + "strings" + + "camlistore.org/pkg/auth" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/osutil" + + "camlistore.org/third_party/code.google.com/p/xsrftoken" +) + +var ignoredFields = map[string]bool{ + "gallery": true, + "blog": true, + "replicateTo": true, +} + +// SetupHandler handles serving the wizard setup page. +type SetupHandler struct { + config jsonconfig.Obj +} + +func init() { + blobserver.RegisterHandlerConstructor("setup", newSetupFromConfig) +} + +func newSetupFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) { + wizard := &SetupHandler{config: conf} + return wizard, nil +} + +func printWizard(i interface{}) (s string) { + switch ei := i.(type) { + case []string: + for _, v := range ei { + s += printWizard(v) + "," + } + s = strings.TrimRight(s, ",") + case []interface{}: + for _, v := range ei { + s += printWizard(v) + "," + } + s = strings.TrimRight(s, ",") + default: + return fmt.Sprintf("%v", i) + } + return s +} + +// TODO(mpl): probably not needed anymore. check later and remove. +// Flatten all published entities as lists and move them at the root +// of the conf, to have them displayed individually by the template +func flattenPublish(config jsonconfig.Obj) error { + gallery := []string{} + blog := []string{} + config["gallery"] = gallery + config["blog"] = blog + published, ok := config["publish"] + if !ok { + delete(config, "publish") + return nil + } + pubObj, ok := published.(map[string]interface{}) + if !ok { + return fmt.Errorf("Was expecting a map[string]interface{} for \"publish\", got %T", published) + } + for k, v := range pubObj { + pub, ok := v.(map[string]interface{}) + if !ok { + return fmt.Errorf("Was expecting a map[string]interface{} for %s, got %T", k, pub) + } + template, rootPermanode, style := "", "", "" + for pk, pv := range pub { + val, ok := pv.(string) + if !ok { + return fmt.Errorf("Was expecting a string for %s, got %T", pk, pv) + } + switch pk { + case "template": + template = val + case "rootPermanode": + rootPermanode = val + case "style": + style = val + default: + return fmt.Errorf("Unknown key %q in %s", pk, k) + } + } + if template == "" || rootPermanode == "" { + return fmt.Errorf("missing \"template\" key or \"rootPermanode\" key in %s", k) + } + obj := []string{k, rootPermanode, style} + config[template] = obj + } + + delete(config, "publish") + return nil +} + +var serverKey = func() string { + var b [20]byte + rand.Read(b[:]) + return string(b[:]) +}() + +func sendWizard(rw http.ResponseWriter, req *http.Request, hasChanged bool) { + config, err := jsonconfig.ReadFile(osutil.UserServerConfigPath()) + if err != nil { + httputil.ServeError(rw, req, err) + return + } + + err = flattenPublish(config) + if err != nil { + httputil.ServeError(rw, req, err) + return + } + + funcMap := template.FuncMap{ + "printWizard": printWizard, + "showField": func(inputName string) bool { + if _, ok := ignoredFields[inputName]; ok { + return false + } + return true + }, + "genXSRF": func() string { + return xsrftoken.Generate(serverKey, "user", "wizardSave") + }, + } + + body := ` + + + {{range $k,$v := .}}{{if showField $k}}{{end}}{{end}} +
    {{printf "%v" $k}}
    + + (Will restart server.)
    ` + + if hasChanged { + body += `

    Configuration succesfully rewritten

    ` + } + + tmpl, err := template.New("wizard").Funcs(funcMap).Parse(topWizard + body + bottomWizard) + if err != nil { + httputil.ServeError(rw, req, err) + return + } + err = tmpl.Execute(rw, config) + if err != nil { + httputil.ServeError(rw, req, err) + return + } +} + +func rewriteConfig(config *jsonconfig.Obj, configfile string) error { + b, err := json.MarshalIndent(*config, "", " ") + if err != nil { + return err + } + s := string(b) + f, err := os.Create(configfile) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteString(s) + return err +} + +func handleSetupChange(rw http.ResponseWriter, req *http.Request) { + hilevelConf, err := jsonconfig.ReadFile(osutil.UserServerConfigPath()) + if err != nil { + httputil.ServeError(rw, req, err) + return + } + if !xsrftoken.Valid(req.FormValue("token"), serverKey, "user", "wizardSave") { + http.Error(rw, "Form expired. Press back and reload form.", http.StatusBadRequest) + log.Printf("invalid xsrf token=%q", req.FormValue("token")) + return + } + + hasChanged := false + var el interface{} + publish := jsonconfig.Obj{} + for k, v := range req.Form { + if _, ok := hilevelConf[k]; !ok { + if k != "gallery" && k != "blog" { + continue + } + } + + switch k { + case "https", "shareHandler": + b, err := strconv.ParseBool(v[0]) + if err != nil { + httputil.ServeError(rw, req, fmt.Errorf("%v field expects a boolean value", k)) + } + el = b + default: + el = v[0] + } + if reflect.DeepEqual(hilevelConf[k], el) { + continue + } + hasChanged = true + hilevelConf[k] = el + } + // "publish" wasn't checked yet + if !reflect.DeepEqual(hilevelConf["publish"], publish) { + hilevelConf["publish"] = publish + hasChanged = true + } + + if hasChanged { + err = rewriteConfig(&hilevelConf, osutil.UserServerConfigPath()) + if err != nil { + httputil.ServeError(rw, req, err) + return + } + err = osutil.RestartProcess() + if err != nil { + log.Fatal("Failed to restart: " + err.Error()) + http.Error(rw, "Failed to restart process", 500) + return + } + } + sendWizard(rw, req, hasChanged) +} + +func (sh *SetupHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if !auth.IsLocalhost(req) { + fmt.Fprintf(rw, + "Setup only allowed from localhost"+ + "

    Back

    "+ + "\n") + return + } + http.Redirect(rw, req, "http://camlistore.org/docs/server-config", http.StatusMovedPermanently) + return + + // TODO: this file and the code in wizard-html.go is outdated. Anyone interested enough + // can take care of updating it as something nicer which would fit better with the + // react UI. But in the meantime we don't link to it anymore. + + if req.Method == "POST" { + err := req.ParseMultipartForm(10e6) + if err != nil { + httputil.ServeError(rw, req, err) + return + } + if len(req.Form) > 0 { + handleSetupChange(rw, req) + } + return + } + + sendWizard(rw, req, false) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/devmode.go b/vendor/github.com/camlistore/camlistore/pkg/serverinit/devmode.go new file mode 100644 index 00000000..9c3d5971 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/devmode.go @@ -0,0 +1,106 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package serverinit + +import ( + "errors" + "fmt" + "log" + "strings" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/env" + "camlistore.org/pkg/jsonsign/signhandler" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/search" + "camlistore.org/pkg/server/app" +) + +func (hl *handlerLoader) initPublisherRootNode(ah *app.Handler) error { + if !env.IsDev() { + return nil + } + + h, err := hl.GetHandler("/my-search/") + if err != nil { + return err + } + sh := h.(*search.Handler) + camliRootQuery := func(camliRoot string) (*search.SearchResult, error) { + return sh.Query(&search.SearchQuery{ + Limit: 1, + Constraint: &search.Constraint{ + Permanode: &search.PermanodeConstraint{ + Attr: "camliRoot", + Value: camliRoot, + }, + }, + }) + } + + appConfig := ah.AppConfig() + if appConfig == nil { + return errors.New("publisher app handler has no AppConfig") + } + camliRoot, ok := appConfig["camliRoot"].(string) + if !ok { + return fmt.Errorf("camliRoot in publisher app handler appConfig is %T, want string", appConfig["camliRoot"]) + } + result, err := camliRootQuery(camliRoot) + if err == nil && len(result.Blobs) > 0 && result.Blobs[0].Blob.Valid() { + // root node found, nothing more to do. + log.Printf("Found %v camliRoot node for publisher: %v", camliRoot, result.Blobs[0].Blob.String()) + return nil + } + + log.Printf("No %v camliRoot node found, creating one from scratch now.", camliRoot) + + bs, err := hl.GetStorage("/bs-recv/") + if err != nil { + return err + } + h, err = hl.GetHandler("/sighelper/") + if err != nil { + return err + } + sigh := h.(*signhandler.Handler) + + signUpload := func(bb *schema.Builder) (blob.Ref, error) { + signed, err := sigh.Sign(bb) + if err != nil { + return blob.Ref{}, fmt.Errorf("could not sign blob: %v", err) + } + br := blob.SHA1FromString(signed) + if _, err := blobserver.Receive(bs, br, strings.NewReader(signed)); err != nil { + return blob.Ref{}, fmt.Errorf("could not upload %v: %v", br.String(), err) + } + return br, nil + } + + pn, err := signUpload(schema.NewUnsignedPermanode()) + if err != nil { + return fmt.Errorf("could not create new camliRoot node: %v", err) + } + if _, err := signUpload(schema.NewSetAttributeClaim(pn, "camliRoot", camliRoot)); err != nil { + return fmt.Errorf("could not set camliRoot on new node %v: %v", pn, err) + } + if _, err := signUpload(schema.NewSetAttributeClaim(pn, "title", "Publish root node for "+camliRoot)); err != nil { + return fmt.Errorf("could not set camliRoot on new node %v: %v", pn, err) + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/env.go b/vendor/github.com/camlistore/camlistore/pkg/serverinit/env.go new file mode 100644 index 00000000..b098bc10 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/env.go @@ -0,0 +1,95 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package serverinit + +import ( + "fmt" + "os" + "strings" + + "camlistore.org/pkg/env" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/types/serverconfig" + "google.golang.org/cloud/compute/metadata" +) + +// DefaultEnvConfig returns the default configuration when running on a known +// environment. Currently this just includes Google Compute Engine. +// If the environment isn't known (nil, nil) is returned. +func DefaultEnvConfig() (*Config, error) { + if !env.OnGCE() { + return nil, nil + } + auth := "none" + user, _ := metadata.InstanceAttributeValue("camlistore-username") + pass, _ := metadata.InstanceAttributeValue("camlistore-password") + confBucket, err := metadata.InstanceAttributeValue("camlistore-config-dir") + if confBucket == "" || err != nil { + return nil, fmt.Errorf("VM instance metadata key 'camlistore-config-dir' not set: %v", err) + } + blobBucket, err := metadata.InstanceAttributeValue("camlistore-blob-dir") + if blobBucket == "" || err != nil { + return nil, fmt.Errorf("VM instance metadata key 'camlistore-blob-dir' not set: %v", err) + } + if user != "" && pass != "" { + auth = "userpass:" + user + ":" + pass + } + + if v := osutil.SecretRingFile(); !strings.HasPrefix(v, "/gcs/") { + return nil, fmt.Errorf("Internal error: secret ring path on GCE should be at /gcs/, not %q", v) + } + keyId, secRing, err := getOrMakeKeyring() + if err != nil { + return nil, err + } + + ipOrHost, _ := metadata.ExternalIP() + host, _ := metadata.InstanceAttributeValue("camlistore-hostname") + if host != "" && host != "localhost" { + ipOrHost = host + } + + highConf := &serverconfig.Config{ + Auth: auth, + BaseURL: fmt.Sprintf("https://%s", ipOrHost), + HTTPS: true, + Listen: "0.0.0.0:443", + Identity: keyId, + IdentitySecretRing: secRing, + GoogleCloudStorage: ":" + strings.TrimPrefix(blobBucket, "gs://"), + DBNames: map[string]string{}, + PackRelated: true, + + // SourceRoot is where we look for the UI js/css/html files, and the Closure resources. + // Must be in sync with misc/docker/server/Dockerfile. + SourceRoot: "/camlistore", + } + + // Detect a linked Docker MySQL container. It must have alias "mysqldb". + if v := os.Getenv("MYSQLDB_PORT"); strings.HasPrefix(v, "tcp://") { + hostPort := strings.TrimPrefix(v, "tcp://") + highConf.MySQL = "root@" + hostPort + ":" // no password + highConf.DBNames["queue-sync-to-index"] = "sync_index_queue" + highConf.DBNames["ui_thumbcache"] = "ui_thumbmeta_cache" + highConf.DBNames["blobpacked_index"] = "blobpacked_index" + } else { + // TODO: also detect Cloud SQL. + highConf.KVFile = "/index.kv" + } + + return genLowLevelConfig(highConf) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/export_test.go b/vendor/github.com/camlistore/camlistore/pkg/serverinit/export_test.go new file mode 100644 index 00000000..68b8efa8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/export_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package serverinit + +var GenLowLevelConfig = genLowLevelConfig + +var DefaultBaseConfig = defaultBaseConfig + +func SetTempDirFunc(f func() string) { + tempDir = f +} + +func SetNoMkdir(v bool) { + noMkdir = v +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/genconfig.go b/vendor/github.com/camlistore/camlistore/pkg/serverinit/genconfig.go new file mode 100644 index 00000000..87473b5a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/genconfig.go @@ -0,0 +1,947 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package serverinit + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "net/url" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/jsonsign" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/types/serverconfig" + "camlistore.org/pkg/wkfs" +) + +var ( + tempDir = os.TempDir + noMkdir bool // for tests to not call os.Mkdir +) + +type tlsOpts struct { + httpsCert string + httpsKey string +} + +// genLowLevelConfig returns a low-level config from a high-level config. +func genLowLevelConfig(conf *serverconfig.Config) (lowLevelConf *Config, err error) { + b := &lowBuilder{ + high: conf, + low: jsonconfig.Obj{ + "prefixes": make(map[string]interface{}), + }, + } + return b.build() +} + +// A lowBuilder builds a low-level config from a high-level config. +type lowBuilder struct { + high *serverconfig.Config // high-level config (input) + low jsonconfig.Obj // low-level handler config (output) +} + +// args is an alias for map[string]interface{} just to cut down on +// noise below. But we take care to convert it back to +// map[string]interface{} in the one place where we accept it. +type args map[string]interface{} + +func (b *lowBuilder) addPrefix(at, handler string, a args) { + v := map[string]interface{}{ + "handler": handler, + } + if a != nil { + v["handlerArgs"] = (map[string]interface{})(a) + } + b.low["prefixes"].(map[string]interface{})[at] = v +} + +func (b *lowBuilder) hasPrefix(p string) bool { + _, ok := b.low["prefixes"].(map[string]interface{})[p] + return ok +} + +func (b *lowBuilder) runIndex() bool { return b.high.RunIndex.Get() } +func (b *lowBuilder) copyIndexToMemory() bool { return b.high.CopyIndexToMemory.Get() } + +// dbName returns which database to use for the provided user ("of"). +// The user should be a key as described in pkg/types/serverconfig/config.go's +// description of DBNames: "index", "queue-sync-to-index", etc. +func (b *lowBuilder) dbName(of string) string { + if v, ok := b.high.DBNames[of]; ok && v != "" { + return v + } + if of == "index" { + if b.high.DBName != "" { + return b.high.DBName + } + username := osutil.Username() + if username == "" { + envVar := "USER" + if runtime.GOOS == "windows" { + envVar += "NAME" + } + return "camlistore_index" + } + return "camli" + username + } + return "" +} + +var errNoOwner = errors.New("no owner") + +// Error is errNoOwner if no identity configured +func (b *lowBuilder) searchOwner() (br blob.Ref, err error) { + if b.high.Identity == "" { + return br, errNoOwner + } + entity, err := jsonsign.EntityFromSecring(b.high.Identity, b.high.IdentitySecretRing) + if err != nil { + return br, err + } + armoredPublicKey, err := jsonsign.ArmoredPublicKey(entity) + if err != nil { + return br, err + } + return blob.SHA1FromString(armoredPublicKey), nil +} + +func (b *lowBuilder) addPublishedConfig(tlsO *tlsOpts) error { + published := b.high.Publish + for k, v := range published { + if v.CamliRoot == "" { + return fmt.Errorf("Missing \"camliRoot\" key in configuration for %s.", k) + } + if v.GoTemplate == "" { + return fmt.Errorf("Missing \"goTemplate\" key in configuration for %s.", k) + } + + appConfig := map[string]interface{}{ + "camliRoot": v.CamliRoot, + "cacheRoot": v.CacheRoot, + "goTemplate": v.GoTemplate, + } + if v.HTTPSCert != "" && v.HTTPSKey != "" { + // user can specify these directly in the publish section + appConfig["httpsCert"] = v.HTTPSCert + appConfig["httpsKey"] = v.HTTPSKey + } else { + // default to Camlistore parameters, if any + if tlsO != nil { + appConfig["httpsCert"] = tlsO.httpsCert + appConfig["httpsKey"] = tlsO.httpsKey + } + } + a := args{ + "program": v.Program, + "appConfig": appConfig, + } + if v.BaseURL != "" { + a["baseURL"] = v.BaseURL + } + program := "publisher" + if v.Program != "" { + program = v.Program + } + a["program"] = program + b.addPrefix(k, "app", a) + } + return nil +} + +// kvFileType returns the file based sorted type defined for index storage, if +// any. It defaults to "leveldb" otherwise. +func (b *lowBuilder) kvFileType() string { + switch { + case b.high.SQLite != "": + return "sqlite" + case b.high.KVFile != "": + return "kv" + case b.high.LevelDB != "": + return "leveldb" + default: + return sorted.DefaultKVFileType + } +} + +func (b *lowBuilder) addUIConfig() { + args := map[string]interface{}{ + "cache": "/cache/", + } + if b.high.SourceRoot != "" { + args["sourceRoot"] = b.high.SourceRoot + } + var thumbCache map[string]interface{} + if b.high.BlobPath != "" { + thumbCache = map[string]interface{}{ + "type": b.kvFileType(), + "file": filepath.Join(b.high.BlobPath, "thumbmeta."+b.kvFileType()), + } + } + if thumbCache == nil { + sorted, err := b.sortedStorage("ui_thumbcache") + if err == nil { + thumbCache = sorted + } + } + if thumbCache != nil { + args["scaledImage"] = thumbCache + } + b.addPrefix("/ui/", "ui", args) +} + +func (b *lowBuilder) mongoIndexStorage(confStr, sortedType string) (map[string]interface{}, error) { + dbName := b.dbName(sortedType) + if dbName == "" { + return nil, fmt.Errorf("no database name configured for sorted store %q", sortedType) + } + fields := strings.Split(confStr, "@") + if len(fields) == 2 { + host := fields[1] + fields = strings.Split(fields[0], ":") + if len(fields) == 2 { + user, pass := fields[0], fields[1] + return map[string]interface{}{ + "type": "mongo", + "host": host, + "user": user, + "password": pass, + "database": dbName, + }, nil + } + } + return nil, errors.New("Malformed mongo config string; want form: \"user:password@host\"") +} + +// parses "user@host:password", which you think would be easy, but we +// documented this format without thinking about port numbers, so this +// uses heuristics to guess what extra colons mean. +func parseUserHostPass(v string) (user, host, password string, ok bool) { + f := strings.SplitN(v, "@", 2) + if len(f) != 2 { + return + } + user = f[0] + f = strings.Split(f[1], ":") + if len(f) < 2 { + return "", "", "", false + } + host = f[0] + f = f[1:] + if len(f) >= 2 { + if _, err := strconv.ParseUint(f[0], 10, 16); err == nil { + host = host + ":" + f[0] + f = f[1:] + } + } + password = strings.Join(f, ":") + ok = true + return +} + +func (b *lowBuilder) dbIndexStorage(rdbms string, confStr string, sortedType string) (map[string]interface{}, error) { + dbName := b.dbName(sortedType) + if dbName == "" { + return nil, fmt.Errorf("no database name configured for sorted store %q", sortedType) + } + user, host, password, ok := parseUserHostPass(confStr) + if !ok { + return nil, fmt.Errorf("Malformed %s config string. Want: \"user@host:password\"", rdbms) + } + return map[string]interface{}{ + "type": rdbms, + "host": host, + "user": user, + "password": password, + "database": b.dbName(sortedType), + }, nil +} + +func (b *lowBuilder) sortedStorage(sortedType string) (map[string]interface{}, error) { + return b.sortedStorageAt(sortedType, "") +} + +// filePrefix gives a file path of where to put the database. It can be omitted by +// some sorted implementations, but is required by others. +// The filePrefix should be to a file, not a directory, and should not end in a ".ext" extension. +// An extension like ".kv" or ".sqlite" will be added. +func (b *lowBuilder) sortedStorageAt(sortedType, filePrefix string) (map[string]interface{}, error) { + if b.high.MySQL != "" { + return b.dbIndexStorage("mysql", b.high.MySQL, sortedType) + } + if b.high.PostgreSQL != "" { + return b.dbIndexStorage("postgres", b.high.PostgreSQL, sortedType) + } + if b.high.Mongo != "" { + return b.mongoIndexStorage(b.high.Mongo, sortedType) + } + if b.high.MemoryIndex { + return map[string]interface{}{ + "type": "memory", + }, nil + } + if sortedType != "index" && filePrefix == "" { + return nil, fmt.Errorf("internal error: use of sortedStorageAt with a non-index type and no file location for non-database sorted implementation") + } + // dbFile returns path directly if sortedType == "index", else it returns filePrefix+"."+ext. + dbFile := func(path, ext string) string { + if sortedType == "index" { + return path + } + return filePrefix + "." + ext + } + if b.high.SQLite != "" { + return map[string]interface{}{ + "type": "sqlite", + "file": dbFile(b.high.SQLite, "sqlite"), + }, nil + } + if b.high.KVFile != "" { + return map[string]interface{}{ + "type": "kv", + "file": dbFile(b.high.KVFile, "kv"), + }, nil + } + if b.high.LevelDB != "" { + return map[string]interface{}{ + "type": "leveldb", + "file": dbFile(b.high.LevelDB, "leveldb"), + }, nil + } + panic("internal error: sortedStorageAt didn't find a sorted implementation") +} + +func (b *lowBuilder) thatQueueUnlessMemory(thatQueue map[string]interface{}) (queue map[string]interface{}) { + if b.high.MemoryStorage { + return map[string]interface{}{ + "type": "memory", + } + } + return thatQueue +} + +func (b *lowBuilder) addS3Config(s3 string) error { + f := strings.SplitN(s3, ":", 4) + if len(f) < 3 { + return errors.New(`genconfig: expected "s3" field to be of form "access_key_id:secret_access_key:bucket"`) + } + accessKey, secret, bucket := f[0], f[1], f[2] + var hostname string + if len(f) == 4 { + hostname = f[3] + } + isPrimary := !b.hasPrefix("/bs/") + s3Prefix := "" + if isPrimary { + s3Prefix = "/bs/" + if b.high.PackRelated { + return errors.New("TODO: finish packRelated support for S3") + } + } else { + s3Prefix = "/sto-s3/" + } + a := args{ + "aws_access_key": accessKey, + "aws_secret_access_key": secret, + "bucket": bucket, + } + if hostname != "" { + a["hostname"] = hostname + } + b.addPrefix(s3Prefix, "storage-s3", a) + if isPrimary { + // TODO(mpl): s3CacheBucket + // See https://camlistore.org/issue/85 + b.addPrefix("/cache/", "storage-filesystem", args{ + "path": filepath.Join(tempDir(), "camli-cache"), + }) + } else { + if b.high.BlobPath == "" && !b.high.MemoryStorage { + panic("unexpected empty blobpath with sync-to-s3") + } + b.addPrefix("/sync-to-s3/", "sync", args{ + "from": "/bs/", + "to": s3Prefix, + "queue": b.thatQueueUnlessMemory( + map[string]interface{}{ + "type": b.kvFileType(), + "file": filepath.Join(b.high.BlobPath, "sync-to-s3-queue."+b.kvFileType()), + }), + }) + } + return nil +} + +func (b *lowBuilder) addGoogleDriveConfig(v string) error { + f := strings.SplitN(v, ":", 4) + if len(f) != 4 { + return errors.New(`genconfig: expected "googledrive" field to be of form "client_id:client_secret:refresh_token:parent_id"`) + } + clientId, secret, refreshToken, parentId := f[0], f[1], f[2], f[3] + + isPrimary := !b.hasPrefix("/bs/") + prefix := "" + if isPrimary { + prefix = "/bs/" + if b.high.PackRelated { + return errors.New("TODO: finish packRelated support for Google Drive") + } + } else { + prefix = "/sto-googledrive/" + } + b.addPrefix(prefix, "storage-googledrive", args{ + "parent_id": parentId, + "auth": map[string]interface{}{ + "client_id": clientId, + "client_secret": secret, + "refresh_token": refreshToken, + }, + }) + + if isPrimary { + b.addPrefix("/cache/", "storage-filesystem", args{ + "path": filepath.Join(tempDir(), "camli-cache"), + }) + } else { + b.addPrefix("/sync-to-googledrive/", "sync", args{ + "from": "/bs/", + "to": prefix, + "queue": b.thatQueueUnlessMemory( + map[string]interface{}{ + "type": b.kvFileType(), + "file": filepath.Join(b.high.BlobPath, "sync-to-googledrive-queue."+b.kvFileType()), + }), + }) + } + + return nil +} + +var errGCSUsage = errors.New(`genconfig: expected "googlecloudstorage" field to be of form "client_id:client_secret:refresh_token:bucket[/dir/]" or ":bucketname[/dir/]"`) + +func (b *lowBuilder) addGoogleCloudStorageConfig(v string) error { + var clientID, secret, refreshToken, bucket string + f := strings.SplitN(v, ":", 4) + switch len(f) { + default: + return errGCSUsage + case 4: + clientID, secret, refreshToken, bucket = f[0], f[1], f[2], f[3] + case 2: + if f[0] != "" { + return errGCSUsage + } + bucket = f[1] + clientID = "auto" + } + + isReplica := b.hasPrefix("/bs/") + if isReplica { + gsPrefix := "/sto-googlecloudstorage/" + b.addPrefix(gsPrefix, "storage-googlecloudstorage", args{ + "bucket": bucket, + "auth": map[string]interface{}{ + "client_id": clientID, + "client_secret": secret, + "refresh_token": refreshToken, + }, + }) + + b.addPrefix("/sync-to-googlecloudstorage/", "sync", args{ + "from": "/bs/", + "to": gsPrefix, + "queue": b.thatQueueUnlessMemory( + map[string]interface{}{ + "type": b.kvFileType(), + "file": filepath.Join(b.high.BlobPath, "sync-to-googlecloud-queue."+b.kvFileType()), + }), + }) + return nil + } + + // TODO: cacheBucket like s3CacheBucket? + b.addPrefix("/cache/", "storage-filesystem", args{ + "path": filepath.Join(tempDir(), "camli-cache"), + }) + if b.high.PackRelated { + b.addPrefix("/bs-loose/", "storage-googlecloudstorage", args{ + "bucket": bucket + "/loose", + "auth": map[string]interface{}{ + "client_id": clientID, + "client_secret": secret, + "refresh_token": refreshToken, + }, + }) + b.addPrefix("/bs-packed/", "storage-googlecloudstorage", args{ + "bucket": bucket + "/packed", + "auth": map[string]interface{}{ + "client_id": clientID, + "client_secret": secret, + "refresh_token": refreshToken, + }, + }) + blobPackedIndex, err := b.sortedStorageAt("blobpacked_index", "") + if err != nil { + return err + } + b.addPrefix("/bs/", "storage-blobpacked", args{ + "smallBlobs": "/bs-loose/", + "largeBlobs": "/bs-packed/", + "metaIndex": blobPackedIndex, + }) + return nil + } + b.addPrefix("/bs/", "storage-googlecloudstorage", args{ + "bucket": bucket, + "auth": map[string]interface{}{ + "client_id": clientID, + "client_secret": secret, + "refresh_token": refreshToken, + }, + }) + + return nil +} + +// indexFileDir returns the directory of the sqlite or kv file, or the +// empty string. +func (b *lowBuilder) indexFileDir() string { + switch { + case b.high.SQLite != "": + return filepath.Dir(b.high.SQLite) + case b.high.KVFile != "": + return filepath.Dir(b.high.KVFile) + case b.high.LevelDB != "": + return filepath.Dir(b.high.LevelDB) + } + return "" +} + +func (b *lowBuilder) syncToIndexArgs() (map[string]interface{}, error) { + a := map[string]interface{}{ + "from": "/bs/", + "to": "/index/", + } + + const sortedType = "queue-sync-to-index" + if dbName := b.dbName(sortedType); dbName != "" { + qj, err := b.sortedStorage(sortedType) + if err != nil { + return nil, err + } + a["queue"] = qj + return a, nil + } + + // TODO: currently when using s3, the index must be + // sqlite or kvfile, since only through one of those + // can we get a directory. + if !b.high.MemoryStorage && b.high.BlobPath == "" && b.indexFileDir() == "" { + // We don't actually have a working sync handler, but we keep a stub registered + // so it can be referred to from other places. + // See http://camlistore.org/issue/201 + a["idle"] = true + return a, nil + } + + dir := b.high.BlobPath + if dir == "" { + dir = b.indexFileDir() + } + a["queue"] = b.thatQueueUnlessMemory( + map[string]interface{}{ + "type": b.kvFileType(), + "file": filepath.Join(dir, "sync-to-index-queue."+b.kvFileType()), + }) + + return a, nil +} + +func (b *lowBuilder) genLowLevelPrefixes() error { + root := "/bs/" + pubKeyDest := root + if b.runIndex() { + root = "/bs-and-maybe-also-index/" + pubKeyDest = "/bs-and-index/" + } + + rootArgs := map[string]interface{}{ + "stealth": false, + "blobRoot": root, + "helpRoot": "/help/", + "statusRoot": "/status/", + "jsonSignRoot": "/sighelper/", + } + if b.high.OwnerName != "" { + rootArgs["ownerName"] = b.high.OwnerName + } + if b.runIndex() { + rootArgs["searchRoot"] = "/my-search/" + } + b.addPrefix("/", "root", rootArgs) + b.addPrefix("/setup/", "setup", nil) + b.addPrefix("/status/", "status", nil) + b.addPrefix("/help/", "help", nil) + + importerArgs := args{} + if b.high.Flickr != "" { + importerArgs["flickr"] = map[string]interface{}{ + "clientSecret": b.high.Flickr, + } + } + if b.high.Picasa != "" { + importerArgs["picasa"] = map[string]interface{}{ + "clientSecret": b.high.Picasa, + } + } + if b.runIndex() { + b.addPrefix("/importer/", "importer", importerArgs) + } + + if path := b.high.ShareHandlerPath; path != "" { + b.addPrefix(path, "share", args{ + "blobRoot": "/bs/", + }) + } + + b.addPrefix("/sighelper/", "jsonsign", args{ + "secretRing": b.high.IdentitySecretRing, + "keyId": b.high.Identity, + "publicKeyDest": pubKeyDest, + }) + + storageType := "filesystem" + if b.high.PackBlobs { + storageType = "diskpacked" + } + if b.high.BlobPath != "" { + if b.high.PackRelated { + b.addPrefix("/bs-loose/", "storage-filesystem", args{ + "path": b.high.BlobPath, + }) + b.addPrefix("/bs-packed/", "storage-filesystem", args{ + "path": filepath.Join(b.high.BlobPath, "packed"), + }) + blobPackedIndex, err := b.sortedStorageAt("blobpacked_index", filepath.Join(b.high.BlobPath, "packed", "packindex")) + if err != nil { + return err + } + b.addPrefix("/bs/", "storage-blobpacked", args{ + "smallBlobs": "/bs-loose/", + "largeBlobs": "/bs-packed/", + "metaIndex": blobPackedIndex, + }) + } else if b.high.PackBlobs { + b.addPrefix("/bs/", "storage-"+storageType, args{ + "path": b.high.BlobPath, + "metaIndex": map[string]interface{}{ + "type": b.kvFileType(), + "file": filepath.Join(b.high.BlobPath, "index."+b.kvFileType()), + }, + }) + } else { + b.addPrefix("/bs/", "storage-"+storageType, args{ + "path": b.high.BlobPath, + }) + } + if b.high.PackBlobs { + b.addPrefix("/cache/", "storage-"+storageType, args{ + "path": filepath.Join(b.high.BlobPath, "/cache"), + "metaIndex": map[string]interface{}{ + "type": b.kvFileType(), + "file": filepath.Join(b.high.BlobPath, "cache", "index."+b.kvFileType()), + }, + }) + } else { + b.addPrefix("/cache/", "storage-"+storageType, args{ + "path": filepath.Join(b.high.BlobPath, "/cache"), + }) + } + } else if b.high.MemoryStorage { + b.addPrefix("/bs/", "storage-memory", nil) + b.addPrefix("/cache/", "storage-memory", nil) + } + + if b.runIndex() { + syncArgs, err := b.syncToIndexArgs() + if err != nil { + return err + } + b.addPrefix("/sync/", "sync", syncArgs) + + b.addPrefix("/bs-and-index/", "storage-replica", args{ + "backends": []interface{}{"/bs/", "/index/"}, + }) + + b.addPrefix("/bs-and-maybe-also-index/", "storage-cond", args{ + "write": map[string]interface{}{ + "if": "isSchema", + "then": "/bs-and-index/", + "else": "/bs/", + }, + "read": "/bs/", + }) + + owner, err := b.searchOwner() + if err != nil { + return err + } + searchArgs := args{ + "index": "/index/", + "owner": owner.String(), + } + if b.copyIndexToMemory() { + searchArgs["slurpToMemory"] = true + } + b.addPrefix("/my-search/", "search", searchArgs) + } + + return nil +} + +func (b *lowBuilder) build() (*Config, error) { + conf, low := b.high, b.low + if conf.HTTPS { + if (conf.HTTPSCert != "") != (conf.HTTPSKey != "") { + return nil, errors.New("Must set both httpsCert and httpsKey (or neither to generate a self-signed cert)") + } + if conf.HTTPSCert != "" { + low["httpsCert"] = conf.HTTPSCert + low["httpsKey"] = conf.HTTPSKey + } else { + low["httpsCert"] = osutil.DefaultTLSCert() + low["httpsKey"] = osutil.DefaultTLSKey() + } + } + + if conf.BaseURL != "" { + u, err := url.Parse(conf.BaseURL) + if err != nil { + return nil, fmt.Errorf("Error parsing baseURL %q as a URL: %v", conf.BaseURL, err) + } + if u.Path != "" && u.Path != "/" { + return nil, fmt.Errorf("baseURL can't have a path, only a scheme, host, and optional port.") + } + u.Path = "" + low["baseURL"] = u.String() + } + if conf.Listen != "" { + low["listen"] = conf.Listen + } + if conf.PackBlobs && conf.PackRelated { + return nil, errors.New("can't use both packBlobs (for 'diskpacked') and packRelated (for 'blobpacked')") + } + low["https"] = conf.HTTPS + low["auth"] = conf.Auth + + numIndexers := numSet(conf.LevelDB, conf.Mongo, conf.MySQL, conf.PostgreSQL, conf.SQLite, conf.KVFile, conf.MemoryIndex) + + switch { + case b.runIndex() && numIndexers == 0: + return nil, fmt.Errorf("Unless runIndex is set to false, you must specify an index option (kvIndexFile, leveldb, mongo, mysql, postgres, sqlite, memoryIndex).") + case b.runIndex() && numIndexers != 1: + return nil, fmt.Errorf("With runIndex set true, you can only pick exactly one indexer (mongo, mysql, postgres, sqlite, kvIndexFile, leveldb, memoryIndex).") + case !b.runIndex() && numIndexers != 0: + return nil, fmt.Errorf("With runIndex disabled, you can't specify any of mongo, mysql, postgres, sqlite.") + } + + if conf.Identity == "" { + return nil, errors.New("no 'identity' in server config") + } + + noLocalDisk := conf.BlobPath == "" + if noLocalDisk { + if !conf.MemoryStorage && conf.S3 == "" && conf.GoogleCloudStorage == "" { + return nil, errors.New("Unless memoryStorage is set, you must specify at least one storage option for your blobserver (blobPath (for localdisk), s3, googlecloudstorage).") + } + if !conf.MemoryStorage && conf.S3 != "" && conf.GoogleCloudStorage != "" { + return nil, errors.New("Using S3 as a primary storage and Google Cloud Storage as a mirror is not supported for now.") + } + } + if conf.ShareHandler && conf.ShareHandlerPath == "" { + conf.ShareHandlerPath = "/share/" + } + if conf.MemoryStorage { + noMkdir = true + if conf.BlobPath != "" { + return nil, errors.New("memoryStorage and blobPath are mutually exclusive.") + } + if conf.PackRelated { + return nil, errors.New("memoryStorage doesn't support packRelated.") + } + } + + if err := b.genLowLevelPrefixes(); err != nil { + return nil, err + } + + var cacheDir string + if noLocalDisk { + // Whether camlistored is run from EC2 or not, we use + // a temp dir as the cache when primary storage is S3. + // TODO(mpl): s3CacheBucket + // See https://camlistore.org/issue/85 + cacheDir = filepath.Join(tempDir(), "camli-cache") + } else { + cacheDir = filepath.Join(conf.BlobPath, "cache") + } + if !noMkdir { + if err := os.MkdirAll(cacheDir, 0700); err != nil { + return nil, fmt.Errorf("Could not create blobs cache dir %s: %v", cacheDir, err) + } + } + + if len(conf.Publish) > 0 { + if !b.runIndex() { + return nil, fmt.Errorf("publishing requires an index") + } + var tlsO *tlsOpts + httpsCert, ok1 := low["httpsCert"].(string) + httpsKey, ok2 := low["httpsKey"].(string) + if ok1 && ok2 { + tlsO = &tlsOpts{ + httpsCert: httpsCert, + httpsKey: httpsKey, + } + } + if err := b.addPublishedConfig(tlsO); err != nil { + return nil, fmt.Errorf("Could not generate config for published: %v", err) + } + } + + if b.runIndex() { + b.addUIConfig() + sto, err := b.sortedStorage("index") + if err != nil { + return nil, err + } + b.addPrefix("/index/", "storage-index", args{ + "blobSource": "/bs/", + "storage": sto, + }) + } + + if conf.S3 != "" { + if err := b.addS3Config(conf.S3); err != nil { + return nil, err + } + } + if conf.GoogleDrive != "" { + if err := b.addGoogleDriveConfig(conf.GoogleDrive); err != nil { + return nil, err + } + } + if conf.GoogleCloudStorage != "" { + if err := b.addGoogleCloudStorageConfig(conf.GoogleCloudStorage); err != nil { + return nil, err + } + } + + return &Config{Obj: b.low}, nil +} + +func numSet(vv ...interface{}) (num int) { + for _, vi := range vv { + switch v := vi.(type) { + case string: + if v != "" { + num++ + } + case bool: + if v { + num++ + } + default: + panic("unknown type") + } + } + return +} + +var defaultBaseConfig = serverconfig.Config{ + Listen: ":3179", + HTTPS: false, + Auth: "localhost", +} + +// WriteDefaultConfigFile generates a new default high-level server configuration +// file at filePath. If useSQLite, the default indexer will use SQLite, otherwise +// kv. If filePath already exists, it is overwritten. +func WriteDefaultConfigFile(filePath string, useSQLite bool) error { + conf := defaultBaseConfig + blobDir := osutil.CamliBlobRoot() + if err := wkfs.MkdirAll(blobDir, 0700); err != nil { + return fmt.Errorf("Could not create default blobs directory: %v", err) + } + conf.BlobPath = blobDir + if useSQLite { + conf.SQLite = filepath.Join(osutil.CamliVarDir(), "camli-index.db") + } else { + conf.KVFile = filepath.Join(osutil.CamliVarDir(), "camli-index.kvdb") + } + + keyID, secretRing, err := getOrMakeKeyring() + if err != nil { + return err + } + conf.Identity = keyID + conf.IdentitySecretRing = secretRing + + confData, err := json.MarshalIndent(conf, "", " ") + if err != nil { + return fmt.Errorf("Could not json encode config file : %v", err) + } + + if err := wkfs.WriteFile(filePath, confData, 0600); err != nil { + return fmt.Errorf("Could not create or write default server config: %v", err) + } + + return nil +} + +func getOrMakeKeyring() (keyID, secRing string, err error) { + secRing = osutil.SecretRingFile() + _, err = wkfs.Stat(secRing) + switch { + case err == nil: + keyID, err = jsonsign.KeyIdFromRing(secRing) + if err != nil { + err = fmt.Errorf("Could not find any keyID in file %q: %v", secRing, err) + return + } + log.Printf("Re-using identity with keyID %q found in file %s", keyID, secRing) + case os.IsNotExist(err): + keyID, err = jsonsign.GenerateNewSecRing(secRing) + if err != nil { + err = fmt.Errorf("Could not generate new secRing at file %q: %v", secRing, err) + return + } + log.Printf("Generated new identity with keyID %q in file %s", keyID, secRing) + default: + err = fmt.Errorf("Could not stat secret ring %q: %v", secRing, err) + } + return +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/genconfig_test.go b/vendor/github.com/camlistore/camlistore/pkg/serverinit/genconfig_test.go new file mode 100644 index 00000000..095049ad --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/genconfig_test.go @@ -0,0 +1,46 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package serverinit + +import "testing" + +func TestParseUserHostPass(t *testing.T) { + tests := []struct { + in string + user, host, password string + }{ + {in: "foo"}, + {in: "foo@bar"}, + {"bob@server:pass", "bob", "server", "pass"}, + {"bob@server:3307:pass", "bob", "server:3307", "pass"}, + {"bob@server:pass:word", "bob", "server", "pass:word"}, + {"bob@server:9999999:word", "bob", "server", "9999999:word"}, + {"bob@server:123:123:word", "bob", "server:123", "123:word"}, + {"bob@server:123", "bob", "server", "123"}, + {"bob@server:123:", "bob", "server:123", ""}, + } + for _, tt := range tests { + user, host, password, ok := parseUserHostPass(tt.in) + if ok != (user != "" || host != "" || password != "") { + t.Errorf("For input %q, inconsistent output %q, %q, %q, %v", tt.in, user, host, password, ok) + continue + } + if user != tt.user || host != tt.host || password != tt.password { + t.Errorf("parseUserHostPass(%q) = %q, %q, %q; want %q, %q, %q", tt.in, user, host, password, tt.user, tt.host, tt.password) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/serverinit.go b/vendor/github.com/camlistore/camlistore/pkg/serverinit/serverinit.go new file mode 100644 index 00000000..6c1379b4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/serverinit.go @@ -0,0 +1,714 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package serverinit is responsible for mapping from a Camlistore +// configuration file and instantiating HTTP Handlers for all the +// necessary endpoints. +package serverinit + +import ( + "bytes" + "encoding/json" + "errors" + "expvar" + "fmt" + "io" + "log" + "net" + "net/http" + "net/http/pprof" + "os" + "regexp" + "runtime" + rpprof "runtime/pprof" + "strconv" + "strings" + + "camlistore.org/pkg/auth" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/blobserver/handlers" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/index" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/server" + "camlistore.org/pkg/server/app" + "camlistore.org/pkg/types/serverconfig" +) + +const camliPrefix = "/camli/" + +var ErrCamliPath = errors.New("Invalid Camlistore request path") + +type handlerConfig struct { + prefix string // "/foo/" + htype string // "localdisk", etc + conf jsonconfig.Obj // never nil + internal bool // if true, not accessible over HTTP + + settingUp, setupDone bool +} + +type handlerLoader struct { + installer HandlerInstaller + baseURL string + config map[string]*handlerConfig // prefix -> config + handler map[string]interface{} // prefix -> http.Handler / func / blobserver.Storage + curPrefix string + closers []io.Closer + prefixStack []string + reindex bool + + // optional context (for App Engine, the first request that + // started up the process). we may need this if setting up + // handlers involves doing datastore/memcache/blobstore + // lookups. + context *http.Request +} + +// A HandlerInstaller is anything that can register an HTTP Handler at +// a prefix path. Both *http.ServeMux and camlistore.org/pkg/webserver.Server +// implement HandlerInstaller. +type HandlerInstaller interface { + Handle(path string, h http.Handler) +} + +type storageAndConfig struct { + blobserver.Storage + config *blobserver.Config +} + +// parseCamliPath looks for "/camli/" in the path and returns +// what follows it (the action). +func parseCamliPath(path string) (action string, err error) { + camIdx := strings.Index(path, camliPrefix) + if camIdx == -1 { + return "", ErrCamliPath + } + action = path[camIdx+len(camliPrefix):] + return +} + +func unsupportedHandler(conn http.ResponseWriter, req *http.Request) { + httputil.BadRequestError(conn, "Unsupported camlistore path or method.") +} + +func (s *storageAndConfig) Config() *blobserver.Config { + return s.config +} + +// GetStorage returns the unwrapped blobserver.Storage interface value for +// callers to type-assert optional interface implementations on. (e.g. EnumeratorConfig) +func (s *storageAndConfig) GetStorage() blobserver.Storage { + return s.Storage +} + +// action is the part following "/camli/" in the URL. It's either a +// string like "enumerate-blobs", "stat", "upload", or a blobref. +func camliHandlerUsingStorage(req *http.Request, action string, storage blobserver.StorageConfiger) (http.Handler, auth.Operation) { + var handler http.Handler + op := auth.OpAll + switch req.Method { + case "GET", "HEAD": + switch action { + case "enumerate-blobs": + handler = handlers.CreateEnumerateHandler(storage) + op = auth.OpGet + case "stat": + handler = handlers.CreateStatHandler(storage) + case "ws": + handler = nil // TODO: handlers.CreateSocketHandler(storage) + op = auth.OpDiscovery // rest of operation auth checks done in handler + default: + handler = handlers.CreateGetHandler(storage) + op = auth.OpGet + } + case "POST": + switch action { + case "stat": + handler = handlers.CreateStatHandler(storage) + op = auth.OpStat + case "upload": + handler = handlers.CreateBatchUploadHandler(storage) + op = auth.OpUpload + case "remove": + handler = handlers.CreateRemoveHandler(storage) + } + case "PUT": + handler = handlers.CreatePutUploadHandler(storage) + op = auth.OpUpload + } + if handler == nil { + handler = http.HandlerFunc(unsupportedHandler) + } + return handler, op +} + +// where prefix is like "/" or "/s3/" for e.g. "/camli/" or "/s3/camli/*" +func makeCamliHandler(prefix, baseURL string, storage blobserver.Storage, hf blobserver.FindHandlerByTyper) http.Handler { + if !strings.HasSuffix(prefix, "/") { + panic("expected prefix to end in slash") + } + baseURL = strings.TrimRight(baseURL, "/") + + canLongPoll := true + // TODO(bradfitz): set to false if this is App Engine, or provide some way to disable + + storageConfig := &storageAndConfig{ + storage, + &blobserver.Config{ + Writable: true, + Readable: true, + Deletable: false, + URLBase: baseURL + prefix[:len(prefix)-1], + CanLongPoll: canLongPoll, + HandlerFinder: hf, + }, + } + return http.HandlerFunc(func(conn http.ResponseWriter, req *http.Request) { + action, err := parseCamliPath(req.URL.Path[len(prefix)-1:]) + if err != nil { + log.Printf("Invalid request for method %q, path %q", + req.Method, req.URL.Path) + unsupportedHandler(conn, req) + return + } + handler := auth.RequireAuth(camliHandlerUsingStorage(req, action, storageConfig)) + handler.ServeHTTP(conn, req) + }) +} + +func (hl *handlerLoader) FindHandlerByType(htype string) (prefix string, handler interface{}, err error) { + nFound := 0 + for pfx, config := range hl.config { + if config.htype == htype { + nFound++ + prefix, handler = pfx, hl.handler[pfx] + } + } + if nFound == 0 { + return "", nil, blobserver.ErrHandlerTypeNotFound + } + if htype == "jsonsign" && nFound > 1 { + // TODO: do this for all handler types later? audit + // callers of FindHandlerByType and see if that's + // feasible. For now I'm only paranoid about jsonsign. + return "", nil, fmt.Errorf("%d handlers found of type %q; ambiguous", nFound, htype) + } + return +} + +func (hl *handlerLoader) AllHandlers() (types map[string]string, handlers map[string]interface{}) { + types = make(map[string]string) + handlers = make(map[string]interface{}) + for pfx, config := range hl.config { + types[pfx] = config.htype + handlers[pfx] = hl.handler[pfx] + } + return +} + +func (hl *handlerLoader) setupAll() { + for prefix := range hl.config { + hl.setupHandler(prefix) + } +} + +func (hl *handlerLoader) configType(prefix string) string { + if h, ok := hl.config[prefix]; ok { + return h.htype + } + return "" +} + +func (hl *handlerLoader) getOrSetup(prefix string) interface{} { + hl.setupHandler(prefix) + return hl.handler[prefix] +} + +func (hl *handlerLoader) MyPrefix() string { + return hl.curPrefix +} + +func (hl *handlerLoader) BaseURL() string { + return hl.baseURL +} + +func (hl *handlerLoader) GetStorage(prefix string) (blobserver.Storage, error) { + hl.setupHandler(prefix) + if s, ok := hl.handler[prefix].(blobserver.Storage); ok { + return s, nil + } + return nil, fmt.Errorf("bogus storage handler referenced as %q", prefix) +} + +func (hl *handlerLoader) GetHandler(prefix string) (interface{}, error) { + hl.setupHandler(prefix) + if s, ok := hl.handler[prefix].(blobserver.Storage); ok { + return s, nil + } + if h, ok := hl.handler[prefix].(http.Handler); ok { + return h, nil + } + return nil, fmt.Errorf("bogus http or storage handler referenced as %q", prefix) +} + +func (hl *handlerLoader) GetHandlerType(prefix string) string { + return hl.configType(prefix) +} + +func exitFailure(pattern string, args ...interface{}) { + if !strings.HasSuffix(pattern, "\n") { + pattern = pattern + "\n" + } + panic(fmt.Sprintf(pattern, args...)) +} + +func (hl *handlerLoader) setupHandler(prefix string) { + h, ok := hl.config[prefix] + if !ok { + exitFailure("invalid reference to undefined handler %q", prefix) + } + if h.setupDone { + // Already setup by something else reference it and forcing it to be + // setup before the bottom loop got to it. + return + } + hl.prefixStack = append(hl.prefixStack, prefix) + if h.settingUp { + buf := make([]byte, 1024) + buf = buf[:runtime.Stack(buf, false)] + exitFailure("loop in configuration graph; %q tried to load itself indirectly: %q\nStack:\n%s", + prefix, hl.prefixStack, buf) + } + h.settingUp = true + defer func() { + // log.Printf("Configured handler %q", prefix) + h.setupDone = true + hl.prefixStack = hl.prefixStack[:len(hl.prefixStack)-1] + r := recover() + if r == nil { + if hl.handler[prefix] == nil { + panic(fmt.Sprintf("setupHandler for %q didn't install a handler", prefix)) + } + } else { + panic(r) + } + }() + + hl.curPrefix = prefix + + if strings.HasPrefix(h.htype, "storage-") { + stype := strings.TrimPrefix(h.htype, "storage-") + // Assume a storage interface + pstorage, err := blobserver.CreateStorage(stype, hl, h.conf) + if err != nil { + exitFailure("error instantiating storage for prefix %q, type %q: %v", + h.prefix, stype, err) + } + if ix, ok := pstorage.(*index.Index); ok && hl.reindex { + log.Printf("Reindexing %s ...", h.prefix) + if err := ix.Reindex(); err != nil { + exitFailure("Error reindexing %s: %v", h.prefix, err) + } + } + hl.handler[h.prefix] = pstorage + if h.internal { + hl.installer.Handle(prefix, unauthorizedHandler{}) + } else { + hl.installer.Handle(prefix+"camli/", makeCamliHandler(prefix, hl.baseURL, pstorage, hl)) + } + if cl, ok := pstorage.(blobserver.ShutdownStorage); ok { + hl.closers = append(hl.closers, cl) + } + return + } + + var hh http.Handler + if h.htype == "app" { + ap, err := app.NewHandler(h.conf, hl.baseURL+"/", prefix) + if err != nil { + exitFailure("error setting up app for prefix %q: %v", h.prefix, err) + } + hh = ap + auth.AddMode(ap.AuthMode()) + if ap.ProgramName() == "publisher" { + if err := hl.initPublisherRootNode(ap); err != nil { + exitFailure("Error looking/setting up root node for publisher on %v: %v", h.prefix, err) + } + } + } else { + var err error + hh, err = blobserver.CreateHandler(h.htype, hl, h.conf) + if err != nil { + exitFailure("error instantiating handler for prefix %q, type %q: %v", + h.prefix, h.htype, err) + } + } + + hl.handler[prefix] = hh + var wrappedHandler http.Handler + if h.internal { + wrappedHandler = unauthorizedHandler{} + } else { + wrappedHandler = &httputil.PrefixHandler{prefix, hh} + if handlerTypeWantsAuth(h.htype) { + wrappedHandler = auth.Handler{wrappedHandler} + } + } + hl.installer.Handle(prefix, wrappedHandler) +} + +type unauthorizedHandler struct{} + +func (unauthorizedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Unauthorized", http.StatusUnauthorized) +} + +func handlerTypeWantsAuth(handlerType string) bool { + // TODO(bradfitz): ask the handler instead? This is a bit of a + // weird spot for this policy maybe? + switch handlerType { + case "ui", "search", "jsonsign", "sync", "status", "help", "importer": + return true + } + return false +} + +// A Config is the wrapper around a Camlistore JSON configuration file. +// Files on disk can be in either high-level or low-level format, but +// the Load function always returns the Config in its low-level format. +type Config struct { + jsonconfig.Obj + UIPath string // Not valid until after InstallHandlers + + // apps is the list of server apps configured during InstallHandlers, + // and that should be started after camlistored has started serving. + apps []*app.Handler +} + +// detectConfigChange returns an informative error if conf contains obsolete keys. +func detectConfigChange(conf jsonconfig.Obj) error { + oldHTTPSKey, oldHTTPSCert := conf.OptionalString("HTTPSKeyFile", ""), conf.OptionalString("HTTPSCertFile", "") + if oldHTTPSKey != "" || oldHTTPSCert != "" { + return fmt.Errorf("Config keys %q and %q have respectively been renamed to %q and %q, please fix your server config.", + "HTTPSKeyFile", "HTTPSCertFile", "httpsKey", "httpsCert") + } + return nil +} + +// LoadFile returns a low-level "handler config" from the provided filename. +// If the config file doesn't contain a top-level JSON key of "handlerConfig" +// with boolean value true, the configuration is assumed to be a high-level +// "user config" file, and transformed into a low-level config. +func LoadFile(filename string) (*Config, error) { + return load(filename, nil) +} + +type jsonFileImpl struct { + *bytes.Reader + name string +} + +func (jsonFileImpl) Close() error { return nil } +func (f jsonFileImpl) Name() string { return f.name } + +// Load returns a low-level "handler config" from the provided config. +// If the config doesn't contain a top-level JSON key of "handlerConfig" +// with boolean value true, the configuration is assumed to be a high-level +// "user config" file, and transformed into a low-level config. +func Load(config []byte) (*Config, error) { + return load("", func(filename string) (jsonconfig.File, error) { + if filename != "" { + return nil, errors.New("JSON files with includes not supported with jsonconfig.Load") + } + return jsonFileImpl{bytes.NewReader(config), "config file"}, nil + }) +} + +func load(filename string, opener func(filename string) (jsonconfig.File, error)) (*Config, error) { + c := &jsonconfig.ConfigParser{Open: opener} + m, err := c.ReadFile(filename) + if err != nil { + return nil, err + } + obj := jsonconfig.Obj(m) + conf := &Config{ + Obj: obj, + } + + if lowLevel := obj.OptionalBool("handlerConfig", false); lowLevel { + return conf, nil + } + + // Check whether the high-level config uses the old names. + if err := detectConfigChange(obj); err != nil { + return nil, err + } + + // Because the original high-level config might have expanded + // through the use of functions, we re-encode the map back to + // JSON here so we can unmarshal it into the hiLevelConf + // struct later. + highExpandedJSON, err := json.Marshal(m) + if err != nil { + return nil, fmt.Errorf("Can't re-marshal high-level JSON config: %v", err) + } + + var hiLevelConf serverconfig.Config + if err := json.Unmarshal(highExpandedJSON, &hiLevelConf); err != nil { + return nil, fmt.Errorf("Could not unmarshal into a serverconfig.Config: %v", err) + } + + conf, err = genLowLevelConfig(&hiLevelConf) + if err != nil { + return nil, fmt.Errorf( + "failed to transform user config file into internal handler configuration: %v", + err) + } + if v, _ := strconv.ParseBool(os.Getenv("CAMLI_DEBUG_CONFIG")); v { + jsconf, _ := json.MarshalIndent(conf.Obj, "", " ") + log.Printf("From high-level config, generated low-level config: %s", jsconf) + } + return conf, nil +} + +func (config *Config) checkValidAuth() error { + authConfig := config.OptionalString("auth", "") + mode, err := auth.FromConfig(authConfig) + if err == nil { + auth.SetMode(mode) + } + return err +} + +// InstallHandlers creates and registers all the HTTP Handlers needed by config +// into the provided HandlerInstaller. +// +// baseURL is required and specifies the root of this webserver, without trailing slash. +// context may be nil (used and required by App Engine only) +// +// The returned shutdown value can be used to cleanly shut down the +// handlers. +func (config *Config) InstallHandlers(hi HandlerInstaller, baseURL string, reindex bool, context *http.Request) (shutdown io.Closer, err error) { + defer func() { + if e := recover(); e != nil { + log.Printf("Caught panic installer handlers: %v", e) + err = fmt.Errorf("Caught panic: %v", e) + } + }() + + if err := config.checkValidAuth(); err != nil { + return nil, fmt.Errorf("error while configuring auth: %v", err) + } + prefixes := config.RequiredObject("prefixes") + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("configuration error in root object's keys: %v", err) + } + + if v := os.Getenv("CAMLI_PPROF_START"); v != "" { + cpuf := mustCreate(v + ".cpu") + defer cpuf.Close() + memf := mustCreate(v + ".mem") + defer memf.Close() + rpprof.StartCPUProfile(cpuf) + defer rpprof.StopCPUProfile() + defer rpprof.WriteHeapProfile(memf) + } + + hl := &handlerLoader{ + installer: hi, + baseURL: baseURL, + config: make(map[string]*handlerConfig), + handler: make(map[string]interface{}), + context: context, + reindex: reindex, + } + + for prefix, vei := range prefixes { + if !strings.HasPrefix(prefix, "/") { + exitFailure("prefix %q doesn't start with /", prefix) + } + if !strings.HasSuffix(prefix, "/") { + exitFailure("prefix %q doesn't end with /", prefix) + } + pmap, ok := vei.(map[string]interface{}) + if !ok { + exitFailure("prefix %q value is a %T, not an object", prefix, vei) + } + pconf := jsonconfig.Obj(pmap) + enabled := pconf.OptionalBool("enabled", true) + if !enabled { + continue + } + handlerType := pconf.RequiredString("handler") + handlerArgs := pconf.OptionalObject("handlerArgs") + internal := pconf.OptionalBool("internal", false) + if err := pconf.Validate(); err != nil { + exitFailure("configuration error in prefix %s: %v", prefix, err) + } + h := &handlerConfig{ + prefix: prefix, + htype: handlerType, + conf: handlerArgs, + internal: internal, + } + hl.config[prefix] = h + + if handlerType == "ui" { + config.UIPath = prefix + } + } + hl.setupAll() + + // Now that everything is setup, run any handlers' InitHandler + // methods. + // And register apps that will be started later. + for pfx, handler := range hl.handler { + if starter, ok := handler.(*app.Handler); ok { + config.apps = append(config.apps, starter) + } + if helpHandler, ok := handler.(*server.HelpHandler); ok { + helpHandler.SetServerConfig(config.Obj) + } + if in, ok := handler.(blobserver.HandlerIniter); ok { + if err := in.InitHandler(hl); err != nil { + return nil, fmt.Errorf("Error calling InitHandler on %s: %v", pfx, err) + } + } + } + + if v, _ := strconv.ParseBool(os.Getenv("CAMLI_HTTP_EXPVAR")); v { + hi.Handle("/debug/vars", expvarHandler{}) + } + if v, _ := strconv.ParseBool(os.Getenv("CAMLI_HTTP_PPROF")); v { + hi.Handle("/debug/pprof/", profileHandler{}) + } + hi.Handle("/debug/config", auth.RequireAuth(configHandler{config}, auth.OpAll)) + hi.Handle("/debug/logs", auth.RequireAuth(http.HandlerFunc(logsHandler), auth.OpAll)) + return multiCloser(hl.closers), nil +} + +// StartApps starts all the server applications that were configured +// during InstallHandlers. It should only be called after camlistored +// has started serving, since these apps might request some configuration +// from Camlistore to finish initializing. +func (config *Config) StartApps() error { + for _, ap := range config.apps { + if err := ap.Start(); err != nil { + return fmt.Errorf("error starting app %v: %v", ap.ProgramName(), err) + } + } + return nil +} + +// AppURL returns a map of app name to app base URL for all the configured +// server apps. +func (config *Config) AppURL() map[string]string { + appURL := make(map[string]string, len(config.apps)) + for _, ap := range config.apps { + appURL[ap.ProgramName()] = ap.BackendURL() + } + return appURL +} + +func mustCreate(path string) *os.File { + f, err := os.Create(path) + if err != nil { + log.Fatalf("Failed to create %s: %v", path, err) + } + return f +} + +type multiCloser []io.Closer + +func (s multiCloser) Close() (err error) { + for _, cl := range s { + if err1 := cl.Close(); err == nil && err1 != nil { + err = err1 + } + } + return +} + +// expvarHandler publishes expvar stats. +type expvarHandler struct{} + +func (expvarHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + fmt.Fprintf(w, "{\n") + first := true + expvar.Do(func(kv expvar.KeyValue) { + if !first { + fmt.Fprintf(w, ",\n") + } + first = false + fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value) + }) + fmt.Fprintf(w, "\n}\n") +} + +type configHandler struct { + c *Config +} + +var ( + knownKeys = regexp.MustCompile(`(?ms)^\s+"_knownkeys": {.+?},?\n`) + sensitiveLine = regexp.MustCompile(`(?m)^\s+\"(auth|aws_secret_access_key|password)\": "[^\"]+".*\n`) +) + +func (h configHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + b, _ := json.MarshalIndent(h.c.Obj, "", " ") + b = knownKeys.ReplaceAll(b, nil) + b = sensitiveLine.ReplaceAllFunc(b, func(ln []byte) []byte { + i := bytes.IndexByte(ln, ':') + return []byte(string(ln[:i+1]) + " REDACTED\n") + }) + w.Write(b) +} + +// profileHandler publishes server profile information. +type profileHandler struct{} + +func (profileHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/debug/pprof/cmdline": + pprof.Cmdline(rw, req) + case "/debug/pprof/profile": + pprof.Profile(rw, req) + case "/debug/pprof/symbol": + pprof.Symbol(rw, req) + default: + pprof.Index(rw, req) + } +} + +func logsHandler(w http.ResponseWriter, r *http.Request) { + c := &http.Client{ + Transport: &http.Transport{ + Dial: func(network, addr string) (net.Conn, error) { + return net.Dial("unix", "/run/camjournald.sock") + }, + }, + } + res, err := c.Get("http://journal/entries") + if err != nil { + http.Error(w, err.Error(), 500) + return + } + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + io.Copy(w, res.Body) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/serverinit_test.go b/vendor/github.com/camlistore/camlistore/pkg/serverinit/serverinit_test.go new file mode 100644 index 00000000..252e5cdf --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/serverinit_test.go @@ -0,0 +1,478 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package serverinit_test + +import ( + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "reflect" + "regexp" + "runtime" + "sort" + "strings" + "testing" + + "camlistore.org/pkg/auth" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/importer" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/jsonsign/signhandler" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/search" + "camlistore.org/pkg/server" + "camlistore.org/pkg/serverinit" + "camlistore.org/pkg/test" + "camlistore.org/pkg/types/clientconfig" + "camlistore.org/pkg/types/serverconfig" + + // For registering all the handler constructors needed in TestInstallHandlers + _ "camlistore.org/pkg/blobserver/cond" + _ "camlistore.org/pkg/blobserver/replica" + _ "camlistore.org/pkg/importer/allimporters" + _ "camlistore.org/pkg/search" + _ "camlistore.org/pkg/server" +) + +var ( + updateGolden = flag.Bool("update_golden", false, "Update golden *.want files") + flagOnly = flag.String("only", "", "If non-empty, substring of foo.json input file to match.") +) + +const ( + // relativeRing points to a real secret ring, but serverinit + // rewrites it to be an absolute path. We then canonicalize + // it to secringPlaceholder in the golden files. + relativeRing = "../jsonsign/testdata/test-secring.gpg" + secringPlaceholder = "/path/to/secring" +) + +func init() { + // Avoid Linux vs. OS X differences in tests. + serverinit.SetTempDirFunc(func() string { return "/tmp" }) + serverinit.SetNoMkdir(true) +} + +func sortedKeys(m map[string]interface{}) (keys []string) { + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return +} + +func prettyPrint(t *testing.T, w io.Writer, v interface{}) { + out, err := json.MarshalIndent(v, "", " ") + if err != nil { + t.Fatal(err) + } + w.Write(out) +} + +func TestConfigs(t *testing.T) { + dir, err := os.Open("testdata") + if err != nil { + t.Fatal(err) + } + names, err := dir.Readdirnames(-1) + if err != nil { + t.Fatal(err) + } + for _, name := range names { + if strings.HasPrefix(name, ".#") { + // Emacs noise. + continue + } + if *flagOnly != "" && !strings.Contains(name, *flagOnly) { + continue + } + if strings.HasSuffix(name, ".json") { + if strings.HasSuffix(name, "-want.json") { + continue + } + testConfig(filepath.Join("testdata", name), t) + } + } +} + +type namedReadSeeker struct { + name string + io.ReadSeeker +} + +func (n namedReadSeeker) Name() string { return n.name } +func (n namedReadSeeker) Close() error { return nil } + +// configParser returns a custom jsonconfig ConfigParser whose reader rewrites +// "/path/to/secring" to the absolute path of the jsonconfig test-secring.gpg file. +// On windows, it also fixes the slash separated paths. +func configParser() *jsonconfig.ConfigParser { + return &jsonconfig.ConfigParser{ + Open: func(path string) (jsonconfig.File, error) { + slurp, err := replaceRingPath(path) + if err != nil { + return nil, err + } + slurp = backslashEscape(slurp) + return namedReadSeeker{path, bytes.NewReader(slurp)}, nil + }, + } +} + +// replaceRingPath returns the contents of the file at path with secringPlaceholder replaced with the absolute path of relativeRing. +func replaceRingPath(path string) ([]byte, error) { + secRing, err := filepath.Abs(relativeRing) + if err != nil { + return nil, fmt.Errorf("Could not get absolute path of %v: %v", relativeRing, err) + } + secRing = strings.Replace(secRing, `\`, `\\`, -1) + slurpBytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + return bytes.Replace(slurpBytes, []byte(secringPlaceholder), []byte(secRing), 1), nil +} + +// We just need to make sure that we don't match the prefix handlers too. +var unixPathPattern = regexp.MustCompile(`"/.*/.+"`) + +// backslashEscape, on windows, changes all the slash separated paths (which +// match unixPathPattern, to omit the prefix handler paths) with escaped +// backslashes. +func backslashEscape(b []byte) []byte { + if runtime.GOOS != "windows" { + return b + } + unixPaths := unixPathPattern.FindAll(b, -1) + if unixPaths == nil { + return b + } + var oldNew []string + for _, v := range unixPaths { + bStr := string(v) + oldNew = append(oldNew, bStr, strings.Replace(bStr, `/`, `\\`, -1)) + } + r := strings.NewReplacer(oldNew...) + return []byte(r.Replace(string(b))) +} + +func testConfig(name string, t *testing.T) { + wantedError := func() error { + slurp, err := ioutil.ReadFile(strings.Replace(name, ".json", ".err", 1)) + if os.IsNotExist(err) { + return nil + } + if err != nil { + t.Fatalf("Error reading .err file: %v", err) + } + return errors.New(string(slurp)) + } + b, err := replaceRingPath(name) + if err != nil { + t.Fatalf("Could not read %s: %v", name, err) + } + b = backslashEscape(b) + var hiLevelConf serverconfig.Config + if err := json.Unmarshal(b, &hiLevelConf); err != nil { + t.Fatalf("Could not unmarshal %s into a serverconfig.Config: %v", name, err) + } + + lowLevelConf, err := serverinit.GenLowLevelConfig(&hiLevelConf) + if g, w := strings.TrimSpace(fmt.Sprint(err)), strings.TrimSpace(fmt.Sprint(wantedError())); g != w { + t.Fatalf("test %s: got GenLowLevelConfig error %q; want %q", name, g, w) + } + if err != nil { + return + } + if err := (&jsonconfig.ConfigParser{}).CheckTypes(lowLevelConf.Obj); err != nil { + t.Fatalf("Error while parsing low-level conf generated from %v: %v", name, err) + } + + // TODO(mpl): should we stop execution (and not update golden files) + // if the comparison fails? Currently this is not the case. + wantFile := strings.Replace(name, ".json", "-want.json", 1) + wantConf, err := configParser().ReadFile(wantFile) + if err != nil { + t.Fatalf("test %s: ReadFile: %v", name, err) + } + if *updateGolden { + contents, err := json.MarshalIndent(lowLevelConf.Obj, "", "\t") + if err != nil { + t.Fatal(err) + } + contents = canonicalizeGolden(t, contents) + if err := ioutil.WriteFile(wantFile, contents, 0644); err != nil { + t.Fatal(err) + } + } + compareConfigurations(t, name, lowLevelConf.Obj, wantConf) +} + +func compareConfigurations(t *testing.T, name, g interface{}, w interface{}) { + var got, want bytes.Buffer + prettyPrint(t, &got, g) + prettyPrint(t, &want, w) + + if got.String() != want.String() { + t.Errorf("test %s configurations differ.\nGot:\n%s\nWant:\n%s\nDiff (want -> got), %s:\n%s", + name, &got, &want, name, test.Diff(want.Bytes(), got.Bytes())) + } +} + +func canonicalizeGolden(t *testing.T, v []byte) []byte { + localPath, err := filepath.Abs(relativeRing) + if err != nil { + t.Fatal(err) + } + v = bytes.Replace(v, []byte(localPath), []byte(secringPlaceholder), 1) + if !bytes.HasSuffix(v, []byte("\n")) { + v = append(v, '\n') + } + return v +} + +func TestExpansionsInHighlevelConfig(t *testing.T) { + camroot, err := osutil.GoPackagePath("camlistore.org") + if err != nil { + t.Fatalf("failed to find camlistore.org GOPATH root: %v", err) + } + const keyID = "26F5ABDA" + os.Setenv("TMP_EXPANSION_TEST", keyID) + os.Setenv("TMP_EXPANSION_SECRING", filepath.Join(camroot, filepath.FromSlash("pkg/jsonsign/testdata/test-secring.gpg"))) + conf, err := serverinit.Load([]byte(` +{ + "auth": "localhost", + "listen": ":4430", + "https": false, + "identity": ["_env", "${TMP_EXPANSION_TEST}"], + "identitySecretRing": ["_env", "${TMP_EXPANSION_SECRING}"], + "googlecloudstorage": ":camlistore-dev-blobs", + "kvIndexFile": "/tmp/camli-index.kvdb" +} +`)) + if err != nil { + t.Fatal(err) + } + got := fmt.Sprintf("%#v", conf) + if !strings.Contains(got, keyID) { + t.Errorf("Expected key %s in resulting low-level config. Got: %s", keyID, got) + } +} + +func TestInstallHandlers(t *testing.T) { + camroot, err := osutil.GoPackagePath("camlistore.org") + if err != nil { + t.Fatalf("failed to find camlistore.org GOPATH root: %v", err) + } + conf := serverinit.DefaultBaseConfig + conf.Identity = "26F5ABDA" + conf.IdentitySecretRing = filepath.Join(camroot, filepath.FromSlash("pkg/jsonsign/testdata/test-secring.gpg")) + conf.MemoryStorage = true + conf.MemoryIndex = true + + confData, err := json.MarshalIndent(conf, "", " ") + if err != nil { + t.Fatalf("Could not json encode config: %v", err) + } + + lowConf, err := serverinit.Load(confData) + if err != nil { + t.Fatal(err) + } + // because these two are normally consumed in camlistored.go + // TODO(mpl): serverinit.Load should consume these 2 as well. Once + // consumed, we should keep all the answers as private fields, and then we + // put accessors on serverinit.Config. Maybe we even stop embedding + // jsonconfig.Obj in serverinit.Config too, so none of those methods are + // accessible. + lowConf.OptionalBool("https", true) + lowConf.OptionalString("listen", "") + + reindex := false + var context *http.Request // only used by App Engine. See handlerLoader in serverinit.go + hi := http.NewServeMux() + address := "http://" + conf.Listen + _, err = lowConf.InstallHandlers(hi, address, reindex, context) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + prefix string + authWrapped bool + prefixWrapped bool + handlerType reflect.Type + }{ + { + prefix: "/", + handlerType: reflect.TypeOf(&server.RootHandler{}), + prefixWrapped: true, + }, + + { + prefix: "/sync/", + handlerType: reflect.TypeOf(&server.SyncHandler{}), + prefixWrapped: true, + authWrapped: true, + }, + + { + prefix: "/my-search/", + handlerType: reflect.TypeOf(&search.Handler{}), + prefixWrapped: true, + authWrapped: true, + }, + + { + prefix: "/ui/", + handlerType: reflect.TypeOf(&server.UIHandler{}), + prefixWrapped: true, + authWrapped: true, + }, + + { + prefix: "/importer/", + handlerType: reflect.TypeOf(&importer.Host{}), + prefixWrapped: true, + authWrapped: true, + }, + + { + prefix: "/sighelper/", + handlerType: reflect.TypeOf(&signhandler.Handler{}), + prefixWrapped: true, + authWrapped: true, + }, + + { + prefix: "/status/", + handlerType: reflect.TypeOf(&server.StatusHandler{}), + prefixWrapped: true, + authWrapped: true, + }, + + { + prefix: "/help/", + handlerType: reflect.TypeOf(&server.HelpHandler{}), + prefixWrapped: true, + authWrapped: true, + }, + + { + prefix: "/setup/", + handlerType: reflect.TypeOf(&server.SetupHandler{}), + prefixWrapped: true, + }, + + { + prefix: "/bs/camli/", + handlerType: reflect.TypeOf(http.HandlerFunc(nil)), + }, + + { + prefix: "/index/camli/", + handlerType: reflect.TypeOf(http.HandlerFunc(nil)), + }, + + { + prefix: "/bs-and-index/camli/", + handlerType: reflect.TypeOf(http.HandlerFunc(nil)), + }, + + { + prefix: "/bs-and-maybe-also-index/camli/", + handlerType: reflect.TypeOf(http.HandlerFunc(nil)), + }, + + { + prefix: "/cache/camli/", + handlerType: reflect.TypeOf(http.HandlerFunc(nil)), + }, + } + for _, v := range tests { + req, err := http.NewRequest("GET", address+v.prefix, nil) + if err != nil { + t.Error(err) + continue + } + h, _ := hi.Handler(req) + if v.authWrapped { + ah, ok := h.(auth.Handler) + if !ok { + t.Errorf("handler for %v should be auth wrapped", v.prefix) + continue + } + h = ah.Handler + } + if v.prefixWrapped { + ph, ok := h.(*httputil.PrefixHandler) + if !ok { + t.Errorf("handler for %v should be prefix wrapped", v.prefix) + continue + } + h = ph.Handler + } + if reflect.TypeOf(h) != v.handlerType { + t.Errorf("for %v: want %v, got %v", v.prefix, v.handlerType, reflect.TypeOf(h)) + } + } +} + +// TestGenerateClientConfig validates the client config generated for display +// by the HelpHandler. +func TestGenerateClientConfig(t *testing.T) { + inName := filepath.Join("testdata", "gen_client_config.in") + wantName := strings.Replace(inName, ".in", ".out", 1) + + b, err := replaceRingPath(inName) + if err != nil { + t.Fatalf("Failed to read high-level server config file: %v", err) + } + b = backslashEscape(b) + var hiLevelConf serverconfig.Config + if err := json.Unmarshal(b, &hiLevelConf); err != nil { + t.Fatalf("Failed to unmarshal server config: %v", err) + } + lowLevelConf, err := serverinit.GenLowLevelConfig(&hiLevelConf) + if err != nil { + t.Fatalf("Failed to generate low-level config: %v", err) + } + generatedConf, err := clientconfig.GenerateClientConfig(lowLevelConf.Obj) + if err != nil { + t.Fatalf("Failed to generate client config: %v", err) + } + + wb, err := replaceRingPath(wantName) + if err != nil { + t.Fatalf("Failed to read want config file: %v", err) + } + wb = backslashEscape(wb) + var wantConf clientconfig.Config + if err := json.Unmarshal(wb, &wantConf); err != nil { + t.Fatalf("Failed to unmarshall want config: %v", err) + } + + compareConfigurations(t, inName, generatedConf, wantConf) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurl-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurl-want.json new file mode 100644 index 00000000..0ca85d3d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurl-want.json @@ -0,0 +1,118 @@ +{ + "auth": "userpass:camlistore:pass3179", + "baseURL": "http://monkey.foo.com", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "ownerName": "Alice", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.kv", + "type": "kv" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurl.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurl.json new file mode 100644 index 00000000..b53d050d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurl.json @@ -0,0 +1,11 @@ +{ + "listen": "localhost:3179", + "auth": "userpass:camlistore:pass3179", + "baseURL": "http://monkey.foo.com", + "blobPath": "/tmp/blobs", + "kvIndexFile": "/path/to/indexkv.db", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "ownerName": "Alice", + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurlbad.err b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurlbad.err new file mode 100644 index 00000000..f1733a00 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurlbad.err @@ -0,0 +1 @@ +baseURL can't have a path, only a scheme, host, and optional port. \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurlbad.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurlbad.json new file mode 100644 index 00000000..e0eaf495 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/baseurlbad.json @@ -0,0 +1,10 @@ +{ + "listen": "localhost:3179", + "baseURL": "http://foo.com/bar/", + "https": false, + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_googlecloud-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_googlecloud-want.json new file mode 100644 index 00000000..ee8a7669 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_googlecloud-want.json @@ -0,0 +1,156 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "ownerName": "Alice", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-blobpacked", + "handlerArgs": { + "smallBlobs": "/bs-loose/", + "largeBlobs": "/bs-packed/", + "metaIndex": { + "database": "blobpacked_index", + "host": "localhost", + "password": "root", + "type": "mysql", + "user": "root" + } + } + }, + "/bs-loose/": { + "handler": "storage-googlecloudstorage", + "handlerArgs": { + "auth": { + "client_id": "clientId", + "client_secret": "clientSecret", + "refresh_token": "refreshToken" + }, + "bucket": "bucketName/blobs/loose" + } + }, + "/bs-packed/": { + "handler": "storage-googlecloudstorage", + "handlerArgs": { + "auth": { + "client_id": "clientId", + "client_secret": "clientSecret", + "refresh_token": "refreshToken" + }, + "bucket": "bucketName/blobs/packed" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/camli-cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "database": "camlitest", + "host": "localhost", + "password": "root", + "type": "mysql", + "user": "root" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "database": "sync_index_queue", + "type": "mysql", + "host": "localhost", + "user": "root", + "password": "root" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "database": "ui_thumbmeta_cache", + "host": "localhost", + "type": "mysql", + "user": "root", + "password": "root" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_googlecloud.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_googlecloud.json new file mode 100644 index 00000000..f32df479 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_googlecloud.json @@ -0,0 +1,17 @@ +{ + "listen": "localhost:3179", + "auth": "userpass:camlistore:pass3179", + "googlecloudstorage": "clientId:clientSecret:refreshToken:bucketName/blobs", + "packRelated": true, + "dbNames": { + "index": "camlitest", + "queue-sync-to-index": "sync_index_queue", + "blobpacked_index": "blobpacked_index", + "ui_thumbcache": "ui_thumbmeta_cache" + }, + "mysql": "root@localhost:root", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "ownerName": "Alice", + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_localdisk-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_localdisk-want.json new file mode 100644 index 00000000..472a3267 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_localdisk-want.json @@ -0,0 +1,134 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "ownerName": "Alice", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-blobpacked", + "handlerArgs": { + "smallBlobs": "/bs-loose/", + "largeBlobs": "/bs-packed/", + "metaIndex": { + "file": "/tmp/blobs/packed/packindex.kv", + "type": "kv" + } + } + }, + "/bs-loose/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/bs-packed/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/packed" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.kv", + "type": "kv" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_localdisk.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_localdisk.json new file mode 100644 index 00000000..7abf7cc8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/blobpacked_localdisk.json @@ -0,0 +1,11 @@ +{ + "listen": "localhost:3179", + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "packRelated": true, + "kvIndexFile": "/path/to/indexkv.db", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "ownerName": "Alice", + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/default-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/default-want.json new file mode 100644 index 00000000..41800bb6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/default-want.json @@ -0,0 +1,117 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "ownerName": "Alice", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.kv", + "type": "kv" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/default.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/default.json new file mode 100644 index 00000000..f15fc9ff --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/default.json @@ -0,0 +1,10 @@ +{ + "listen": "localhost:3179", + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "kvIndexFile": "/path/to/indexkv.db", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "ownerName": "Alice", + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/diskpacked-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/diskpacked-want.json new file mode 100644 index 00000000..03aadf28 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/diskpacked-want.json @@ -0,0 +1,125 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "ownerName": "Alice", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-diskpacked", + "handlerArgs": { + "path": "/tmp/blobs", + "metaIndex": { + "file": "/tmp/blobs/index.kv", + "type": "kv" + } + } + }, + "/cache/": { + "handler": "storage-diskpacked", + "handlerArgs": { + "metaIndex": { + "file": "/tmp/blobs/cache/index.kv", + "type": "kv" + }, + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.kv", + "type": "kv" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/diskpacked.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/diskpacked.json new file mode 100644 index 00000000..4e904cd6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/diskpacked.json @@ -0,0 +1,11 @@ +{ + "listen": "localhost:3179", + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "packBlobs": true, + "kvIndexFile": "/path/to/indexkv.db", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "ownerName": "Alice", + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/flickr-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/flickr-want.json new file mode 100644 index 00000000..3fd6a7b4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/flickr-want.json @@ -0,0 +1,121 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "ownerName": "Alice", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": { + "flickr": { + "clientSecret": "monkey:balls" + } + } + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.kv", + "type": "kv" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/flickr.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/flickr.json new file mode 100644 index 00000000..2070bbf0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/flickr.json @@ -0,0 +1,11 @@ +{ + "listen": "localhost:3179", + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "flickr": "monkey:balls", + "kvIndexFile": "/path/to/indexkv.db", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "ownerName": "Alice", + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/gen_client_config.in b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/gen_client_config.in new file mode 100644 index 00000000..0b7c7a90 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/gen_client_config.in @@ -0,0 +1,9 @@ +{ + "listen": "1.2.3.4:567", + "https": false, + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db" +} \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/gen_client_config.out b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/gen_client_config.out new file mode 100644 index 00000000..192fc57f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/gen_client_config.out @@ -0,0 +1,14 @@ +{ + "servers": { + "default": { + "server": "http://1.2.3.4:567", + "auth": "userpass:camlistore:pass3179", + "default": true + } + }, + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "ignoredFiles": [ + ".DS_Store" + ] +} \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk-want.json new file mode 100644 index 00000000..ce59dbbb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk-want.json @@ -0,0 +1,117 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-googlecloudstorage", + "handlerArgs": { + "auth": { + "client_id": "clientId", + "client_secret": "clientSecret", + "refresh_token": "refreshToken" + }, + "bucket": "bucketName" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/camli-cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/path/to/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/" + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk.json new file mode 100644 index 00000000..aee30af0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk.json @@ -0,0 +1,12 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db", + "googlecloudstorage": "clientId:clientSecret:refreshToken:bucketName", + "replicateTo": [], + "publish": {}, + "shareHandler": true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk_subdir-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk_subdir-want.json new file mode 100644 index 00000000..697c48e9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk_subdir-want.json @@ -0,0 +1,117 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-googlecloudstorage", + "handlerArgs": { + "auth": { + "client_id": "clientId", + "client_secret": "clientSecret", + "refresh_token": "refreshToken" + }, + "bucket": "bucketName/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/camli-cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/path/to/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/" + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk_subdir.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk_subdir.json new file mode 100644 index 00000000..8250e694 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_nolocaldisk_subdir.json @@ -0,0 +1,12 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db", + "googlecloudstorage": "clientId:clientSecret:refreshToken:bucketName/blobs", + "replicateTo": [], + "publish": {}, + "shareHandler": true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_queues_on_db-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_queues_on_db-want.json new file mode 100644 index 00000000..ad45f27f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_queues_on_db-want.json @@ -0,0 +1,123 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-googlecloudstorage", + "handlerArgs": { + "auth": { + "client_id": "auto", + "client_secret": "", + "refresh_token": "" + }, + "bucket": "bucketName" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/camli-cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "database": "camindex", + "host": "camlistore.cloudsql.google.internal", + "password": "root", + "type": "mysql", + "user": "root" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "to": "/index/", + "queue": { + "database": "camindex_syncindex_q", + "host": "camlistore.cloudsql.google.internal", + "password": "root", + "type": "mysql", + "user": "root" + } + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/" + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_queues_on_db.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_queues_on_db.json new file mode 100644 index 00000000..e52aa5e4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_queues_on_db.json @@ -0,0 +1,14 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "mysql": "root@camlistore.cloudsql.google.internal:root", + "googlecloudstorage": ":bucketName", + "shareHandler": true, + "dbNames": { + "index": "camindex", + "queue-sync-to-index": "camindex_syncindex_q" + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account-want.json new file mode 100644 index 00000000..6d1aed82 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account-want.json @@ -0,0 +1,117 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-googlecloudstorage", + "handlerArgs": { + "auth": { + "client_id": "auto", + "client_secret": "", + "refresh_token": "" + }, + "bucket": "bucketName" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/camli-cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/path/to/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/" + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account.json new file mode 100644 index 00000000..cf056f69 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account.json @@ -0,0 +1,12 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db", + "googlecloudstorage": ":bucketName", + "replicateTo": [], + "publish": {}, + "shareHandler": true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account_subdir-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account_subdir-want.json new file mode 100644 index 00000000..ddd8f0f3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account_subdir-want.json @@ -0,0 +1,117 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-googlecloudstorage", + "handlerArgs": { + "auth": { + "client_id": "auto", + "client_secret": "", + "refresh_token": "" + }, + "bucket": "bucketName/config/" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/camli-cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/path/to/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/" + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account_subdir.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account_subdir.json new file mode 100644 index 00000000..0a18bcc6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/google_service_account_subdir.json @@ -0,0 +1,12 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db", + "googlecloudstorage": ":bucketName/config/", + "replicateTo": [], + "publish": {}, + "shareHandler": true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/justblobs-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/justblobs-want.json new file mode 100644 index 00000000..d1cef537 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/justblobs-want.json @@ -0,0 +1,52 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/justblobs.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/justblobs.json new file mode 100644 index 00000000..41107326 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/justblobs.json @@ -0,0 +1,10 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "runIndex": false, + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/leveldb-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/leveldb-want.json new file mode 100644 index 00000000..2c4ec435 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/leveldb-want.json @@ -0,0 +1,117 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "ownerName": "Alice", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexleveldb.ldb", + "type": "leveldb" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.leveldb", + "type": "leveldb" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.leveldb", + "type": "leveldb" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/leveldb.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/leveldb.json new file mode 100644 index 00000000..d087a866 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/leveldb.json @@ -0,0 +1,10 @@ +{ + "listen": "localhost:3179", + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "levelDB": "/path/to/indexleveldb.ldb", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "ownerName": "Alice", + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/listenbase-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/listenbase-want.json new file mode 100644 index 00000000..1e0ac957 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/listenbase-want.json @@ -0,0 +1,117 @@ +{ + "auth": "userpass:camlistore:pass3179", + "baseURL": "http://foo.com", + "https": false, + "listen": "1.2.3.4:80", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.kv", + "type": "kv" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/listenbase.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/listenbase.json new file mode 100644 index 00000000..c8e5554e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/listenbase.json @@ -0,0 +1,10 @@ +{ + "listen": "1.2.3.4:80", + "baseURL": "http://foo.com/", + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "kvIndexFile": "/path/to/indexkv.db", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "shareHandler": true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mem-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mem-want.json new file mode 100644 index 00000000..c9dceb90 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mem-want.json @@ -0,0 +1,180 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "ownerName": "Brad", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sto-googlecloudstorage/": { + "handler": "storage-googlecloudstorage", + "handlerArgs": { + "auth": { + "client_id": "clientId", + "client_secret": "clientSecret", + "refresh_token": "refreshToken" + }, + "bucket": "bucketName" + } + }, + "/sto-googledrive/": { + "handler": "storage-googledrive", + "handlerArgs": { + "auth": { + "client_id": "clientId", + "client_secret": "clientSecret", + "refresh_token": "refreshToken" + }, + "parent_id": "parentDirId" + } + }, + "/sto-s3/": { + "handler": "storage-s3", + "handlerArgs": { + "aws_access_key": "key", + "aws_secret_access_key": "secret", + "bucket": "bucket" + } + }, + "/sync-to-googlecloudstorage/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-googlecloud-queue.kv", + "type": "kv" + }, + "to": "/sto-googlecloudstorage/" + } + }, + "/sync-to-googledrive/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-googledrive-queue.kv", + "type": "kv" + }, + "to": "/sto-googledrive/" + } + }, + "/sync-to-s3/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-s3-queue.kv", + "type": "kv" + }, + "to": "/sto-s3/" + } + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.kv", + "type": "kv" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mem.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mem.json new file mode 100644 index 00000000..9bdb1ee9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mem.json @@ -0,0 +1,16 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db", + "s3": "key:secret:bucket", + "googlecloudstorage": "clientId:clientSecret:refreshToken:bucketName", + "googledrive": "clientId:clientSecret:refreshToken:parentDirId", + "replicateTo": [], + "publish": {}, + "ownerName": "Brad", + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memindex-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memindex-want.json new file mode 100644 index 00000000..9d22fe21 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memindex-want.json @@ -0,0 +1,116 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "ownerName": "Alice", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "type": "memory" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.leveldb", + "type": "leveldb" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.leveldb", + "type": "leveldb" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memindex.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memindex.json new file mode 100644 index 00000000..ae0fadc3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memindex.json @@ -0,0 +1,10 @@ +{ + "listen": "localhost:3179", + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "memoryIndex": true, + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "ownerName": "Alice", + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memory_storage-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memory_storage-want.json new file mode 100644 index 00000000..470c68a0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memory_storage-want.json @@ -0,0 +1,106 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "ownerName": "Alice", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-memory" + }, + "/cache/": { + "handler": "storage-memory" + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "type": "memory" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/" + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memory_storage.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memory_storage.json new file mode 100644 index 00000000..43905973 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/memory_storage.json @@ -0,0 +1,10 @@ +{ + "listen": "localhost:3179", + "auth": "userpass:camlistore:pass3179", + "memoryStorage": true, + "kvIndexFile": "/path/to/indexkv.db", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "ownerName": "Alice", + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mongo-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mongo-want.json new file mode 100644 index 00000000..4f14079d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mongo-want.json @@ -0,0 +1,120 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "ownerName": "Alice", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "database": "camlitest", + "host": "localhost", + "password": "", + "type": "mongo", + "user": "" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.leveldb", + "type": "leveldb" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.leveldb", + "type": "leveldb" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mongo.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mongo.json new file mode 100644 index 00000000..ff6bfd75 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/mongo.json @@ -0,0 +1,11 @@ +{ + "listen": "localhost:3179", + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "dbname": "camlitest", + "mongo": ":@localhost", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "ownerName": "Alice", + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/multipublish-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/multipublish-want.json new file mode 100644 index 00000000..f9a29e67 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/multipublish-want.json @@ -0,0 +1,140 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "ownerName": "Alice", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/music/": { + "handler": "app", + "handlerArgs": { + "program": "publisher", + "baseURL": "http://localhost:3178/", + "appConfig": { + "camliRoot": "musicRoot", + "goTemplate": "music.html", + "cacheRoot": "/tmp/blobs/cache" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/pics/": { + "handler": "app", + "handlerArgs": { + "program": "publisher", + "appConfig": { + "camliRoot": "picsRoot", + "goTemplate": "gallery.html", + "cacheRoot": "/tmp/blobs/cache" + } + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.kv", + "type": "kv" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/multipublish.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/multipublish.json new file mode 100644 index 00000000..db23f87e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/multipublish.json @@ -0,0 +1,23 @@ +{ + "listen": "localhost:3179", + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "kvIndexFile": "/path/to/indexkv.db", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "ownerName": "Alice", + "shareHandlerPath": "/share/", + "publish": { + "/pics/": { + "camliRoot": "picsRoot", + "cacheRoot": "/tmp/blobs/cache", + "goTemplate": "gallery.html" + }, + "/music/": { + "camliRoot": "musicRoot", + "baseURL": "http://localhost:3178/", + "cacheRoot": "/tmp/blobs/cache", + "goTemplate": "music.html" + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/noindex.err b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/noindex.err new file mode 100644 index 00000000..14c33614 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/noindex.err @@ -0,0 +1 @@ +Unless runIndex is set to false, you must specify an index option (kvIndexFile, leveldb, mongo, mysql, postgres, sqlite, memoryIndex). diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/noindex.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/noindex.json new file mode 100644 index 00000000..7575438c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/noindex.json @@ -0,0 +1,7 @@ +{ + "listen": "localhost:3179", + "auth": "localhost", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "blobPath": "/var/blobs" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_alt_host-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_alt_host-want.json new file mode 100644 index 00000000..10bfcdb1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_alt_host-want.json @@ -0,0 +1,115 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-s3", + "handlerArgs": { + "aws_access_key": "key", + "aws_secret_access_key": "secret", + "bucket": "bucket", + "hostname": "foo.com" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/camli-cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/path/to/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/" + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_alt_host.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_alt_host.json new file mode 100644 index 00000000..ca187002 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_alt_host.json @@ -0,0 +1,12 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db", + "s3": "key:secret:bucket:foo.com", + "replicateTo": [], + "publish": {}, + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_google_nolocaldisk.err b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_google_nolocaldisk.err new file mode 100644 index 00000000..01dd0e63 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_google_nolocaldisk.err @@ -0,0 +1 @@ +Using S3 as a primary storage and Google Cloud Storage as a mirror is not supported for now. \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_google_nolocaldisk.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_google_nolocaldisk.json new file mode 100644 index 00000000..31be495e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_google_nolocaldisk.json @@ -0,0 +1,13 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db", + "s3": "key:secret:bucket", + "googlecloudstorage": "clientId:clientSecret:refreshToken:bucketName", + "replicateTo": [], + "publish": {}, + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk-want.json new file mode 100644 index 00000000..506a53d3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk-want.json @@ -0,0 +1,114 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-s3", + "handlerArgs": { + "aws_access_key": "key", + "aws_secret_access_key": "secret", + "bucket": "bucket" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/camli-cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/path/to/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/" + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk.json new file mode 100644 index 00000000..5d741bb9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk.json @@ -0,0 +1,12 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db", + "s3": "key:secret:bucket", + "replicateTo": [], + "publish": {}, + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk_mysql-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk_mysql-want.json new file mode 100644 index 00000000..cba91976 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk_mysql-want.json @@ -0,0 +1,114 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-s3", + "handlerArgs": { + "aws_access_key": "key", + "aws_secret_access_key": "secret", + "bucket": "bucket" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/camli-cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "database": "camlitest", + "host": "localhost", + "password": "password", + "type": "mysql", + "user": "user" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "idle": true, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/" + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk_mysql.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk_mysql.json new file mode 100644 index 00000000..eb0d88c8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/s3_nolocaldisk_mysql.json @@ -0,0 +1,13 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "dbname": "camlitest", + "mysql": "user@localhost:password", + "s3": "key:secret:bucket", + "replicateTo": [], + "publish": {}, + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/sqlite-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/sqlite-want.json new file mode 100644 index 00000000..688a07f1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/sqlite-want.json @@ -0,0 +1,116 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/tmp/camli.db", + "type": "sqlite" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.sqlite", + "type": "sqlite" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.sqlite", + "type": "sqlite" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/sqlite.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/sqlite.json new file mode 100644 index 00000000..1d3c6f22 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/sqlite.json @@ -0,0 +1,10 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "blobPath": "/tmp/blobs", + "sqlite": "/tmp/camli.db", + "shareHandler": true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/thumbcache_on_db-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/thumbcache_on_db-want.json new file mode 100644 index 00000000..f4fe61f9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/thumbcache_on_db-want.json @@ -0,0 +1,130 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-googlecloudstorage", + "handlerArgs": { + "auth": { + "client_id": "auto", + "client_secret": "", + "refresh_token": "" + }, + "bucket": "bucketName" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/camli-cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "database": "camindex", + "host": "camlistore.cloudsql.google.internal", + "password": "root", + "type": "mysql", + "user": "root" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "to": "/index/", + "queue": { + "database": "camindex_syncindex_q", + "host": "camlistore.cloudsql.google.internal", + "password": "root", + "type": "mysql", + "user": "root" + } + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "database": "thumbcache_db", + "host": "camlistore.cloudsql.google.internal", + "password": "root", + "type": "mysql", + "user": "root" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/thumbcache_on_db.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/thumbcache_on_db.json new file mode 100644 index 00000000..d8bb564a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/thumbcache_on_db.json @@ -0,0 +1,15 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "mysql": "root@camlistore.cloudsql.google.internal:root", + "googlecloudstorage": ":bucketName", + "shareHandler": true, + "dbNames": { + "index": "camindex", + "ui_thumbcache": "thumbcache_db", + "queue-sync-to-index": "camindex_syncindex_q" + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/tls-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/tls-want.json new file mode 100644 index 00000000..fb8d8815 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/tls-want.json @@ -0,0 +1,131 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": true, + "httpsCert": "/tls.crt", + "httpsKey": "/tls.key", + "listen": "1.2.3.4:443", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/pics/": { + "handler": "app", + "handlerArgs": { + "program": "publisher", + "appConfig": { + "camliRoot": "picsRoot", + "goTemplate": "gallery.html", + "cacheRoot": "/tmp/blobs/cache", + "httpsCert": "/tls.crt", + "httpsKey": "/tls.key" + } + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.kv", + "type": "kv" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/tls.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/tls.json new file mode 100644 index 00000000..72a449d6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/tls.json @@ -0,0 +1,21 @@ +{ + "listen": "1.2.3.4:443", + "https": true, + "httpsCert": "/tls.crt", + "httpsKey": "/tls.key", + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db", + "s3": "", + "replicateTo": [], + "publish": { + "/pics/": { + "camliRoot": "picsRoot", + "cacheRoot": "/tmp/blobs/cache", + "goTemplate": "gallery.html" + } + }, + "shareHandler": true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_blog-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_blog-want.json new file mode 100644 index 00000000..a8b99351 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_blog-want.json @@ -0,0 +1,128 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/blog/": { + "handler": "app", + "handlerArgs": { + "program": "publisher", + "baseURL": "http://localhost:3178/", + "appConfig": { + "camliRoot": "blogRoot", + "goTemplate": "blog.html", + "cacheRoot": "/tmp/blobs/cache" + } + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.kv", + "type": "kv" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_blog.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_blog.json new file mode 100644 index 00000000..56096d7e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_blog.json @@ -0,0 +1,20 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db", + "s3": "", + "publish": { + "/blog/": { + "camliRoot": "blogRoot", + "baseURL": "http://localhost:3178/", + "cacheRoot": "/tmp/blobs/cache", + "goTemplate": "blog.html" + } + }, + "replicateTo": [], + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_gallery-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_gallery-want.json new file mode 100644 index 00000000..8b0a1ec5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_gallery-want.json @@ -0,0 +1,130 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/pics/": { + "handler": "app", + "handlerArgs": { + "program": "publisher", + "baseURL": "http://localhost:3178/", + "appConfig": { + "httpsCert": "/tls.crt", + "httpsKey": "/tls.key", + "camliRoot": "picsRoot", + "goTemplate": "gallery.html", + "cacheRoot": "/tmp/blobs/cache" + } + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.kv", + "type": "kv" + } + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_gallery.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_gallery.json new file mode 100644 index 00000000..bc0250ae --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_gallery.json @@ -0,0 +1,22 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db", + "s3": "", + "publish": { + "/pics/": { + "httpsCert": "/tls.crt", + "httpsKey": "/tls.key", + "camliRoot": "picsRoot", + "baseURL": "http://localhost:3178/", + "cacheRoot": "/tmp/blobs/cache", + "goTemplate": "gallery.html" + } + }, + "replicateTo": [], + "shareHandlerPath": "/share/" +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_sourceroot-want.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_sourceroot-want.json new file mode 100644 index 00000000..695ec329 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_sourceroot-want.json @@ -0,0 +1,136 @@ +{ + "auth": "userpass:camlistore:pass3179", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs-and-maybe-also-index/", + "jsonSignRoot": "/sighelper/", + "helpRoot": "/help/", + "searchRoot": "/my-search/", + "statusRoot": "/status/", + "stealth": false + } + }, + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": [ + "/bs/", + "/index/" + ] + } + }, + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "read": "/bs/", + "write": { + "else": "/bs/", + "if": "isSchema", + "then": "/bs-and-index/" + } + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs" + } + }, + "/cache/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": "/tmp/blobs/cache" + } + }, + "/help/": { + "handler": "help" + }, + "/importer/": { + "handler": "importer", + "handlerArgs": {} + }, + "/index/": { + "handler": "storage-index", + "handlerArgs": { + "blobSource": "/bs/", + "storage": { + "file": "/path/to/indexkv.db", + "type": "kv" + } + } + }, + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4", + "slurpToMemory": true + } + }, + "/setup/": { + "handler": "setup" + }, + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "keyId": "26F5ABDA", + "publicKeyDest": "/bs-and-index/", + "secretRing": "/path/to/secring" + } + }, + "/status/": { + "handler": "status" + }, + "/sto-s3/": { + "handler": "storage-s3", + "handlerArgs": { + "aws_access_key": "key", + "aws_secret_access_key": "secret", + "bucket": "bucket" + } + }, + "/sync-to-s3/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-s3-queue.kv", + "type": "kv" + }, + "to": "/sto-s3/" + } + }, + "/sync/": { + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "queue": { + "file": "/tmp/blobs/sync-to-index-queue.kv", + "type": "kv" + }, + "to": "/index/" + } + }, + "/ui/": { + "handler": "ui", + "handlerArgs": { + "cache": "/cache/", + "scaledImage": { + "file": "/tmp/blobs/thumbmeta.kv", + "type": "kv" + }, + "sourceRoot": "/path/to/alternative/camli/source" + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_sourceroot.json b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_sourceroot.json new file mode 100644 index 00000000..d3f96b60 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/serverinit/testdata/with_sourceroot.json @@ -0,0 +1,14 @@ +{ + "listen": "localhost:3179", + "https": false, + "auth": "userpass:camlistore:pass3179", + "blobPath": "/tmp/blobs", + "identity": "26F5ABDA", + "identitySecretRing": "/path/to/secring", + "kvIndexFile": "/path/to/indexkv.db", + "s3": "key:secret:bucket", + "replicateTo": [], + "publish": {}, + "sourceRoot": "/path/to/alternative/camli/source", + "shareHandler": true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/singleflight/singleflight.go b/vendor/github.com/camlistore/camlistore/pkg/singleflight/singleflight.go new file mode 100644 index 00000000..3b174172 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/singleflight/singleflight.go @@ -0,0 +1,64 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package singleflight provides a duplicate function call suppression +// mechanism. +package singleflight + +import "sync" + +// call is an in-flight or completed Do call +type call struct { + wg sync.WaitGroup + val interface{} + err error +} + +// Group represents a class of work and forms a namespace in which +// units of work can be executed with duplicate suppression. +type Group struct { + mu sync.Mutex // protects m + m map[string]*call // lazily initialized +} + +// Do executes and returns the results of the given function, making +// sure that only one execution is in-flight for a given key at a +// time. If a duplicate comes in, the duplicate caller waits for the +// original to complete and receives the same results. +func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { + g.mu.Lock() + if g.m == nil { + g.m = make(map[string]*call) + } + if c, ok := g.m[key]; ok { + g.mu.Unlock() + c.wg.Wait() + return c.val, c.err + } + c := new(call) + c.wg.Add(1) + g.m[key] = c + g.mu.Unlock() + + c.val, c.err = fn() + c.wg.Done() + + g.mu.Lock() + delete(g.m, key) + g.mu.Unlock() + + return c.val, c.err +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/singleflight/singleflight_test.go b/vendor/github.com/camlistore/camlistore/pkg/singleflight/singleflight_test.go new file mode 100644 index 00000000..40edcf30 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/singleflight/singleflight_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package singleflight + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" +) + +func TestDo(t *testing.T) { + var g Group + v, err := g.Do("key", func() (interface{}, error) { + return "bar", nil + }) + if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want { + t.Errorf("Do = %v; want %v", got, want) + } + if err != nil { + t.Errorf("Do error = %v", err) + } +} + +func TestDoErr(t *testing.T) { + var g Group + someErr := errors.New("Some error") + v, err := g.Do("key", func() (interface{}, error) { + return nil, someErr + }) + if err != someErr { + t.Errorf("Do error = %v; want someErr %v", err, someErr) + } + if v != nil { + t.Errorf("unexpected non-nil value %#v", v) + } +} + +func TestDoDupSuppress(t *testing.T) { + var g Group + c := make(chan string) + var calls int32 + fn := func() (interface{}, error) { + atomic.AddInt32(&calls, 1) + return <-c, nil + } + + const n = 10 + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + v, err := g.Do("key", fn) + if err != nil { + t.Errorf("Do error: %v", err) + } + if v.(string) != "bar" { + t.Errorf("got %q; want %q", v, "bar") + } + wg.Done() + }() + } + time.Sleep(100 * time.Millisecond) // let goroutines above block + c <- "bar" + wg.Wait() + if got := atomic.LoadInt32(&calls); got != 1 { + t.Errorf("number of calls = %d; want 1", got) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/buffer/buffer.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/buffer/buffer.go new file mode 100644 index 00000000..601f4a8a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/buffer/buffer.go @@ -0,0 +1,319 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package buffer provides a sorted.KeyValue implementation that +// buffers one KeyValue implementation in front of an another. It's +// used for cases such as reindexing where you need a KeyValue but it +// doesn't need to be flushed and consistent until the end. +package buffer + +import ( + "fmt" + "sync" + + "camlistore.org/pkg/sorted" +) + +// New returnes a sorted.KeyValue implementation that adds a Flush +// method to flush the buffer to the backing storage. A flush will +// also be performed when maxBufferBytes are reached. If +// maxBufferBytes <= 0, no automatic flushing is performed. +func New(buffer, backing sorted.KeyValue, maxBufferBytes int64) *KeyValue { + return &KeyValue{ + buf: buffer, + back: backing, + maxBuffer: maxBufferBytes, + } +} + +var _ sorted.KeyValue = (*KeyValue)(nil) + +type KeyValue struct { + buf, back sorted.KeyValue + maxBuffer int64 + + bufMu sync.Mutex + buffered int64 + + // This read lock should be held during Set/Get/Delete/BatchCommit, + // and the write lock should be held during Flush. + mu sync.RWMutex +} + +func (kv *KeyValue) Flush() error { + kv.mu.Lock() + defer kv.mu.Unlock() + var ( + bmback = kv.back.BeginBatch() + bmbuf = kv.buf.BeginBatch() + commit = false + it = kv.buf.Find("", "") + ) + for it.Next() { + bmback.Set(it.Key(), it.Value()) + bmbuf.Delete(it.Key()) + commit = true + } + if err := it.Close(); err != nil { + return err + } + if commit { + if err := kv.back.CommitBatch(bmback); err != nil { + return err + } + if err := kv.buf.CommitBatch(bmbuf); err != nil { + return err + } + kv.bufMu.Lock() + kv.buffered = 0 + kv.bufMu.Unlock() + } + return nil +} + +func (kv *KeyValue) Get(key string) (string, error) { + kv.mu.RLock() + defer kv.mu.RUnlock() + v, err := kv.buf.Get(key) + switch err { + case sorted.ErrNotFound: + break + case nil: + return v, nil + default: + return "", err + } + return kv.back.Get(key) +} + +func (kv *KeyValue) Set(key, value string) error { + if err := sorted.CheckSizes(key, value); err != nil { + return err + } + kv.mu.RLock() + err := kv.buf.Set(key, value) + kv.mu.RUnlock() + if err == nil { + kv.bufMu.Lock() + kv.buffered += int64(len(key) + len(value)) + doFlush := kv.buffered > kv.maxBuffer + kv.bufMu.Unlock() + if doFlush { + err = kv.Flush() + } + } + return err +} + +func (kv *KeyValue) Delete(key string) error { + kv.mu.RLock() + defer kv.mu.RUnlock() + // This isn't an ideal implementation, since it synchronously + // deletes from the backing store. But deletes aren't really + // used, so ignoring for now. + // Could also use a syncutil.Group to do these in parallel, + // but the buffer should be an in-memory implementation + // anyway, so should be fast. + err1 := kv.buf.Delete(key) + err2 := kv.back.Delete(key) + if err1 != nil { + return err1 + } + return err2 +} + +func (kv *KeyValue) BeginBatch() sorted.BatchMutation { + return new(batch) +} + +func (kv *KeyValue) CommitBatch(bm sorted.BatchMutation) error { + kv.mu.RLock() + defer kv.mu.RUnlock() + b, ok := bm.(*batch) + if !ok { + return fmt.Errorf("unexpected BatchMutation type %T", bm) + } + var ( + // A batch mutation for applying this mutation to the buffer. + bmbuf = kv.buf.BeginBatch() + // A lazily created batch mutation for deleting from the backing + // storage; this should be rare. (See Delete above.) + bmback sorted.BatchMutation + ) + for _, m := range b.mods { + if m.isDelete { + bmbuf.Delete(m.key) + if bmback == nil { + bmback = kv.back.BeginBatch() + } + bmback.Delete(m.key) + continue + } else { + if err := sorted.CheckSizes(m.key, m.value); err != nil { + return err + } + } + bmbuf.Set(m.key, m.value) + } + if err := kv.buf.CommitBatch(bmbuf); err != nil { + return err + } + if bmback != nil { + return kv.back.CommitBatch(bmback) + } + return nil +} + +func (kv *KeyValue) Close() error { + if err := kv.Flush(); err != nil { + return err + } + return kv.back.Close() +} + +func (kv *KeyValue) Find(start, end string) sorted.Iterator { + // TODO(adg): hold read lock while iterating? seems complicated + ibuf := kv.buf.Find(start, end) + iback := kv.back.Find(start, end) + return &iter{ + buf: subIter{Iterator: ibuf}, + back: subIter{Iterator: iback}, + } +} + +type batch struct { + mu sync.Mutex + mods []mod +} + +type mod struct { + isDelete bool + key, value string +} + +func (b *batch) Set(key, value string) { + defer b.mu.Unlock() + b.mu.Lock() + b.mods = append(b.mods, mod{key: key, value: value}) +} + +func (b *batch) Delete(key string) { + defer b.mu.Unlock() + b.mu.Lock() + b.mods = append(b.mods, mod{key: key, isDelete: true}) +} + +type iter struct { + buf, back subIter +} + +func (it *iter) current() *subIter { + switch { + case it.back.eof: + return &it.buf + case it.buf.eof: + return &it.back + case it.buf.key <= it.back.key: + return &it.buf + default: + return &it.back + } +} + +func (it *iter) Next() bool { + // Call Next on both iterators for the first time, if we haven't + // already, so that the key comparisons below are valid. + start := false + if it.buf.key == "" && !it.buf.eof { + start = it.buf.next() + } + if it.back.key == "" && !it.buf.eof { + start = it.back.next() || start + } + if start { + // We started iterating with at least one value. + return true + } + // Bail if both iterators are done. + if it.buf.eof && it.back.eof { + return false + } + // If one iterator is done, advance the other. + if it.buf.eof { + return it.back.next() + } + if it.back.eof { + return it.buf.next() + } + // If both iterators still going, + // advance the one that is further behind, + // or both simultaneously if they point to the same key. + switch { + case it.buf.key < it.back.key: + it.buf.next() + case it.buf.key > it.back.key: + it.back.next() + case it.buf.key == it.back.key: + n1, n2 := it.buf.next(), it.back.next() + if !n1 && !n2 { + // Both finished simultaneously. + return false + } + } + return true +} + +func (it *iter) Key() string { + return it.current().key +} + +func (it *iter) Value() string { + return it.current().Value() +} + +func (it *iter) KeyBytes() []byte { + return it.current().KeyBytes() +} + +func (it *iter) ValueBytes() []byte { + return it.current().ValueBytes() +} + +func (it *iter) Close() error { + err1 := it.buf.Close() + err2 := it.back.Close() + if err1 != nil { + return err1 + } + return err2 +} + +// subIter is an iterator (either the backing storage or the buffer) that +// keeps track of the current key and whether it has reached EOF. +type subIter struct { + sorted.Iterator + key string + eof bool +} + +func (it *subIter) next() bool { + if it.Next() { + it.key = it.Key() + return true + } + it.eof = true + return false +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/buffer/buffer_test.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/buffer/buffer_test.go new file mode 100644 index 00000000..dbb64648 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/buffer/buffer_test.go @@ -0,0 +1,104 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package buffer + +import ( + "testing" + + "camlistore.org/pkg/sorted" +) + +// TODO(adg): test batch mutations +// TODO(adg): test auto-flush behavior + +func TestBuffer(t *testing.T) { + var ( + toBack = []mod{ + {false, "b", "b1"}, + {false, "d", "d1"}, + {false, "f", "f1"}, + } + toBuf = []mod{ + {false, "a", "a2"}, + {false, "b", "b2"}, + {false, "c", "c2"}, + {false, "e", "e2"}, + {true, "f", ""}, + {false, "g", "g2"}, + } + backBeforeFlush = []mod{ + {false, "b", "b1"}, + {false, "d", "d1"}, + // f deleted + } + want = []mod{ + {false, "a", "a2"}, + {false, "b", "b2"}, + {false, "c", "c2"}, + {false, "d", "d1"}, + {false, "e", "e2"}, + // f deleted + {false, "g", "g2"}, + } + ) + + // Populate backing storage. + backing := sorted.NewMemoryKeyValue() + for _, m := range toBack { + backing.Set(m.key, m.value) + } + // Wrap with buffered storage, populate. + buf := New(sorted.NewMemoryKeyValue(), backing, 1<<20) + for _, m := range toBuf { + if m.isDelete { + buf.Delete(m.key) + } else { + buf.Set(m.key, m.value) + } + } + + // Check contents of buffered storage. + check(t, buf, "buffered", want) + check(t, backing, "backing before flush", backBeforeFlush) + + // Flush. + if err := buf.Flush(); err != nil { + t.Fatal("flush error: ", err) + } + + // Check contents of backing storage. + check(t, backing, "backing after flush", want) +} + +func check(t *testing.T, kv sorted.KeyValue, prefix string, want []mod) { + it := kv.Find("", "") + for i, m := range want { + if !it.Next() { + t.Fatalf("%v: unexpected it.Next == false on iteration %d", prefix, i) + } + if k, v := it.Key(), it.Value(); k != m.key || v != m.value { + t.Errorf("%v: got key == %q value == %q, want key == %q value == %q on iteration %d", + prefix, k, v, m.key, m.value, i) + } + } + if it.Next() { + t.Errorf("%v: unexpected it.Next == true after complete iteration", prefix) + } + if err := it.Close(); err != nil { + t.Errorf("%v: error closing iterator: %v", prefix, err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/kv.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/kv.go new file mode 100644 index 00000000..2daaa839 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/kv.go @@ -0,0 +1,237 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package sorted provides a KeyValue interface and constructor registry. +package sorted + +import ( + "errors" + "fmt" + + "camlistore.org/pkg/jsonconfig" +) + +const ( + MaxKeySize = 767 // Maximum size, in bytes, for a key in any store implementing KeyValue. + MaxValueSize = 63000 // Maximum size, in bytes, for a value in any store implementing KeyValue. MaxKeySize and MaxValueSize values originate from InnoDB and MySQL limitations. +) + +const DefaultKVFileType = "leveldb" + +var ( + ErrNotFound = errors.New("sorted: key not found") + ErrKeyTooLarge = fmt.Errorf("sorted: key size is over %v", MaxKeySize) + ErrValueTooLarge = fmt.Errorf("sorted: value size is over %v", MaxValueSize) +) + +// KeyValue is a sorted, enumerable key-value interface supporting +// batch mutations. +type KeyValue interface { + // Get gets the value for the given key. It returns ErrNotFound if the DB + // does not contain the key. + Get(key string) (string, error) + + Set(key, value string) error + + // Delete deletes keys. Deleting a non-existent key does not return an error. + Delete(key string) error + + BeginBatch() BatchMutation + CommitBatch(b BatchMutation) error + + // Find returns an iterator positioned before the first key/value pair + // whose key is 'greater than or equal to' the given key. There may be no + // such pair, in which case the iterator will return false on Next. + // + // The optional end value specifies the exclusive upper + // bound. If the empty string, the iterator returns keys + // where "key >= start". + // If non-empty, the iterator returns keys where + // "key >= start && key < endHint". + // + // Any error encountered will be implicitly returned via the iterator. An + // error-iterator will yield no key/value pairs and closing that iterator + // will return that error. + Find(start, end string) Iterator + + // Close is a polite way for the server to shut down the storage. + // Implementations should never lose data after a Set, Delete, + // or CommmitBatch, though. + Close() error +} + +// Wiper is an optional interface that may be implemented by storage +// implementations. +type Wiper interface { + KeyValue + + // Wipe removes all key/value pairs. + Wipe() error +} + +// Iterator iterates over an index KeyValue's key/value pairs in key order. +// +// An iterator must be closed after use, but it is not necessary to read an +// iterator until exhaustion. +// +// An iterator is not necessarily goroutine-safe, but it is safe to use +// multiple iterators concurrently, with each in a dedicated goroutine. +type Iterator interface { + // Next moves the iterator to the next key/value pair. + // It returns false when the iterator is exhausted. + Next() bool + + // Key returns the key of the current key/value pair. + // Only valid after a call to Next returns true. + Key() string + + // KeyBytes returns the key as bytes. The returned bytes + // should not be written and are invalid after the next call + // to Next or Close. + // TODO(bradfitz): rename this and change it to return a + // mem.RO instead? + KeyBytes() []byte + + // Value returns the value of the current key/value pair. + // Only valid after a call to Next returns true. + Value() string + + // ValueBytes returns the value as bytes. The returned bytes + // should not be written and are invalid after the next call + // to Next or Close. + // TODO(bradfitz): rename this and change it to return a + // mem.RO instead? + ValueBytes() []byte + + // Close closes the iterator and returns any accumulated error. Exhausting + // all the key/value pairs in a table is not considered to be an error. + // It is valid to call Close multiple times. Other methods should not be + // called after the iterator has been closed. + Close() error +} + +type BatchMutation interface { + Set(key, value string) + Delete(key string) +} + +type Mutation interface { + Key() string + Value() string + IsDelete() bool +} + +type mutation struct { + key string + value string // used if !delete + delete bool // if to be deleted +} + +func (m mutation) Key() string { + return m.key +} + +func (m mutation) Value() string { + return m.value +} + +func (m mutation) IsDelete() bool { + return m.delete +} + +func NewBatchMutation() BatchMutation { + return &batch{} +} + +type batch struct { + m []Mutation +} + +func (b *batch) Mutations() []Mutation { + return b.m +} + +func (b *batch) Delete(key string) { + b.m = append(b.m, mutation{key: key, delete: true}) +} + +func (b *batch) Set(key, value string) { + b.m = append(b.m, mutation{key: key, value: value}) +} + +var ( + ctors = make(map[string]func(jsonconfig.Obj) (KeyValue, error)) +) + +func RegisterKeyValue(typ string, fn func(jsonconfig.Obj) (KeyValue, error)) { + if typ == "" || fn == nil { + panic("zero type or func") + } + if _, dup := ctors[typ]; dup { + panic("duplication registration of type " + typ) + } + ctors[typ] = fn +} + +func NewKeyValue(cfg jsonconfig.Obj) (KeyValue, error) { + var s KeyValue + var err error + typ := cfg.RequiredString("type") + ctor, ok := ctors[typ] + if typ != "" && !ok { + return nil, fmt.Errorf("Invalid sorted.KeyValue type %q", typ) + } + if ok { + s, err = ctor(cfg) + if err != nil { + return nil, fmt.Errorf("error from %q KeyValue: %v", typ, err) + } + } + return s, cfg.Validate() +} + +// Foreach runs fn for each key/value pair in kv. If fn returns an error, +// that same error is returned from Foreach and iteration stops. +func Foreach(kv KeyValue, fn func(key, value string) error) error { + return ForeachInRange(kv, "", "", fn) +} + +// ForeachInRange runs fn for each key/value pair in kv in the range +// of start and end, which behave the same as kv.Find. If fn returns +// an error, that same error is returned from Foreach and iteration +// stops. +func ForeachInRange(kv KeyValue, start, end string, fn func(key, value string) error) error { + it := kv.Find(start, end) + for it.Next() { + if err := fn(it.Key(), it.Value()); err != nil { + it.Close() + return err + } + } + return it.Close() +} + +// CheckSizes returns ErrKeyTooLarge if key does not respect KeyMaxSize or +// ErrValueTooLarge if value does not respect ValueMaxSize +func CheckSizes(key, value string) error { + if len(key) > MaxKeySize { + return ErrKeyTooLarge + } + if len(value) > MaxValueSize { + return ErrValueTooLarge + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/kvfile/kvfile.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/kvfile/kvfile.go new file mode 100644 index 00000000..4959cb30 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/kvfile/kvfile.go @@ -0,0 +1,269 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package kvfile provides an implementation of sorted.KeyValue +// on top of a single mutable database file on disk using +// github.com/cznic/kv. +package kvfile + +import ( + "bytes" + "errors" + "fmt" + "io" + "log" + "os" + "sync" + + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/kvutil" + "camlistore.org/pkg/sorted" + + "camlistore.org/third_party/github.com/cznic/kv" +) + +var _ sorted.Wiper = (*kvis)(nil) + +func init() { + sorted.RegisterKeyValue("kv", newKeyValueFromJSONConfig) +} + +// NewStorage is a convenience that calls newKeyValueFromJSONConfig +// with file as the kv storage file. +func NewStorage(file string) (sorted.KeyValue, error) { + return newKeyValueFromJSONConfig(jsonconfig.Obj{"file": file}) +} + +// newKeyValueFromJSONConfig returns a KeyValue implementation on top of a +// github.com/cznic/kv file. +func newKeyValueFromJSONConfig(cfg jsonconfig.Obj) (sorted.KeyValue, error) { + file := cfg.RequiredString("file") + if err := cfg.Validate(); err != nil { + return nil, err + } + opts := &kv.Options{} + db, err := kvutil.Open(file, opts) + if err != nil { + return nil, err + } + is := &kvis{ + db: db, + opts: opts, + path: file, + } + return is, nil +} + +type kvis struct { + path string + db *kv.DB + opts *kv.Options + txmu sync.Mutex +} + +// TODO: use bytepool package. +func getBuf(n int) []byte { return make([]byte, n) } +func putBuf([]byte) {} + +func (is *kvis) Get(key string) (string, error) { + buf := getBuf(200) + defer putBuf(buf) + val, err := is.db.Get(buf, []byte(key)) + if err != nil { + return "", err + } + if val == nil { + return "", sorted.ErrNotFound + } + return string(val), nil +} + +func (is *kvis) Set(key, value string) error { + if err := sorted.CheckSizes(key, value); err != nil { + return err + } + return is.db.Set([]byte(key), []byte(value)) +} + +func (is *kvis) Delete(key string) error { + return is.db.Delete([]byte(key)) +} + +func (is *kvis) Find(start, end string) sorted.Iterator { + it := &iter{ + db: is.db, + startKey: start, + endKey: []byte(end), + } + it.enum, _, it.err = it.db.Seek([]byte(start)) + return it +} + +func (is *kvis) BeginBatch() sorted.BatchMutation { + return sorted.NewBatchMutation() +} + +func (is *kvis) Wipe() error { + // Unlock the already open DB. + if err := is.db.Close(); err != nil { + return err + } + if err := os.Remove(is.path); err != nil { + return err + } + + db, err := kv.Create(is.path, is.opts) + if err != nil { + return fmt.Errorf("error creating %s: %v", is.path, err) + } + is.db = db + return nil +} + +type batch interface { + Mutations() []sorted.Mutation +} + +func (is *kvis) CommitBatch(bm sorted.BatchMutation) error { + b, ok := bm.(batch) + if !ok { + return errors.New("invalid batch type") + } + is.txmu.Lock() + defer is.txmu.Unlock() + + good := false + defer func() { + if !good { + is.db.Rollback() + } + }() + + if err := is.db.BeginTransaction(); err != nil { + return err + } + for _, m := range b.Mutations() { + if m.IsDelete() { + if err := is.db.Delete([]byte(m.Key())); err != nil { + return err + } + } else { + if err := sorted.CheckSizes(m.Key(), m.Value()); err != nil { + return err + } + if err := is.db.Set([]byte(m.Key()), []byte(m.Value())); err != nil { + return err + } + } + } + + good = true + return is.db.Commit() +} + +func (is *kvis) Close() error { + log.Printf("Closing kvfile database %s", is.path) + return is.db.Close() +} + +type iter struct { + db *kv.DB + startKey string + endKey []byte + + enum *kv.Enumerator + + valid bool + key, val []byte + skey, sval *string // non-nil if valid + + err error + closed bool +} + +func (it *iter) Close() error { + it.closed = true + return it.err +} + +func (it *iter) KeyBytes() []byte { + if !it.valid { + panic("not valid") + } + return it.key +} + +func (it *iter) Key() string { + if !it.valid { + panic("not valid") + } + if it.skey != nil { + return *it.skey + } + str := string(it.key) + it.skey = &str + return str +} + +func (it *iter) ValueBytes() []byte { + if !it.valid { + panic("not valid") + } + return it.val +} + +func (it *iter) Value() string { + if !it.valid { + panic("not valid") + } + if it.sval != nil { + return *it.sval + } + str := string(it.val) + it.sval = &str + return str +} + +func (it *iter) end() bool { + it.valid = false + it.closed = true + return false +} + +func (it *iter) Next() bool { + if it.err != nil { + return false + } + if it.closed { + panic("Next called after Next returned value") + } + it.skey, it.sval = nil, nil + var err error + it.key, it.val, err = it.enum.Next() + if err == io.EOF { + it.err = nil + return it.end() + } + if err != nil { + it.err = err + return it.end() + } + if len(it.endKey) > 0 && bytes.Compare(it.key, it.endKey) >= 0 { + return it.end() + } + it.valid = true + return true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/kvfile/kvfile_test.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/kvfile/kvfile_test.go new file mode 100644 index 00000000..3b567160 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/kvfile/kvfile_test.go @@ -0,0 +1,45 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kvfile + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/kvtest" +) + +func TestKvfileKV(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "camlistore-kvfilekv_test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + dbname := filepath.Join(tmpDir, "testdb.kvfile") + kv, err := sorted.NewKeyValue(jsonconfig.Obj{ + "type": "kv", + "file": dbname, + }) + if err != nil { + t.Fatalf("Could not create kvfile sorted kv at %v: %v", dbname, err) + } + kvtest.TestSorted(t, kv) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/kvtest/kvtest.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/kvtest/kvtest.go new file mode 100644 index 00000000..0d4dd101 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/kvtest/kvtest.go @@ -0,0 +1,173 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package kvtest tests sorted.KeyValue implementations. +package kvtest + +import ( + "reflect" + "testing" + + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/test" +) + +func TestSorted(t *testing.T, kv sorted.KeyValue) { + defer test.TLog(t)() + if !isEmpty(t, kv) { + t.Fatal("kv for test is expected to be initially empty") + } + set := func(k, v string) { + if err := kv.Set(k, v); err != nil { + t.Fatalf("Error setting %q to %q: %v", k, v, err) + } + } + set("foo", "bar") + if isEmpty(t, kv) { + t.Fatalf("iterator reports the kv is empty after adding foo=bar; iterator must be broken") + } + if v, err := kv.Get("foo"); err != nil || v != "bar" { + t.Errorf("get(foo) = %q, %v; want bar", v, err) + } + if v, err := kv.Get("NOT_EXIST"); err != sorted.ErrNotFound { + t.Errorf("get(NOT_EXIST) = %q, %v; want error sorted.ErrNotFound", v, err) + } + for i := 0; i < 2; i++ { + if err := kv.Delete("foo"); err != nil { + t.Errorf("Delete(foo) (on loop %d/2) returned error %v", i+1, err) + } + } + set("a", "av") + set("b", "bv") + set("c", "cv") + testEnumerate(t, kv, "", "", "av", "bv", "cv") + testEnumerate(t, kv, "a", "", "av", "bv", "cv") + testEnumerate(t, kv, "b", "", "bv", "cv") + testEnumerate(t, kv, "a", "c", "av", "bv") + testEnumerate(t, kv, "a", "b", "av") + testEnumerate(t, kv, "a", "a") + testEnumerate(t, kv, "d", "") + testEnumerate(t, kv, "d", "e") + + // Verify that < comparison works identically for all DBs (because it is affected by collation rules) + // http://postgresql.1045698.n5.nabble.com/String-comparison-and-the-SQL-standard-td5740721.html + set("foo|abc", "foo|abcv") + testEnumerate(t, kv, "foo|", "", "foo|abcv") + testEnumerate(t, kv, "foo|", "foo}", "foo|abcv") + + // Verify that the value isn't being used instead of the key in the range comparison. + set("y", "x:foo") + testEnumerate(t, kv, "x:", "x~") + + testInsertLarge(t, kv) + testInsertTooLarge(t, kv) + + // TODO: test batch commits +} + +func testInsertLarge(t *testing.T, kv sorted.KeyValue) { + largeKey := make([]byte, sorted.MaxKeySize-1) + // setting all the bytes because postgres whines about an invalid byte sequence + // otherwise + for k, _ := range largeKey { + largeKey[k] = 'A' + } + largeKey[sorted.MaxKeySize-2] = 'B' + largeValue := make([]byte, sorted.MaxValueSize-1) + for k, _ := range largeValue { + largeValue[k] = 'A' + } + largeValue[sorted.MaxValueSize-2] = 'B' + + // insert with large key + if err := kv.Set(string(largeKey), "whatever"); err != nil { + t.Fatalf("Insertion of large key failed: %v", err) + } + + // and verify we can get it back, i.e. that the key hasn't been truncated. + it := kv.Find(string(largeKey), "") + if !it.Next() || it.Key() != string(largeKey) || it.Value() != "whatever" { + it.Close() + t.Fatalf("Find(largeKey) = %q, %q; want %q, %q", it.Key(), it.Value(), largeKey, "whatever") + } + it.Close() + + // insert with large value + if err := kv.Set("whatever", string(largeValue)); err != nil { + t.Fatalf("Insertion of large value failed: %v", err) + } + // and verify we can get it back, i.e. that the value hasn't been truncated. + if v, err := kv.Get("whatever"); err != nil || v != string(largeValue) { + t.Fatalf("get(\"whatever\") = %q, %v; want %q", v, err, largeValue) + } + + // insert with large key and large value + if err := kv.Set(string(largeKey), string(largeValue)); err != nil { + t.Fatalf("Insertion of large key and value failed: %v", err) + } + // and verify we can get them back + it = kv.Find(string(largeKey), "") + defer it.Close() + if !it.Next() || it.Key() != string(largeKey) || it.Value() != string(largeValue) { + t.Fatalf("Find(largeKey) = %q, %q; want %q, %q", it.Key(), it.Value(), largeKey, largeValue) + } +} + +func testInsertTooLarge(t *testing.T, kv sorted.KeyValue) { + largeKey := make([]byte, sorted.MaxKeySize+1) + largeValue := make([]byte, sorted.MaxValueSize+1) + if err := kv.Set(string(largeKey), "whatever"); err == nil || err != sorted.ErrKeyTooLarge { + t.Fatalf("Insertion of too large a key should have failed, but err was %v", err) + } + if err := kv.Set("whatever", string(largeValue)); err == nil || err != sorted.ErrValueTooLarge { + t.Fatalf("Insertion of too large a value should have failed, but err was %v", err) + } +} + +func testEnumerate(t *testing.T, kv sorted.KeyValue, start, end string, want ...string) { + var got []string + it := kv.Find(start, end) + for it.Next() { + key, val := it.Key(), it.Value() + keyb, valb := it.KeyBytes(), it.ValueBytes() + if key != string(keyb) { + t.Errorf("Key and KeyBytes disagree: %q vs %q", key, keyb) + } + if val != string(valb) { + t.Errorf("Value and ValueBytes disagree: %q vs %q", val, valb) + } + if key+"v" != val { + t.Errorf("iterator returned unexpected pair for test: %q, %q", key, val) + } + got = append(got, val) + } + err := it.Close() + if err != nil { + t.Errorf("for enumerate of (%q, %q), Close error: %v", start, end, err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("for enumerate of (%q, %q), got: %q; want %q", start, end, got, want) + } +} + +func isEmpty(t *testing.T, kv sorted.KeyValue) bool { + it := kv.Find("", "") + hasRow := it.Next() + if err := it.Close(); err != nil { + t.Fatalf("Error closing iterator while testing for emptiness: %v", err) + } + return !hasRow +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/leveldb/leveldb.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/leveldb/leveldb.go new file mode 100644 index 00000000..5ed7ee99 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/leveldb/leveldb.go @@ -0,0 +1,258 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package leveldb provides an implementation of sorted.KeyValue +// on top of a single mutable database file on disk using +// github.com/syndtr/goleveldb. +package leveldb + +import ( + "errors" + "fmt" + "os" + "sync" + + "camlistore.org/pkg/env" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" + + "camlistore.org/third_party/github.com/syndtr/goleveldb/leveldb" + "camlistore.org/third_party/github.com/syndtr/goleveldb/leveldb/filter" + "camlistore.org/third_party/github.com/syndtr/goleveldb/leveldb/iterator" + "camlistore.org/third_party/github.com/syndtr/goleveldb/leveldb/opt" + "camlistore.org/third_party/github.com/syndtr/goleveldb/leveldb/util" +) + +var _ sorted.Wiper = (*kvis)(nil) + +func init() { + sorted.RegisterKeyValue("leveldb", newKeyValueFromJSONConfig) +} + +// NewStorage is a convenience that calls newKeyValueFromJSONConfig +// with file as the leveldb storage file. +func NewStorage(file string) (sorted.KeyValue, error) { + return newKeyValueFromJSONConfig(jsonconfig.Obj{"file": file}) +} + +// newKeyValueFromJSONConfig returns a KeyValue implementation on top of a +// github.com/syndtr/goleveldb/leveldb file. +func newKeyValueFromJSONConfig(cfg jsonconfig.Obj) (sorted.KeyValue, error) { + file := cfg.RequiredString("file") + if err := cfg.Validate(); err != nil { + return nil, err + } + strictness := opt.DefaultStrict + if env.IsDev() { + // Be more strict in dev mode. + strictness = opt.StrictAll + } + opts := &opt.Options{ + // The default is 10, + // 8 means 2.126% or 1/47th disk check rate, + // 10 means 0.812% error rate (1/2^(bits/1.44)) or 1/123th disk check rate, + // 12 means 0.31% or 1/322th disk check rate. + // TODO(tgulacsi): decide which number is the best here. Till that go with the default. + Filter: filter.NewBloomFilter(10), + Strict: strictness, + } + db, err := leveldb.OpenFile(file, opts) + if err != nil { + return nil, err + } + is := &kvis{ + db: db, + path: file, + opts: opts, + readOpts: &opt.ReadOptions{Strict: strictness}, + // On machine crash we want to reindex anyway, and + // fsyncs may impose great performance penalty. + writeOpts: &opt.WriteOptions{Sync: false}, + } + return is, nil +} + +type kvis struct { + path string + db *leveldb.DB + opts *opt.Options + readOpts *opt.ReadOptions + writeOpts *opt.WriteOptions + txmu sync.Mutex +} + +func (is *kvis) Get(key string) (string, error) { + val, err := is.db.Get([]byte(key), is.readOpts) + if err != nil { + if err == leveldb.ErrNotFound { + return "", sorted.ErrNotFound + } + return "", err + } + if val == nil { + return "", sorted.ErrNotFound + } + return string(val), nil +} + +func (is *kvis) Set(key, value string) error { + if err := sorted.CheckSizes(key, value); err != nil { + return err + } + return is.db.Put([]byte(key), []byte(value), is.writeOpts) +} + +func (is *kvis) Delete(key string) error { + return is.db.Delete([]byte(key), is.writeOpts) +} + +func (is *kvis) Find(start, end string) sorted.Iterator { + var startB, endB []byte + // A nil Range.Start is treated as a key before all keys in the DB. + if start != "" { + startB = []byte(start) + } + // A nil Range.Limit is treated as a key after all keys in the DB. + if end != "" { + endB = []byte(end) + } + it := &iter{ + it: is.db.NewIterator( + &util.Range{Start: startB, Limit: endB}, + is.readOpts, + ), + } + return it +} + +func (is *kvis) Wipe() error { + // Close the already open DB. + if err := is.db.Close(); err != nil { + return err + } + if err := os.RemoveAll(is.path); err != nil { + return err + } + + db, err := leveldb.OpenFile(is.path, is.opts) + if err != nil { + return fmt.Errorf("error creating %s: %v", is.path, err) + } + is.db = db + return nil +} + +func (is *kvis) BeginBatch() sorted.BatchMutation { + return &lvbatch{batch: new(leveldb.Batch)} +} + +type lvbatch struct { + errMu sync.Mutex + err error // Set if one of the mutations had too large a key or value. Sticky. + + batch *leveldb.Batch +} + +func (lvb *lvbatch) Set(key, value string) { + lvb.errMu.Lock() + defer lvb.errMu.Unlock() + if lvb.err != nil { + return + } + if err := sorted.CheckSizes(key, value); err != nil { + if err == sorted.ErrKeyTooLarge { + lvb.err = fmt.Errorf("%v: %v", err, key) + } else { + lvb.err = fmt.Errorf("%v: %v", err, value) + } + return + } + lvb.batch.Put([]byte(key), []byte(value)) +} + +func (lvb *lvbatch) Delete(key string) { + lvb.batch.Delete([]byte(key)) +} + +func (is *kvis) CommitBatch(bm sorted.BatchMutation) error { + b, ok := bm.(*lvbatch) + if !ok { + return errors.New("invalid batch type") + } + b.errMu.Lock() + defer b.errMu.Unlock() + if b.err != nil { + return b.err + } + return is.db.Write(b.batch, is.writeOpts) +} + +func (is *kvis) Close() error { + return is.db.Close() +} + +type iter struct { + it iterator.Iterator + + key, val []byte + skey, sval *string // for caching string values + + err error + closed bool +} + +func (it *iter) Close() error { + it.closed = true + it.it.Release() + return nil +} + +func (it *iter) KeyBytes() []byte { + return it.it.Key() +} + +func (it *iter) Key() string { + if it.skey != nil { + return *it.skey + } + str := string(it.it.Key()) + it.skey = &str + return str +} + +func (it *iter) ValueBytes() []byte { + return it.it.Value() +} + +func (it *iter) Value() string { + if it.sval != nil { + return *it.sval + } + str := string(it.it.Value()) + it.sval = &str + return str +} + +func (it *iter) Next() bool { + if err := it.it.Error(); err != nil { + return false + } + if it.closed { + panic("Next called after Next returned value") + } + it.skey, it.sval = nil, nil + return it.it.Next() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/leveldb/leveldb_test.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/leveldb/leveldb_test.go new file mode 100644 index 00000000..7e72ffdd --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/leveldb/leveldb_test.go @@ -0,0 +1,46 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package leveldb + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/kvtest" +) + +func TestLeveldbKV(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "camlistore-leveldbkv_test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + dbname := filepath.Join(tmpDir, "testdb.leveldb") + t.Logf("Testing leveldb %q.", dbname) + kv, err := sorted.NewKeyValue(jsonconfig.Obj{ + "type": "leveldb", + "file": dbname, + }) + if err != nil { + t.Fatalf("Could not create leveldb sorted kv at %v: %v", dbname, err) + } + kvtest.TestSorted(t, kv) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/mem.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/mem.go new file mode 100644 index 00000000..e4fb6370 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/mem.go @@ -0,0 +1,176 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sorted + +import ( + "bytes" + "errors" + "sync" + + "camlistore.org/pkg/jsonconfig" + "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/db" + "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/memdb" +) + +// NewMemoryKeyValue returns a KeyValue implementation that's backed only +// by memory. It's mostly useful for tests and development. +func NewMemoryKeyValue() KeyValue { + db := memdb.New(nil) + return &memKeys{db: db} +} + +// memKeys is a naive in-memory implementation of KeyValue for test & development +// purposes only. +type memKeys struct { + mu sync.Mutex // guards db + db db.DB +} + +// memIter converts from leveldb's db.Iterator interface, which +// operates on []byte, to Camlistore's index.Iterator, which operates +// on string. +type memIter struct { + lit db.Iterator // underlying leveldb iterator + k, v *string // if nil, not stringified yet + end []byte // if len(end) > 0, the upper bound +} + +func (t *memIter) Next() bool { + t.k, t.v = nil, nil + if !t.lit.Next() { + return false + } + if len(t.end) > 0 && bytes.Compare(t.KeyBytes(), t.end) >= 0 { + return false + } + return true +} + +func (s *memIter) Close() error { + if s.lit == nil { + // Already closed. + return nil + } + err := s.lit.Close() + *s = memIter{} // to cause crashes on future access + return err +} + +func (s *memIter) KeyBytes() []byte { + return s.lit.Key() +} + +func (s *memIter) ValueBytes() []byte { + return s.lit.Value() +} + +func (s *memIter) Key() string { + if s.k != nil { + return *s.k + } + str := string(s.KeyBytes()) + s.k = &str + return str +} + +func (s *memIter) Value() string { + if s.v != nil { + return *s.v + } + str := string(s.ValueBytes()) + s.v = &str + return str +} + +func (mk *memKeys) Get(key string) (string, error) { + mk.mu.Lock() + defer mk.mu.Unlock() + k, err := mk.db.Get([]byte(key), nil) + if err == db.ErrNotFound { + return "", ErrNotFound + } + return string(k), err +} + +func (mk *memKeys) Find(start, end string) Iterator { + mk.mu.Lock() + defer mk.mu.Unlock() + lit := mk.db.Find([]byte(start), nil) + it := &memIter{lit: lit} + if end != "" { + it.end = []byte(end) + } + return it +} + +func (mk *memKeys) Set(key, value string) error { + if err := CheckSizes(key, value); err != nil { + return err + } + mk.mu.Lock() + defer mk.mu.Unlock() + return mk.db.Set([]byte(key), []byte(value), nil) +} + +func (mk *memKeys) Delete(key string) error { + mk.mu.Lock() + defer mk.mu.Unlock() + err := mk.db.Delete([]byte(key), nil) + if err == db.ErrNotFound { + return nil + } + return err +} + +func (mk *memKeys) BeginBatch() BatchMutation { + return &batch{} +} + +func (mk *memKeys) CommitBatch(bm BatchMutation) error { + b, ok := bm.(*batch) + if !ok { + return errors.New("invalid batch type; not an instance returned by BeginBatch") + } + mk.mu.Lock() + defer mk.mu.Unlock() + for _, m := range b.Mutations() { + if m.IsDelete() { + if err := mk.db.Delete([]byte(m.Key()), nil); err != nil { + return err + } + } else { + if err := CheckSizes(m.Key(), m.Value()); err != nil { + return err + } + if err := mk.db.Set([]byte(m.Key()), []byte(m.Value()), nil); err != nil { + return err + } + } + } + return nil +} + +func (mk *memKeys) Close() error { return nil } + +func init() { + RegisterKeyValue("memory", func(cfg jsonconfig.Obj) (KeyValue, error) { + if err := cfg.Validate(); err != nil { + return nil, err + } + return NewMemoryKeyValue(), nil + }) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/mem_test.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/mem_test.go new file mode 100644 index 00000000..2f722e26 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/mem_test.go @@ -0,0 +1,43 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sorted_test + +import ( + "testing" + + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/kvtest" +) + +func TestMemoryKV(t *testing.T) { + kv := sorted.NewMemoryKeyValue() + kvtest.TestSorted(t, kv) +} + +// TODO(mpl): move this test into kvtest. But that might require +// kvtest taking a "func () sorted.KeyValue) constructor param, +// so kvtest can create several and close in different ways. +func TestMemoryKV_DoubleClose(t *testing.T) { + kv := sorted.NewMemoryKeyValue() + + it := kv.Find("", "") + it.Close() + it.Close() + + kv.Close() + kv.Close() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/mongo/mongokv.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/mongo/mongokv.go new file mode 100644 index 00000000..69978036 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/mongo/mongokv.go @@ -0,0 +1,280 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package mongo provides an implementation of sorted.KeyValue +// using MongoDB. +package mongo + +import ( + "bytes" + "errors" + "sync" + "time" + + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" + + "camlistore.org/third_party/labix.org/v2/mgo" + "camlistore.org/third_party/labix.org/v2/mgo/bson" +) + +// We explicitely separate the key and the value in a document, +// instead of simply storing as key:value, to avoid problems +// such as "." being an illegal char in a key name. Also because +// there is no way to do partial matching for key names (one can +// only check for their existence with bson.M{$exists: true}). +const ( + CollectionName = "keys" // MongoDB collection, equiv. to SQL table + mgoKey = "k" + mgoValue = "v" +) + +func init() { + sorted.RegisterKeyValue("mongo", newKeyValueFromJSONConfig) +} + +func newKeyValueFromJSONConfig(cfg jsonconfig.Obj) (sorted.KeyValue, error) { + ins := &instance{ + server: cfg.OptionalString("host", "localhost"), + database: cfg.RequiredString("database"), + user: cfg.OptionalString("user", ""), + password: cfg.OptionalString("password", ""), + } + if err := cfg.Validate(); err != nil { + return nil, err + } + db, err := ins.getCollection() + if err != nil { + return nil, err + } + return &keyValue{db: db, session: ins.session}, nil +} + +// Implementation of Iterator +type iter struct { + res bson.M + *mgo.Iter + end []byte +} + +func (it *iter) Next() bool { + if !it.Iter.Next(&it.res) { + return false + } + if len(it.end) > 0 && bytes.Compare(it.KeyBytes(), it.end) >= 0 { + return false + } + return true +} + +func (it *iter) Key() string { + key, ok := (it.res[mgoKey]).(string) + if !ok { + return "" + } + return key +} + +func (it *iter) KeyBytes() []byte { + // TODO(bradfitz,mpl): this is less efficient than the string way. we should + // do better here, somehow, like all the other KeyValue iterators. + // For now: + return []byte(it.Key()) +} + +func (it *iter) Value() string { + value, ok := (it.res[mgoValue]).(string) + if !ok { + return "" + } + return value +} + +func (it *iter) ValueBytes() []byte { + // TODO(bradfitz,mpl): this is less efficient than the string way. we should + // do better here, somehow, like all the other KeyValue iterators. + // For now: + return []byte(it.Value()) +} + +func (it *iter) Close() error { + return it.Iter.Close() +} + +// Implementation of KeyValue +type keyValue struct { + session *mgo.Session // so we can close it + mu sync.Mutex // guards db + db *mgo.Collection +} + +func (kv *keyValue) Get(key string) (string, error) { + kv.mu.Lock() + defer kv.mu.Unlock() + res := bson.M{} + q := kv.db.Find(&bson.M{mgoKey: key}) + err := q.One(&res) + if err != nil { + if err == mgo.ErrNotFound { + return "", sorted.ErrNotFound + } else { + return "", err + } + } + return res[mgoValue].(string), err +} + +func (kv *keyValue) Find(start, end string) sorted.Iterator { + kv.mu.Lock() + defer kv.mu.Unlock() + it := kv.db.Find(&bson.M{mgoKey: &bson.M{"$gte": start}}).Sort(mgoKey).Iter() + return &iter{res: bson.M{}, Iter: it, end: []byte(end)} +} + +func (kv *keyValue) Set(key, value string) error { + if err := sorted.CheckSizes(key, value); err != nil { + return err + } + kv.mu.Lock() + defer kv.mu.Unlock() + _, err := kv.db.Upsert(&bson.M{mgoKey: key}, &bson.M{mgoKey: key, mgoValue: value}) + return err +} + +// Delete removes the document with the matching key. +func (kv *keyValue) Delete(key string) error { + kv.mu.Lock() + defer kv.mu.Unlock() + err := kv.db.Remove(&bson.M{mgoKey: key}) + if err == mgo.ErrNotFound { + return nil + } + return err +} + +// Wipe removes all documents from the collection. +func (kv *keyValue) Wipe() error { + kv.mu.Lock() + defer kv.mu.Unlock() + _, err := kv.db.RemoveAll(nil) + return err +} + +type batch interface { + Mutations() []sorted.Mutation +} + +func (kv *keyValue) BeginBatch() sorted.BatchMutation { + return sorted.NewBatchMutation() +} + +func (kv *keyValue) CommitBatch(bm sorted.BatchMutation) error { + b, ok := bm.(batch) + if !ok { + return errors.New("invalid batch type") + } + + kv.mu.Lock() + defer kv.mu.Unlock() + for _, m := range b.Mutations() { + if m.IsDelete() { + if err := kv.db.Remove(bson.M{mgoKey: m.Key()}); err != nil { + return err + } + } else { + if err := sorted.CheckSizes(m.Key(), m.Value()); err != nil { + return err + } + if _, err := kv.db.Upsert(&bson.M{mgoKey: m.Key()}, &bson.M{mgoKey: m.Key(), mgoValue: m.Value()}); err != nil { + return err + } + } + } + return nil +} + +func (kv *keyValue) Close() error { + kv.session.Close() + return nil +} + +// Ping tests if MongoDB on host can be dialed. +func Ping(host string, timeout time.Duration) bool { + return (&instance{server: host}).ping(timeout) +} + +// instance helps with the low level details about +// the connection to MongoDB. +type instance struct { + server string + database string + user string + password string + session *mgo.Session +} + +func (ins *instance) url() string { + if ins.user == "" || ins.password == "" { + return ins.server + } + return ins.user + ":" + ins.password + "@" + ins.server + "/" + ins.database +} + +// ping won't work with old (1.2) mongo servers. +func (ins *instance) ping(timeout time.Duration) bool { + session, err := mgo.DialWithTimeout(ins.url(), timeout) + if err != nil { + return false + } + defer session.Close() + session.SetSyncTimeout(timeout) + if err = session.Ping(); err != nil { + return false + } + return true +} + +func (ins *instance) getConnection() (*mgo.Session, error) { + if ins.session != nil { + return ins.session, nil + } + // TODO(mpl): do some "client caching" as in mysql, to avoid systematically dialing? + session, err := mgo.Dial(ins.url()) + if err != nil { + return nil, err + } + session.SetMode(mgo.Monotonic, true) + session.SetSafe(&mgo.Safe{}) // so we get an ErrNotFound error when deleting an absent key + ins.session = session + return session, nil +} + +// TODO(mpl): I'm only calling getCollection at the beginning, and +// keeping the collection around and reusing it everywhere, instead +// of calling getCollection everytime, because that's the easiest. +// But I can easily change that. Gustavo says it does not make +// much difference either way. +// Brad, what do you think? +func (ins *instance) getCollection() (*mgo.Collection, error) { + session, err := ins.getConnection() + if err != nil { + return nil, err + } + session.SetSafe(&mgo.Safe{}) + session.SetMode(mgo.Strong, true) + c := session.DB(ins.database).C(CollectionName) + return c, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/mongo/mongokv_test.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/mongo/mongokv_test.go new file mode 100644 index 00000000..25945d72 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/mongo/mongokv_test.go @@ -0,0 +1,44 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mongo + +import ( + "testing" + + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/kvtest" + "camlistore.org/pkg/test/dockertest" +) + +// TestMongoKV tests against a real MongoDB instance, using a Docker container. +func TestMongoKV(t *testing.T) { + // SetupMongoContainer may skip or fatal the test if docker isn't found or something goes wrong when setting up the container. + // Thus, no error is returned + containerID, ip := dockertest.SetupMongoContainer(t) + defer containerID.KillRemove(t) + + kv, err := sorted.NewKeyValue(jsonconfig.Obj{ + "type": "mongo", + "host": ip, + "database": "camlitest", + }) + if err != nil { + t.Fatalf("mongo.NewKeyValue = %v", err) + } + kvtest.TestSorted(t, kv) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/cloudsql.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/cloudsql.go new file mode 100644 index 00000000..8109a838 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/cloudsql.go @@ -0,0 +1,69 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mysql + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "strings" + + "golang.org/x/net/context" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + sqladmin "google.golang.org/api/sqladmin/v1beta3" + "google.golang.org/cloud/compute/metadata" +) + +const cloudSQLSuffix = ".cloudsql.google.internal" + +func maybeRemapCloudSQL(host string) (out string, err error) { + if !strings.HasSuffix(host, cloudSQLSuffix) { + return host, nil + } + inst := strings.TrimSuffix(host, cloudSQLSuffix) + if !metadata.OnGCE() { + return "", errors.New("CloudSQL support only available when running on Google Compute Engine.") + } + proj, err := metadata.ProjectID() + if err != nil { + return "", fmt.Errorf("Failed to lookup GCE project ID: %v", err) + } + + admin, _ := sqladmin.New(oauth2.NewClient(context.Background(), google.ComputeTokenSource(""))) + listRes, err := admin.Instances.List(proj).Do() + if err != nil { + return "", fmt.Errorf("error enumerating Cloud SQL instances: %v", err) + } + for _, it := range listRes.Items { + if !strings.EqualFold(it.Instance, inst) { + continue + } + js, _ := json.Marshal(it) + log.Printf("Found Cloud SQL instance %s: %s", inst, js) + for _, ipm := range it.IpAddresses { + return ipm.IpAddress, nil + } + return "", fmt.Errorf("No external IP address for Cloud SQL instances %s", inst) + } + var found []string + for _, it := range listRes.Items { + found = append(found, it.Instance) + } + return "", fmt.Errorf("Cloud SQL instance %q not found. Found: %q", inst, found) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/dbschema.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/dbschema.go new file mode 100644 index 00000000..bf8b338f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/dbschema.go @@ -0,0 +1,47 @@ +/* +Copyright 2011 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mysql + +import ( + "strconv" + + "camlistore.org/pkg/sorted" +) + +const requiredSchemaVersion = 22 + +func SchemaVersion() int { + return requiredSchemaVersion +} + +// Note: using character set "binary", as any knowledge +// of character set encodings is handled by higher layers. +// At this layer we're just obeying the IndexStorage interface, +// which is purely about bytes. +func SQLCreateTables() []string { + return []string{ + `CREATE TABLE IF NOT EXISTS /*DB*/.rows ( + k VARCHAR(` + strconv.Itoa(sorted.MaxKeySize) + `) NOT NULL PRIMARY KEY, + v VARCHAR(` + strconv.Itoa(sorted.MaxValueSize) + `)) + DEFAULT CHARACTER SET binary`, + + `CREATE TABLE IF NOT EXISTS /*DB*/.meta ( + metakey VARCHAR(255) NOT NULL PRIMARY KEY, + value VARCHAR(255) NOT NULL) + DEFAULT CHARACTER SET binary`, + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/mysqlkv.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/mysqlkv.go new file mode 100644 index 00000000..1e6f3c92 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/mysqlkv.go @@ -0,0 +1,211 @@ +/* +Copyright 2011 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package mysql provides an implementation of sorted.KeyValue +// on top of MySQL. +package mysql + +import ( + "database/sql" + "errors" + "fmt" + "os" + "regexp" + "strconv" + "strings" + "sync" + + "camlistore.org/pkg/env" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/sqlkv" + _ "camlistore.org/third_party/github.com/go-sql-driver/mysql" +) + +func init() { + sorted.RegisterKeyValue("mysql", newKeyValueFromJSONConfig) +} + +func newKeyValueFromJSONConfig(cfg jsonconfig.Obj) (sorted.KeyValue, error) { + var ( + user = cfg.RequiredString("user") + database = cfg.RequiredString("database") + host = cfg.OptionalString("host", "") + password = cfg.OptionalString("password", "") + ) + if err := cfg.Validate(); err != nil { + return nil, err + } + var err error + if host != "" { + host, err = maybeRemapCloudSQL(host) + if err != nil { + return nil, err + } + if !strings.Contains(host, ":") { + host += ":3306" + } + host = "tcp(" + host + ")" + } + // The DSN does NOT have a database name in it so it's + // cacheable and can be shared between different queues & the + // index, all sharing the same database server, cutting down + // number of TCP connections required. We add the database + // name in queries instead. + dsn := fmt.Sprintf("%s:%s@%s/", user, password, host) + + db, err := openOrCachedDB(dsn) + if err != nil { + return nil, err + } + + if err := CreateDB(db, database); err != nil { + return nil, err + } + for _, tableSQL := range SQLCreateTables() { + tableSQL = strings.Replace(tableSQL, "/*DB*/", database, -1) + if _, err := db.Exec(tableSQL); err != nil { + errMsg := "error creating table with %q: %v." + createError := err + sv, err := serverVersion(db) + if err != nil { + return nil, err + } + if !hasLargeVarchar(sv) { + errMsg += "\nYour MySQL server is too old (< 5.0.3) to support VARCHAR larger than 255." + } + return nil, fmt.Errorf(errMsg, tableSQL, createError) + } + } + if _, err := db.Exec(fmt.Sprintf(`REPLACE INTO %s.meta VALUES ('version', '%d')`, database, SchemaVersion())); err != nil { + return nil, fmt.Errorf("error setting schema version: %v", err) + } + + kv := &keyValue{ + db: db, + KeyValue: &sqlkv.KeyValue{ + DB: db, + TablePrefix: database + ".", + }, + } + if err := kv.ping(); err != nil { + return nil, fmt.Errorf("MySQL db unreachable: %v", err) + } + version, err := kv.SchemaVersion() + if err != nil { + return nil, fmt.Errorf("error getting schema version (need to init database?): %v", err) + } + if version != requiredSchemaVersion { + if version == 20 && requiredSchemaVersion == 21 { + fmt.Fprintf(os.Stderr, fixSchema20to21) + } + if env.IsDev() { + // Good signal that we're using the devcam server, so help out + // the user with a more useful tip: + return nil, fmt.Errorf("database schema version is %d; expect %d (run \"devcam server --wipe\" to wipe both your blobs and re-populate the database schema)", version, requiredSchemaVersion) + } + return nil, fmt.Errorf("database schema version is %d; expect %d (need to re-init/upgrade database?)", + version, requiredSchemaVersion) + } + + return kv, nil +} + +// CreateDB creates the named database if it does not already exist. +func CreateDB(db *sql.DB, dbname string) error { + if dbname == "" { + return errors.New("can not create database: database name is missing") + } + if _, err := db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", dbname)); err != nil { + return fmt.Errorf("error creating database %v: %v", dbname, err) + } + return nil +} + +// We keep a cache of open database handles. +var ( + dbsmu sync.Mutex + dbs = map[string]*sql.DB{} // DSN -> db +) + +func openOrCachedDB(dsn string) (*sql.DB, error) { + dbsmu.Lock() + defer dbsmu.Unlock() + if db, ok := dbs[dsn]; ok { + return db, nil + } + db, err := sql.Open("mysql", dsn) + if err != nil { + return nil, err + } + dbs[dsn] = db + return db, nil +} + +type keyValue struct { + *sqlkv.KeyValue + + db *sql.DB +} + +func (kv *keyValue) ping() error { + // TODO(bradfitz): something more efficient here? + _, err := kv.SchemaVersion() + return err +} + +func (kv *keyValue) SchemaVersion() (version int, err error) { + err = kv.db.QueryRow("SELECT value FROM " + kv.KeyValue.TablePrefix + "meta WHERE metakey='version'").Scan(&version) + return +} + +const fixSchema20to21 = `Character set in tables changed to binary, you can fix your tables with: +ALTER TABLE rows CONVERT TO CHARACTER SET binary; +ALTER TABLE meta CONVERT TO CHARACTER SET binary; +UPDATE meta SET value=21 WHERE metakey='version' AND value=20; +` + +// serverVersion returns the MySQL server version as []int{major, minor, revision}. +func serverVersion(db *sql.DB) ([]int, error) { + versionRx := regexp.MustCompile(`([0-9]+)\.([0-9]+)\.([0-9]+)-.*`) + var version string + if err := db.QueryRow("SELECT VERSION()").Scan(&version); err != nil { + return nil, fmt.Errorf("error getting MySQL server version: %v", err) + } + m := versionRx.FindStringSubmatch(version) + if len(m) < 4 { + return nil, fmt.Errorf("bogus MySQL server version: %v", version) + } + major, _ := strconv.Atoi(m[1]) + minor, _ := strconv.Atoi(m[2]) + rev, _ := strconv.Atoi(m[3]) + return []int{major, minor, rev}, nil +} + +// hasLargeVarchar returns whether the given version (as []int{major, minor, revision}) +// supports VARCHAR larger than 255. +func hasLargeVarchar(version []int) bool { + if len(version) < 3 { + panic(fmt.Sprintf("bogus mysql server version %v: ", version)) + } + if version[0] < 5 { + return false + } + if version[1] > 0 { + return true + } + return version[0] == 5 && version[1] == 0 && version[2] >= 3 +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/mysqlkv_test.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/mysqlkv_test.go new file mode 100644 index 00000000..8d79ec12 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/mysql/mysqlkv_test.go @@ -0,0 +1,49 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mysql + +import ( + "testing" + + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/kvtest" + "camlistore.org/pkg/test/dockertest" +) + +// TestMySQLKV tests against a real MySQL instance, using a Docker container. +func TestMySQLKV(t *testing.T) { + dbname := "camlitest_" + osutil.Username() + containerID, ip := dockertest.SetupMySQLContainer(t, dbname) + defer containerID.KillRemove(t) + + // TODO(mpl): add test for serverVersion once we host the docker image ourselves + // (and hence have the control over the version). + + kv, err := sorted.NewKeyValue(jsonconfig.Obj{ + "type": "mysql", + "host": ip + ":3306", + "database": dbname, + "user": dockertest.MySQLUsername, + "password": dockertest.MySQLPassword, + }) + if err != nil { + t.Fatalf("mysql.NewKeyValue = %v", err) + } + kvtest.TestSorted(t, kv) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/postgres/dbschema.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/postgres/dbschema.go new file mode 100644 index 00000000..4d9385fb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/postgres/dbschema.go @@ -0,0 +1,108 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package postgres + +import ( + "strconv" + + "camlistore.org/pkg/sorted" +) + +const requiredSchemaVersion = 2 + +func SchemaVersion() int { + return requiredSchemaVersion +} + +func SQLCreateTables() []string { + return []string{ + `CREATE TABLE IF NOT EXISTS rows ( + k VARCHAR(` + strconv.Itoa(sorted.MaxKeySize) + `) NOT NULL PRIMARY KEY, + v VARCHAR(` + strconv.Itoa(sorted.MaxValueSize) + `))`, + + `CREATE TABLE IF NOT EXISTS meta ( + metakey VARCHAR(255) NOT NULL PRIMARY KEY, + value VARCHAR(255) NOT NULL)`, + } +} + +func SQLDefineReplace() []string { + return []string{ + // The first 3 statements here are a work around that allows us to issue + // the "CREATE LANGUAGE plpsql;" statement only if the language doesn't + // already exist. + `CREATE OR REPLACE FUNCTION create_language_plpgsql() RETURNS INTEGER AS +$$ +CREATE LANGUAGE plpgsql; +SELECT 1; +$$ +LANGUAGE SQL;`, + + `SELECT CASE WHEN NOT +( + SELECT TRUE AS exists + FROM pg_language + WHERE lanname = 'plpgsql' + UNION + SELECT FALSE AS exists + ORDER BY exists DESC + LIMIT 1 +) +THEN + create_language_plpgsql() +ELSE + 0 +END AS plpgsql_created;`, + + `DROP FUNCTION create_language_plpgsql();`, + + `CREATE OR REPLACE FUNCTION replaceinto(key TEXT, value TEXT) RETURNS VOID AS +$$ +BEGIN + LOOP + UPDATE rows SET v = value WHERE k = key; + IF found THEN + RETURN; + END IF; + BEGIN + INSERT INTO rows(k,v) VALUES (key, value); + RETURN; + EXCEPTION WHEN unique_violation THEN + END; + END LOOP; +END; +$$ +LANGUAGE plpgsql;`, + `CREATE OR REPLACE FUNCTION replaceintometa(key TEXT, val TEXT) RETURNS VOID AS +$$ +BEGIN + LOOP + UPDATE meta SET value = val WHERE metakey = key; + IF found THEN + RETURN; + END IF; + BEGIN + INSERT INTO meta(metakey,value) VALUES (key, val); + RETURN; + EXCEPTION WHEN unique_violation THEN + END; + END LOOP; +END; +$$ +LANGUAGE plpgsql;`, + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/postgres/postgreskv.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/postgres/postgreskv.go new file mode 100644 index 00000000..227a5bd2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/postgres/postgreskv.go @@ -0,0 +1,143 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package postgres provides an implementation of sorted.KeyValue +// on top of PostgreSQL. +package postgres + +import ( + "database/sql" + "fmt" + "regexp" + + "camlistore.org/pkg/env" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/sqlkv" + + _ "camlistore.org/third_party/github.com/lib/pq" +) + +func init() { + sorted.RegisterKeyValue("postgres", newKeyValueFromJSONConfig) +} + +func newKeyValueFromJSONConfig(cfg jsonconfig.Obj) (sorted.KeyValue, error) { + conninfo := fmt.Sprintf("user=%s dbname=%s host=%s password=%s sslmode=%s", + cfg.RequiredString("user"), + cfg.RequiredString("database"), + cfg.OptionalString("host", "localhost"), + cfg.OptionalString("password", ""), + cfg.OptionalString("sslmode", "require"), + ) + if err := cfg.Validate(); err != nil { + return nil, err + } + db, err := sql.Open("postgres", conninfo) + if err != nil { + return nil, err + } + for _, tableSql := range SQLCreateTables() { + if _, err := db.Exec(tableSql); err != nil { + return nil, fmt.Errorf("error creating table with %q: %v", tableSql, err) + } + } + for _, statement := range SQLDefineReplace() { + if _, err := db.Exec(statement); err != nil { + return nil, fmt.Errorf("error setting up replace statement with %q: %v", statement, err) + } + } + r, err := db.Query(fmt.Sprintf(`SELECT replaceintometa('version', '%d')`, SchemaVersion())) + if err != nil { + return nil, fmt.Errorf("error setting schema version: %v", err) + } + r.Close() + + kv := &keyValue{ + db: db, + KeyValue: &sqlkv.KeyValue{ + DB: db, + SetFunc: altSet, + BatchSetFunc: altBatchSet, + PlaceHolderFunc: replacePlaceHolders, + }, + } + if err := kv.ping(); err != nil { + return nil, fmt.Errorf("PostgreSQL db unreachable: %v", err) + } + version, err := kv.SchemaVersion() + if err != nil { + return nil, fmt.Errorf("error getting schema version (need to init database?): %v", err) + } + if version != requiredSchemaVersion { + if env.IsDev() { + // Good signal that we're using the devcam server, so help out + // the user with a more useful tip: + return nil, fmt.Errorf("database schema version is %d; expect %d (run \"devcam server --wipe\" to wipe both your blobs and re-populate the database schema)", version, requiredSchemaVersion) + } + return nil, fmt.Errorf("database schema version is %d; expect %d (need to re-init/upgrade database?)", + version, requiredSchemaVersion) + } + + return kv, nil +} + +type keyValue struct { + *sqlkv.KeyValue + db *sql.DB +} + +// postgres does not have REPLACE INTO (upsert), so we use that custom +// one for Set operations instead +func altSet(db *sql.DB, key, value string) error { + r, err := db.Query("SELECT replaceinto($1, $2)", key, value) + if err != nil { + return err + } + return r.Close() +} + +// postgres does not have REPLACE INTO (upsert), so we use that custom +// one for Set operations in batch instead +func altBatchSet(tx *sql.Tx, key, value string) error { + r, err := tx.Query("SELECT replaceinto($1, $2)", key, value) + if err != nil { + return err + } + return r.Close() +} + +var qmark = regexp.MustCompile(`\?`) + +// replace all ? placeholders into the corresponding $n in queries +var replacePlaceHolders = func(query string) string { + i := 0 + dollarInc := func(b []byte) []byte { + i++ + return []byte(fmt.Sprintf("$%d", i)) + } + return string(qmark.ReplaceAllFunc([]byte(query), dollarInc)) +} + +func (kv *keyValue) ping() error { + _, err := kv.SchemaVersion() + return err +} + +func (kv *keyValue) SchemaVersion() (version int, err error) { + err = kv.db.QueryRow("SELECT value FROM meta WHERE metakey='version'").Scan(&version) + return +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/postgres/postgreskv_test.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/postgres/postgreskv_test.go new file mode 100644 index 00000000..2d825e4e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/postgres/postgreskv_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package postgres + +import ( + "testing" + + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/kvtest" + "camlistore.org/pkg/test/dockertest" +) + +// TestPostgreSQLKV tests against a real PostgreSQL instance, using a Docker container. +func TestPostgreSQLKV(t *testing.T) { + dbname := "camlitest_" + osutil.Username() + containerID, ip := dockertest.SetupPostgreSQLContainer(t, dbname) + defer containerID.KillRemove(t) + + kv, err := sorted.NewKeyValue(jsonconfig.Obj{ + "type": "postgres", + "host": ip, + "database": dbname, + "user": dockertest.PostgresUsername, + "password": dockertest.PostgresPassword, + "sslmode": "disable", + }) + if err != nil { + t.Fatalf("postgres.NewKeyValue = %v", err) + } + kvtest.TestSorted(t, kv) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/dbschema.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/dbschema.go new file mode 100644 index 00000000..fa467d91 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/dbschema.go @@ -0,0 +1,110 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sqlite + +import ( + "bytes" + "database/sql" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + + "camlistore.org/pkg/sorted" +) + +const requiredSchemaVersion = 1 + +func SchemaVersion() int { + return requiredSchemaVersion +} + +func SQLCreateTables() []string { + // sqlite ignores n in VARCHAR(n), but setting it as such for consistency with + // other sqls. + return []string{ + `CREATE TABLE rows ( + k VARCHAR(` + strconv.Itoa(sorted.MaxKeySize) + `) NOT NULL PRIMARY KEY, + v VARCHAR(` + strconv.Itoa(sorted.MaxValueSize) + `))`, + + `CREATE TABLE meta ( + metakey VARCHAR(255) NOT NULL PRIMARY KEY, + value VARCHAR(255) NOT NULL)`, + } +} + +// IsWALCapable checks if the installed sqlite3 library can +// use Write-Ahead Logging (i.e version >= 3.7.0) +func IsWALCapable() bool { + // TODO(mpl): alternative to make it work on windows + cmdPath, err := exec.LookPath("pkg-config") + if err != nil { + log.Printf("Could not find pkg-config to check sqlite3 lib version: %v", err) + return false + } + var stderr bytes.Buffer + cmd := exec.Command(cmdPath, "--modversion", "sqlite3") + cmd.Stderr = &stderr + if runtime.GOOS == "darwin" && os.Getenv("PKG_CONFIG_PATH") == "" { + matches, err := filepath.Glob("/usr/local/Cellar/sqlite/*/lib/pkgconfig/sqlite3.pc") + if err == nil && len(matches) > 0 { + cmd.Env = append(os.Environ(), "PKG_CONFIG_PATH="+filepath.Dir(matches[0])) + } + } + + out, err := cmd.Output() + if err != nil { + log.Printf("Could not check sqlite3 version: %v\n", stderr.String()) + return false + } + version := strings.TrimRight(string(out), "\n") + return version >= "3.7.0" +} + +// EnableWAL returns the statement to enable Write-Ahead Logging, +// which improves SQLite concurrency. +// Requires SQLite >= 3.7.0 +func EnableWAL() string { + return "PRAGMA journal_mode = WAL" +} + +// initDB creates a new sqlite database based on the file at path. +func initDB(path string) error { + db, err := sql.Open("sqlite3", path) + if err != nil { + return err + } + defer db.Close() + for _, tableSql := range SQLCreateTables() { + if _, err := db.Exec(tableSql); err != nil { + return err + } + } + if IsWALCapable() { + if _, err := db.Exec(EnableWAL()); err != nil { + return err + } + } else { + log.Print("WARNING: An SQLite DB without Write Ahead Logging will most likely fail. See http://camlistore.org/issues/114") + } + _, err = db.Exec(fmt.Sprintf(`REPLACE INTO meta VALUES ('version', '%d')`, SchemaVersion())) + return err +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/sqlite_cond.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/sqlite_cond.go new file mode 100644 index 00000000..b53ff42e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/sqlite_cond.go @@ -0,0 +1,11 @@ +// +build with_sqlite + +package sqlite + +import ( + _ "camlistore.org/third_party/github.com/mattn/go-sqlite3" +) + +func init() { + compiled = true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/sqlitekv.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/sqlitekv.go new file mode 100644 index 00000000..04f92802 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/sqlitekv.go @@ -0,0 +1,122 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package sqlite provides an implementation of sorted.KeyValue +// using an SQLite database file. +package sqlite + +import ( + "database/sql" + "errors" + "fmt" + "os" + + "camlistore.org/pkg/env" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/sqlkv" +) + +func init() { + sorted.RegisterKeyValue("sqlite", newKeyValueFromConfig) +} + +func newKeyValueFromConfig(cfg jsonconfig.Obj) (sorted.KeyValue, error) { + if !compiled { + return nil, ErrNotCompiled + } + + file := cfg.RequiredString("file") + if err := cfg.Validate(); err != nil { + return nil, err + } + + fi, err := os.Stat(file) + if os.IsNotExist(err) || (err == nil && fi.Size() == 0) { + if err := initDB(file); err != nil { + return nil, fmt.Errorf("could not initialize sqlite DB at %s: %v", file, err) + } + } + db, err := sql.Open("sqlite3", file) + if err != nil { + return nil, err + } + kv := &keyValue{ + file: file, + db: db, + KeyValue: &sqlkv.KeyValue{ + DB: db, + Serial: true, + }, + } + + version, err := kv.SchemaVersion() + if err != nil { + return nil, fmt.Errorf("error getting schema version (need to init database with 'camtool dbinit %s'?): %v", file, err) + } + + if err := kv.ping(); err != nil { + return nil, err + } + + if version != requiredSchemaVersion { + if env.IsDev() { + // Good signal that we're using the devcam server, so help out + // the user with a more useful tip: + return nil, fmt.Errorf("database schema version is %d; expect %d (run \"devcam server --wipe\" to wipe both your blobs and re-populate the database schema)", version, requiredSchemaVersion) + } + return nil, fmt.Errorf("database schema version is %d; expect %d (need to re-init/upgrade database?)", + version, requiredSchemaVersion) + } + + return kv, nil + +} + +type keyValue struct { + *sqlkv.KeyValue + + file string + db *sql.DB +} + +var compiled = false + +// CompiledIn returns whether SQLite support is compiled in. +// If it returns false, the build tag "with_sqlite" was not specified. +func CompiledIn() bool { + return compiled +} + +var ErrNotCompiled = errors.New("camlistored was not built with SQLite support. If you built with make.go, use go run make.go --sqlite=true. If you used go get or get install, use go {get,install} --tags=with_sqlite" + compileHint()) + +func compileHint() string { + if _, err := os.Stat("/etc/apt"); err == nil { + return " (Hint: apt-get install libsqlite3-dev)" + } + return "" +} + +func (kv *keyValue) ping() error { + // TODO(bradfitz): something more efficient here? + _, err := kv.SchemaVersion() + return err +} + +func (kv *keyValue) SchemaVersion() (version int, err error) { + err = kv.db.QueryRow("SELECT value FROM meta WHERE metakey='version'").Scan(&version) + return +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/sqlitekv_test.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/sqlitekv_test.go new file mode 100644 index 00000000..570f082e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlite/sqlitekv_test.go @@ -0,0 +1,48 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sqlite + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" + "camlistore.org/pkg/sorted/kvtest" +) + +func TestSQLiteKV(t *testing.T) { + if !CompiledIn() { + t.Skip(ErrNotCompiled.Error()) + } + tmpDir, err := ioutil.TempDir("", "camlistore-sqlitekv_test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + dbname := filepath.Join(tmpDir, "testdb.sqlite") + kv, err := sorted.NewKeyValue(jsonconfig.Obj{ + "type": "sqlite", + "file": dbname, + }) + if err != nil { + t.Fatalf("Could not create sqlite sorted kv at %v: %v", dbname, err) + } + kvtest.TestSorted(t, kv) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlkv/sqlkv.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlkv/sqlkv.go new file mode 100644 index 00000000..93b51abb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlkv/sqlkv.go @@ -0,0 +1,296 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package sqlkv implements the sorted.KeyValue interface using an *sql.DB. +package sqlkv + +import ( + "database/sql" + "errors" + "fmt" + "log" + "regexp" + "strings" + "sync" + + "camlistore.org/pkg/leak" + "camlistore.org/pkg/sorted" +) + +// KeyValue implements the sorted.KeyValue interface using an *sql.DB. +type KeyValue struct { + DB *sql.DB + + // SetFunc is an optional func to use when REPLACE INTO does not exist + SetFunc func(*sql.DB, string, string) error + BatchSetFunc func(*sql.Tx, string, string) error + + // PlaceHolderFunc optionally replaces ? placeholders + // with the right ones for the rdbms in use. + PlaceHolderFunc func(string) string + + // Serial determines whether a Go-level mutex protects DB from + // concurrent access. This isn't perfect and exists just for + // SQLite, whose driver likes to return "the database is + // locked" (camlistore.org/issue/114), so this keeps some + // pressure off. But we still trust SQLite to deal with + // concurrency in most cases. + Serial bool + + // TablePrefix optionally provides a prefix for SQL table + // names. This is typically "dbname.", ending in a period. + TablePrefix string + + mu sync.Mutex // the mutex used, if Serial is set + + queriesInitOnce sync.Once // guards initialization of both queries and replacer + replacer *strings.Replacer + + queriesMu sync.RWMutex + queries map[string]string +} + +// sql returns the query, replacing placeholders using PlaceHolderFunc, +// and /*TPRE*/ with TablePrefix. +func (kv *KeyValue) sql(sqlStmt string) string { + // string manipulation is done only once + kv.queriesInitOnce.Do(func() { + kv.queries = make(map[string]string, 8) // we have 8 queries in this file + kv.replacer = strings.NewReplacer("/*TPRE*/", kv.TablePrefix) + }) + kv.queriesMu.RLock() + sqlQuery, ok := kv.queries[sqlStmt] + kv.queriesMu.RUnlock() + if ok { + return sqlQuery + } + kv.queriesMu.Lock() + // check again, now holding the lock + if sqlQuery, ok = kv.queries[sqlStmt]; ok { + kv.queriesMu.Unlock() + return sqlQuery + } + sqlQuery = sqlStmt + if f := kv.PlaceHolderFunc; f != nil { + sqlQuery = f(sqlQuery) + } + sqlQuery = kv.replacer.Replace(sqlQuery) + kv.queries[sqlStmt] = sqlQuery + kv.queriesMu.Unlock() + return sqlQuery +} + +type batchTx struct { + tx *sql.Tx + err error // sticky + kv *KeyValue +} + +func (b *batchTx) Set(key, value string) { + if b.err != nil { + return + } + if err := sorted.CheckSizes(key, value); err != nil { + if err == sorted.ErrKeyTooLarge { + b.err = fmt.Errorf("%v: %v", err, key) + } else { + b.err = fmt.Errorf("%v: %v", err, value) + } + return + } + if b.kv.BatchSetFunc != nil { + b.err = b.kv.BatchSetFunc(b.tx, key, value) + return + } + _, b.err = b.tx.Exec(b.kv.sql("REPLACE INTO /*TPRE*/rows (k, v) VALUES (?, ?)"), key, value) +} + +func (b *batchTx) Delete(key string) { + if b.err != nil { + return + } + _, b.err = b.tx.Exec(b.kv.sql("DELETE FROM /*TPRE*/rows WHERE k=?"), key) +} + +func (kv *KeyValue) BeginBatch() sorted.BatchMutation { + if kv.Serial { + kv.mu.Lock() + } + tx, err := kv.DB.Begin() + if err != nil { + log.Printf("SQL BEGIN BATCH: %v", err) + } + return &batchTx{ + tx: tx, + err: err, + kv: kv, + } +} + +func (kv *KeyValue) CommitBatch(b sorted.BatchMutation) error { + if kv.Serial { + defer kv.mu.Unlock() + } + bt, ok := b.(*batchTx) + if !ok { + return fmt.Errorf("wrong BatchMutation type %T", b) + } + if bt.err != nil { + return bt.err + } + return bt.tx.Commit() +} + +func (kv *KeyValue) Get(key string) (value string, err error) { + if kv.Serial { + kv.mu.Lock() + defer kv.mu.Unlock() + } + err = kv.DB.QueryRow(kv.sql("SELECT v FROM /*TPRE*/rows WHERE k=?"), key).Scan(&value) + if err == sql.ErrNoRows { + err = sorted.ErrNotFound + } + return +} + +func (kv *KeyValue) Set(key, value string) error { + if err := sorted.CheckSizes(key, value); err != nil { + return err + } + if kv.Serial { + kv.mu.Lock() + defer kv.mu.Unlock() + } + if kv.SetFunc != nil { + return kv.SetFunc(kv.DB, key, value) + } + _, err := kv.DB.Exec(kv.sql("REPLACE INTO /*TPRE*/rows (k, v) VALUES (?, ?)"), key, value) + return err +} + +func (kv *KeyValue) Delete(key string) error { + if kv.Serial { + kv.mu.Lock() + defer kv.mu.Unlock() + } + _, err := kv.DB.Exec(kv.sql("DELETE FROM /*TPRE*/rows WHERE k=?"), key) + return err +} + +func (kv *KeyValue) Wipe() error { + if kv.Serial { + kv.mu.Lock() + defer kv.mu.Unlock() + } + _, err := kv.DB.Exec(kv.sql("DELETE FROM /*TPRE*/rows")) + return err +} + +func (kv *KeyValue) Close() error { return kv.DB.Close() } + +func (kv *KeyValue) Find(start, end string) sorted.Iterator { + if kv.Serial { + kv.mu.Lock() + // TODO(mpl): looks like sqlite considers the db locked until we've closed + // the iterator, so we can't do anything else until then. We should probably + // move that Unlock to the closing of the iterator. Investigating. + defer kv.mu.Unlock() + } + var rows *sql.Rows + var err error + if end == "" { + rows, err = kv.DB.Query(kv.sql("SELECT k, v FROM /*TPRE*/rows WHERE k >= ? ORDER BY k "), start) + } else { + rows, err = kv.DB.Query(kv.sql("SELECT k, v FROM /*TPRE*/rows WHERE k >= ? AND k < ? ORDER BY k "), start, end) + } + if err != nil { + log.Printf("unexpected query error: %v", err) + return &iter{err: err} + } + + it := &iter{ + kv: kv, + rows: rows, + closeCheck: leak.NewChecker(), + } + return it +} + +var wordThenPunct = regexp.MustCompile(`^\w+\W$`) + +// iter is a iterator over sorted key/value pairs in rows. +type iter struct { + kv *KeyValue + end string // optional end bound + err error // accumulated error, returned at Close + + closeCheck *leak.Checker + + rows *sql.Rows // if non-nil, the rows we're reading from + + key sql.RawBytes + val sql.RawBytes + skey, sval *string // if non-nil, it's been stringified +} + +var errClosed = errors.New("sqlkv: Iterator already closed") + +func (t *iter) KeyBytes() []byte { return t.key } +func (t *iter) Key() string { + if t.skey != nil { + return *t.skey + } + str := string(t.key) + t.skey = &str + return str +} + +func (t *iter) ValueBytes() []byte { return t.val } +func (t *iter) Value() string { + if t.sval != nil { + return *t.sval + } + str := string(t.val) + t.sval = &str + return str +} + +func (t *iter) Close() error { + t.closeCheck.Close() + if t.rows != nil { + t.rows.Close() + t.rows = nil + } + err := t.err + t.err = errClosed + return err +} + +func (t *iter) Next() bool { + if t.err != nil { + return false + } + t.skey, t.sval = nil, nil + if !t.rows.Next() { + return false + } + t.err = t.rows.Scan(&t.key, &t.val) + if t.err != nil { + log.Printf("unexpected Scan error: %v", t.err) + return false + } + return true +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlkv/sqlkv_test.go b/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlkv/sqlkv_test.go new file mode 100644 index 00000000..6cb30290 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/sorted/sqlkv/sqlkv_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2015 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sqlkv + +import ( + "strings" + "testing" +) + +var queries = []string{ + "REPLACE INTO /*TPRE*/rows (k, v) VALUES (?, ?)", + "DELETE FROM /*TPRE*/rows WHERE k=?", + "SELECT v FROM /*TPRE*/rows WHERE k=?", + "REPLACE INTO /*TPRE*/rows (k, v) VALUES (?, ?)", + "DELETE FROM /*TPRE*/rows WHERE k=?", + "DELETE FROM /*TPRE*/rows", + "SELECT k, v FROM /*TPRE*/rows WHERE k >= ? ORDER BY k ", + "SELECT k, v FROM /*TPRE*/rows WHERE k >= ? AND k < ? ORDER BY k ", +} + +var ( + qmarkRepl = strings.NewReplacer("?", ":placeholder") + + kv = &KeyValue{ + TablePrefix: "T_", + PlaceHolderFunc: func(q string) string { return qmarkRepl.Replace(q) }, + } +) + +func TestSql(t *testing.T) { + repl := strings.NewReplacer("/*TPRE*/", "T_", "?", ":placeholder") + for i, q := range queries { + want := repl.Replace(q) + got := kv.sql(q) + if want != got { + t.Errorf("%d. got %q, wanted %q.", i, got, want) + } + } +} + +func BenchmarkSql(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, s := range queries { + kv.sql(s) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/strutil/intern.go b/vendor/github.com/camlistore/camlistore/pkg/strutil/intern.go new file mode 100644 index 00000000..633ebb36 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/strutil/intern.go @@ -0,0 +1,39 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package strutil + +var internStr = map[string]string{} + +// RegisterCommonString adds common strings to the interned string +// table. This should be called during init from the main +// goroutine, not later at runtime. +func RegisterCommonString(s ...string) { + for _, v := range s { + internStr[v] = v + } +} + +// StringFromBytes returns string(v), minimizing copies for common values of v +// as previously registered with RegisterCommonString. +func StringFromBytes(v []byte) string { + // In Go 1.3, this string conversion in the map lookup does not allocate + // to make a new string. We depend on Go 1.3, so this is always free: + if s, ok := internStr[string(v)]; ok { + return s + } + return string(v) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/strutil/strconv.go b/vendor/github.com/camlistore/camlistore/pkg/strutil/strconv.go new file mode 100644 index 00000000..9d4ccfff --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/strutil/strconv.go @@ -0,0 +1,117 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package strutil + +import ( + "errors" + "strconv" +) + +// ParseUintBytes is like strconv.ParseUint, but using a []byte. +func ParseUintBytes(s []byte, base int, bitSize int) (n uint64, err error) { + var cutoff, maxVal uint64 + + if bitSize == 0 { + bitSize = int(strconv.IntSize) + } + + s0 := s + switch { + case len(s) < 1: + err = strconv.ErrSyntax + goto Error + + case 2 <= base && base <= 36: + // valid base; nothing to do + + case base == 0: + // Look for octal, hex prefix. + switch { + case s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X'): + base = 16 + s = s[2:] + if len(s) < 1 { + err = strconv.ErrSyntax + goto Error + } + case s[0] == '0': + base = 8 + default: + base = 10 + } + + default: + err = errors.New("invalid base " + strconv.Itoa(base)) + goto Error + } + + n = 0 + cutoff = cutoff64(base) + maxVal = 1<= base { + n = 0 + err = strconv.ErrSyntax + goto Error + } + + if n >= cutoff { + // n*base overflows + n = 1<<64 - 1 + err = strconv.ErrRange + goto Error + } + n *= uint64(base) + + n1 := n + uint64(v) + if n1 < n || n1 > maxVal { + // n+v overflows + n = 1<<64 - 1 + err = strconv.ErrRange + goto Error + } + n = n1 + } + + return n, nil + +Error: + return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} +} + +// Return the first number n such that n*base >= 1<<64. +func cutoff64(base int) uint64 { + if base < 2 { + return 0 + } + return (1<<64-1)/uint64(base) + 1 +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/strutil/strutil.go b/vendor/github.com/camlistore/camlistore/pkg/strutil/strutil.go new file mode 100644 index 00000000..41ce9797 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/strutil/strutil.go @@ -0,0 +1,200 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package strutil contains string and byte processing functions. +package strutil + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +// Fork of Go's implementation in pkg/strings/strings.go: +// Generic split: splits after each instance of sep, +// including sepSave bytes of sep in the subarrays. +func genSplit(dst []string, s, sep string, sepSave, n int) []string { + if n == 0 { + return nil + } + if sep == "" { + panic("sep is empty") + } + if n < 0 { + n = strings.Count(s, sep) + 1 + } + c := sep[0] + start := 0 + na := 0 + for i := 0; i+len(sep) <= len(s) && na+1 < n; i++ { + if s[i] == c && (len(sep) == 1 || s[i:i+len(sep)] == sep) { + dst = append(dst, s[start:i+sepSave]) + na++ + start = i + len(sep) + i += len(sep) - 1 + } + } + dst = append(dst, s[start:]) + return dst +} + +// AppendSplitN is like strings.SplitN but appends to and returns dst. +// Unlike strings.SplitN, an empty separator is not supported. +// The count n determines the number of substrings to return: +// n > 0: at most n substrings; the last substring will be the unsplit remainder. +// n == 0: the result is nil (zero substrings) +// n < 0: all substrings +func AppendSplitN(dst []string, s, sep string, n int) []string { + return genSplit(dst, s, sep, 0, n) +} + +// equalFoldRune compares a and b runes whether they fold equally. +// +// The code comes from strings.EqualFold, but shortened to only one rune. +func equalFoldRune(sr, tr rune) bool { + if sr == tr { + return true + } + // Make sr < tr to simplify what follows. + if tr < sr { + sr, tr = tr, sr + } + // Fast check for ASCII. + if tr < utf8.RuneSelf && 'A' <= sr && sr <= 'Z' { + // ASCII, and sr is upper case. tr must be lower case. + if tr == sr+'a'-'A' { + return true + } + return false + } + + // General case. SimpleFold(x) returns the next equivalent rune > x + // or wraps around to smaller values. + r := unicode.SimpleFold(sr) + for r != sr && r < tr { + r = unicode.SimpleFold(r) + } + if r == tr { + return true + } + return false +} + +// HasPrefixFold is like strings.HasPrefix but uses Unicode case-folding. +func HasPrefixFold(s, prefix string) bool { + if prefix == "" { + return true + } + for _, pr := range prefix { + if s == "" { + return false + } + // step with s, too + sr, size := utf8.DecodeRuneInString(s) + if sr == utf8.RuneError { + return false + } + s = s[size:] + if !equalFoldRune(sr, pr) { + return false + } + } + return true +} + +// HasSuffixFold is like strings.HasPrefix but uses Unicode case-folding. +func HasSuffixFold(s, suffix string) bool { + if suffix == "" { + return true + } + // count the runes and bytes in s, but only till rune count of suffix + bo, so := len(s), len(suffix) + for bo > 0 && so > 0 { + r, size := utf8.DecodeLastRuneInString(s[:bo]) + if r == utf8.RuneError { + return false + } + bo -= size + + sr, size := utf8.DecodeLastRuneInString(suffix[:so]) + if sr == utf8.RuneError { + return false + } + so -= size + + if !equalFoldRune(r, sr) { + return false + } + } + return so == 0 +} + +// ContainsFold is like strings.Contains but uses Unicode case-folding. +func ContainsFold(s, substr string) bool { + if substr == "" { + return true + } + if s == "" { + return false + } + firstRune := rune(substr[0]) + if firstRune >= utf8.RuneSelf { + firstRune, _ = utf8.DecodeRuneInString(substr) + } + for i, rune := range s { + if equalFoldRune(rune, firstRune) && HasPrefixFold(s[i:], substr) { + return true + } + } + return false +} + +// IsPlausibleJSON reports whether s likely contains a JSON object, without +// actually parsing it. It's meant to be a light heuristic. +func IsPlausibleJSON(s string) bool { + return startsWithOpenBrace(s) && endsWithCloseBrace(s) +} + +func isASCIIWhite(b byte) bool { return b == ' ' || b == '\n' || b == '\r' || b == '\t' } + +func startsWithOpenBrace(s string) bool { + for len(s) > 0 { + switch { + case s[0] == '{': + return true + case isASCIIWhite(s[0]): + s = s[1:] + default: + return false + } + } + return false +} + +func endsWithCloseBrace(s string) bool { + for len(s) > 0 { + last := len(s) - 1 + switch { + case s[last] == '}': + return true + case isASCIIWhite(s[last]): + s = s[:last] + default: + return false + } + } + return false +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/strutil/strutil_test.go b/vendor/github.com/camlistore/camlistore/pkg/strutil/strutil_test.go new file mode 100644 index 00000000..fa93ee95 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/strutil/strutil_test.go @@ -0,0 +1,230 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package strutil + +import ( + "reflect" + "strings" + "testing" +) + +func TestAppendSplitN(t *testing.T) { + var got []string + tests := []struct { + s, sep string + n int + }{ + {"foo", "|", 1}, + {"foo", "|", -1}, + {"foo|bar", "|", 1}, + {"foo|bar", "|", -1}, + {"foo|bar|", "|", 2}, + {"foo|bar|", "|", -1}, + {"foo|bar|baz", "|", 1}, + {"foo|bar|baz", "|", 2}, + {"foo|bar|baz", "|", 3}, + {"foo|bar|baz", "|", -1}, + } + for _, tt := range tests { + want := strings.SplitN(tt.s, tt.sep, tt.n) + got = AppendSplitN(got[:0], tt.s, tt.sep, tt.n) + if !reflect.DeepEqual(want, got) { + t.Errorf("AppendSplitN(%q, %q, %d) = %q; want %q", + tt.s, tt.sep, tt.n, got, want) + } + } +} + +func TestStringFromBytes(t *testing.T) { + for _, s := range []string{"foo", "permanode", "file", "zzzz"} { + got := StringFromBytes([]byte(s)) + if got != s { + t.Errorf("StringFromBytes(%q) didn't round-trip; got %q instead", s, got) + } + } +} + +func TestHasPrefixFold(t *testing.T) { + tests := []struct { + s, prefix string + result bool + }{ + {"camli", "CAML", true}, + {"CAMLI", "caml", true}, + {"cam", "Cam", true}, + {"camli", "car", false}, + {"caml", "camli", false}, + {"Hello, 世界 dasdsa", "HeLlO, 世界", true}, + {"Hello, 世界", "HeLlO, 世界-", false}, + + {"kelvin", "\u212A" + "elvin", true}, // "\u212A" is the Kelvin temperature sign + {"Kelvin", "\u212A" + "elvin", true}, + {"kelvin", "\u212A" + "el", true}, + {"Kelvin", "\u212A" + "el", true}, + {"\u212A" + "elvin", "Kelvin", true}, + {"\u212A" + "elvin", "kelvin", true}, + {"\u212A" + "elvin", "Kel", true}, + {"\u212A" + "elvin", "kel", true}, + } + for _, tt := range tests { + r := HasPrefixFold(tt.s, tt.prefix) + if r != tt.result { + t.Errorf("HasPrefixFold(%q, %q) returned %v", tt.s, tt.prefix, r) + } + } +} + +func TestHasSuffixFold(t *testing.T) { + tests := []struct { + s, suffix string + result bool + }{ + {"camli", "AMLI", true}, + {"CAMLI", "amli", true}, + {"mli", "MLI", true}, + {"camli", "ali", false}, + {"amli", "camli", false}, + {"asas Hello, 世界", "HeLlO, 世界", true}, + {"Hello, 世界", "HeLlO, 世界-", false}, + {"KkkkKKkelvin", "\u212A" + "elvin", true}, // "\u212A" is the Kelvin temperature sign + + {"kelvin", "\u212A" + "elvin", true}, // "\u212A" is the Kelvin temperature sign + {"Kelvin", "\u212A" + "elvin", true}, + {"\u212A" + "elvin", "Kelvin", true}, + {"\u212A" + "elvin", "kelvin", true}, + {"\u212A" + "elvin", "vin", true}, + {"\u212A" + "elvin", "viN", true}, + } + for _, tt := range tests { + r := HasSuffixFold(tt.s, tt.suffix) + if r != tt.result { + t.Errorf("HasSuffixFold(%q, %q) returned %v", tt.s, tt.suffix, r) + } + } +} + +func TestContainsFold(t *testing.T) { + // TODO: more tests, more languages. + tests := []struct { + s, substr string + result bool + }{ + {"camli", "CAML", true}, + {"CAMLI", "caml", true}, + {"cam", "Cam", true}, + {"мир", "ми", true}, + {"МИP", "ми", true}, + {"КАМЛИЙСТОР", "камлийс", true}, + {"КаМлИйСтОр", "КаМлИйС", true}, + {"camli", "car", false}, + {"caml", "camli", false}, + + {"camli", "AMLI", true}, + {"CAMLI", "amli", true}, + {"mli", "MLI", true}, + {"мир", "ир", true}, + {"МИP", "ми", true}, + {"КАМЛИЙСТОР", "лийстор", true}, + {"КаМлИйСтОр", "лИйСтОр", true}, + {"мир", "р", true}, + {"camli", "ali", false}, + {"amli", "camli", false}, + + {"МИP", "и", true}, + {"мир", "и", true}, + {"КАМЛИЙСТОР", "лийс", true}, + {"КаМлИйСтОр", "лИйС", true}, + + {"árvíztűrő tükörfúrógép", "árvíztŰrŐ", true}, + {"I love ☕", "i love ☕", true}, + + {"k", "\u212A", true}, // "\u212A" is the Kelvin temperature sign + {"\u212A" + "elvin", "k", true}, + {"kelvin", "\u212A" + "elvin", true}, + {"Kelvin", "\u212A" + "elvin", true}, + {"\u212A" + "elvin", "Kelvin", true}, + {"\u212A" + "elvin", "kelvin", true}, + {"273.15 kelvin", "\u212A" + "elvin", true}, + {"273.15 Kelvin", "\u212A" + "elvin", true}, + {"273.15 \u212A" + "elvin", "Kelvin", true}, + {"273.15 \u212A" + "elvin", "kelvin", true}, + } + for _, tt := range tests { + r := ContainsFold(tt.s, tt.substr) + if r != tt.result { + t.Errorf("ContainsFold(%q, %q) returned %v", tt.s, tt.substr, r) + } + } +} + +func TestIsPlausibleJSON(t *testing.T) { + tests := []struct { + in string + want bool + }{ + {"{}", true}, + {" {}", true}, + {"{} ", true}, + {"\n\r\t {}\t \r \n", true}, + + {"\n\r\t {x\t \r \n", false}, + {"{x", false}, + {"x}", false}, + {"x", false}, + {"", false}, + } + for _, tt := range tests { + got := IsPlausibleJSON(tt.in) + if got != tt.want { + t.Errorf("IsPlausibleJSON(%q) = %v; want %v", tt.in, got, tt.want) + } + } +} + +func BenchmarkHasSuffixFoldToLower(tb *testing.B) { + a, b := "camlik", "AMLI\u212A" + for i := 0; i < tb.N; i++ { + if !strings.HasSuffix(strings.ToLower(a), strings.ToLower(b)) { + tb.Fatalf("%q should have the same suffix as %q", a, b) + } + } +} +func BenchmarkHasSuffixFold(tb *testing.B) { + a, b := "camlik", "AMLI\u212A" + for i := 0; i < tb.N; i++ { + if !HasSuffixFold(a, b) { + tb.Fatalf("%q should have the same suffix as %q", a, b) + } + } +} + +func BenchmarkHasPrefixFoldToLower(tb *testing.B) { + a, b := "kamlistore", "\u212AAMLI" + for i := 0; i < tb.N; i++ { + if !strings.HasPrefix(strings.ToLower(a), strings.ToLower(b)) { + tb.Fatalf("%q should have the same suffix as %q", a, b) + } + } +} +func BenchmarkHasPrefixFold(tb *testing.B) { + a, b := "kamlistore", "\u212AAMLI" + for i := 0; i < tb.N; i++ { + if !HasPrefixFold(a, b) { + tb.Fatalf("%q should have the same suffix as %q", a, b) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/syncutil/gate.go b/vendor/github.com/camlistore/camlistore/pkg/syncutil/gate.go new file mode 100644 index 00000000..497c7a5a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/syncutil/gate.go @@ -0,0 +1,42 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package syncutil provides various concurrency mechanisms. +package syncutil + +// A Gate limits concurrency. +type Gate struct { + c chan struct{} +} + +// NewGate returns a new gate that will only permit max operations at once. +func NewGate(max int) *Gate { + return &Gate{make(chan struct{}, max)} +} + +// Start starts an operation, blocking until the gate has room. +func (g *Gate) Start() { + g.c <- struct{}{} +} + +// Done finishes an operation. +func (g *Gate) Done() { + select { + case <-g.c: + default: + panic("Done called more than Start") + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/syncutil/group.go b/vendor/github.com/camlistore/camlistore/pkg/syncutil/group.go new file mode 100644 index 00000000..dacef4c4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/syncutil/group.go @@ -0,0 +1,64 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package syncutil + +import "sync" + +// A Group is like a sync.WaitGroup and coordinates doing +// multiple things at once. Its zero value is ready to use. +type Group struct { + wg sync.WaitGroup + mu sync.Mutex // guards errs + errs []error +} + +// Go runs fn in its own goroutine, but does not wait for it to complete. +// Call Err or Errs to wait for all the goroutines to complete. +func (g *Group) Go(fn func() error) { + g.wg.Add(1) + go func() { + defer g.wg.Done() + err := fn() + if err != nil { + g.mu.Lock() + defer g.mu.Unlock() + g.errs = append(g.errs, err) + } + }() +} + +// Wait waits for all the previous calls to Go to complete. +func (g *Group) Wait() { + g.wg.Wait() +} + +// Err waits for all previous calls to Go to complete and returns the +// first non-nil error, or nil. +func (g *Group) Err() error { + g.wg.Wait() + if len(g.errs) > 0 { + return g.errs[0] + } + return nil +} + +// Errs waits for all previous calls to Go to complete and returns +// all non-nil errors. +func (g *Group) Errs() []error { + g.wg.Wait() + return g.errs +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/syncutil/lock.go b/vendor/github.com/camlistore/camlistore/pkg/syncutil/lock.go new file mode 100644 index 00000000..52de8e48 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/syncutil/lock.go @@ -0,0 +1,191 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package syncutil + +import ( + "bytes" + "fmt" + "log" + "runtime" + "sync" + "sync/atomic" + "time" + + "camlistore.org/pkg/strutil" +) + +// RWMutexTracker is a sync.RWMutex that tracks who owns the current +// exclusive lock. It's used for debugging deadlocks. +type RWMutexTracker struct { + mu sync.RWMutex + + // Atomic counters for number waiting and having read and write locks. + nwaitr int32 + nwaitw int32 + nhaver int32 + nhavew int32 // should always be 0 or 1 + + logOnce sync.Once + + hmu sync.Mutex + holder []byte + holdr map[int64]bool // goroutines holding read lock +} + +const stackBufSize = 16 << 20 + +var stackBuf = make(chan []byte, 8) + +func getBuf() []byte { + select { + case b := <-stackBuf: + return b[:stackBufSize] + default: + return make([]byte, stackBufSize) + } +} + +func putBuf(b []byte) { + select { + case stackBuf <- b: + default: + } +} + +var goroutineSpace = []byte("goroutine ") + +func GoroutineID() int64 { + b := getBuf() + defer putBuf(b) + b = b[:runtime.Stack(b, false)] + // Parse the 4707 otu of "goroutine 4707 [" + b = bytes.TrimPrefix(b, goroutineSpace) + i := bytes.IndexByte(b, ' ') + if i < 0 { + panic(fmt.Sprintf("No space found in %q", b)) + } + b = b[:i] + n, err := strutil.ParseUintBytes(b, 10, 64) + if err != nil { + panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err)) + } + return int64(n) +} + +func (m *RWMutexTracker) startLogger() { + go func() { + var buf bytes.Buffer + for { + time.Sleep(1 * time.Second) + buf.Reset() + m.hmu.Lock() + for gid := range m.holdr { + fmt.Fprintf(&buf, " [%d]", gid) + } + m.hmu.Unlock() + log.Printf("Mutex %p: waitW %d haveW %d waitR %d haveR %d %s", + m, + atomic.LoadInt32(&m.nwaitw), + atomic.LoadInt32(&m.nhavew), + atomic.LoadInt32(&m.nwaitr), + atomic.LoadInt32(&m.nhaver), buf.Bytes()) + } + }() +} + +func (m *RWMutexTracker) Lock() { + m.logOnce.Do(m.startLogger) + atomic.AddInt32(&m.nwaitw, 1) + m.mu.Lock() + atomic.AddInt32(&m.nwaitw, -1) + atomic.AddInt32(&m.nhavew, 1) + + m.hmu.Lock() + defer m.hmu.Unlock() + if len(m.holder) == 0 { + m.holder = make([]byte, stackBufSize) + } + m.holder = m.holder[:runtime.Stack(m.holder[:stackBufSize], false)] + log.Printf("Lock at %s", string(m.holder)) +} + +func (m *RWMutexTracker) Unlock() { + m.hmu.Lock() + m.holder = nil + m.hmu.Unlock() + + atomic.AddInt32(&m.nhavew, -1) + m.mu.Unlock() +} + +func (m *RWMutexTracker) RLock() { + m.logOnce.Do(m.startLogger) + atomic.AddInt32(&m.nwaitr, 1) + + // Catch read-write-read lock. See if somebody (us? via + // another goroutine?) already has a read lock, and then + // somebody else is waiting to write, meaning our second read + // will deadlock. + if atomic.LoadInt32(&m.nhaver) > 0 && atomic.LoadInt32(&m.nwaitw) > 0 { + buf := getBuf() + buf = buf[:runtime.Stack(buf, false)] + log.Printf("Potential R-W-R deadlock at: %s", buf) + putBuf(buf) + } + + m.mu.RLock() + atomic.AddInt32(&m.nwaitr, -1) + atomic.AddInt32(&m.nhaver, 1) + + gid := GoroutineID() + m.hmu.Lock() + defer m.hmu.Unlock() + if m.holdr == nil { + m.holdr = make(map[int64]bool) + } + if m.holdr[gid] { + buf := getBuf() + buf = buf[:runtime.Stack(buf, false)] + log.Fatalf("Recursive call to RLock: %s", buf) + } + m.holdr[gid] = true +} + +func stack() []byte { + buf := make([]byte, 1024) + return buf[:runtime.Stack(buf, false)] +} + +func (m *RWMutexTracker) RUnlock() { + atomic.AddInt32(&m.nhaver, -1) + + gid := GoroutineID() + m.hmu.Lock() + delete(m.holdr, gid) + m.hmu.Unlock() + + m.mu.RUnlock() +} + +// Holder returns the stack trace of the current exclusive lock holder's stack +// when it acquired the lock (with Lock). It returns the empty string if the lock +// is not currently held. +func (m *RWMutexTracker) Holder() string { + m.hmu.Lock() + defer m.hmu.Unlock() + return string(m.holder) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/syncutil/once.go b/vendor/github.com/camlistore/camlistore/pkg/syncutil/once.go new file mode 100644 index 00000000..1123f092 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/syncutil/once.go @@ -0,0 +1,60 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package syncutil + +import ( + "sync" + "sync/atomic" +) + +// A Once will perform a successful action exactly once. +// +// Unlike a sync.Once, this Once's func returns an error +// and is re-armed on failure. +type Once struct { + m sync.Mutex + done uint32 +} + +// Do calls the function f if and only if Do has not been invoked +// without error for this instance of Once. In other words, given +// var once Once +// if once.Do(f) is called multiple times, only the first call will +// invoke f, even if f has a different value in each invocation unless +// f returns an error. A new instance of Once is required for each +// function to execute. +// +// Do is intended for initialization that must be run exactly once. Since f +// is niladic, it may be necessary to use a function literal to capture the +// arguments to a function to be invoked by Do: +// err := config.once.Do(func() error { return config.init(filename) }) +func (o *Once) Do(f func() error) error { + if atomic.LoadUint32(&o.done) == 1 { + return nil + } + // Slow-path. + o.m.Lock() + defer o.m.Unlock() + var err error + if o.done == 0 { + err = f() + if err == nil { + atomic.StoreUint32(&o.done, 1) + } + } + return err +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/syncutil/once_test.go b/vendor/github.com/camlistore/camlistore/pkg/syncutil/once_test.go new file mode 100644 index 00000000..e321d509 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/syncutil/once_test.go @@ -0,0 +1,57 @@ +package syncutil + +import ( + "errors" + "testing" +) + +func TestOnce(t *testing.T) { + timesRan := 0 + f := func() error { + timesRan++ + return nil + } + + once := Once{} + grp := Group{} + + for i := 0; i < 10; i++ { + grp.Go(func() error { return once.Do(f) }) + } + + if grp.Err() != nil { + t.Errorf("Expected no errors, got %v", grp.Err()) + } + + if timesRan != 1 { + t.Errorf("Expected to run one time, ran %d", timesRan) + } +} + +// TestOnceErroring verifies we retry on every error, but stop after +// the first success. +func TestOnceErroring(t *testing.T) { + timesRan := 0 + f := func() error { + timesRan++ + if timesRan < 3 { + return errors.New("retry") + } + return nil + } + + once := Once{} + grp := Group{} + + for i := 0; i < 10; i++ { + grp.Go(func() error { return once.Do(f) }) + } + + if len(grp.Errs()) != 2 { + t.Errorf("Expected two errors, got %d", len(grp.Errs())) + } + + if timesRan != 3 { + t.Errorf("Expected to run two times, ran %d", timesRan) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/syncutil/sem.go b/vendor/github.com/camlistore/camlistore/pkg/syncutil/sem.go new file mode 100644 index 00000000..092655ff --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/syncutil/sem.go @@ -0,0 +1,64 @@ +package syncutil + +import ( + "fmt" + "log" + "sync" +) + +type debugT bool + +var debug = debugT(false) + +func (d debugT) Printf(format string, args ...interface{}) { + if bool(d) { + log.Printf(format, args...) + } +} + +// Sem implements a semaphore that can have multiple units acquired/released +// at a time. +type Sem struct { + c *sync.Cond // Protects size + max, free int64 +} + +// NewSem creates a semaphore with max units available for acquisition. +func NewSem(max int64) *Sem { + return &Sem{ + c: sync.NewCond(new(sync.Mutex)), + free: max, + max: max, + } +} + +// Acquire will deduct n units from the semaphore. If the deduction would +// result in the available units falling below zero, the call will block until +// another go routine returns units via a call to Release. If more units are +// requested than the semaphore is configured to hold, error will be non-nil. +func (s *Sem) Acquire(n int64) error { + if n > s.max { + return fmt.Errorf("sem: attempt to acquire more units than semaphore size %d > %d", n, s.max) + } + s.c.L.Lock() + defer s.c.L.Unlock() + for { + debug.Printf("Acquire check max %d free %d, n %d", s.max, s.free, n) + if s.free >= n { + s.free -= n + return nil + } + debug.Printf("Acquire Wait max %d free %d, n %d", s.max, s.free, n) + s.c.Wait() + } +} + +// Release will return n units to the semaphore and notify any currently +// blocking Acquire calls. +func (s *Sem) Release(n int64) { + s.c.L.Lock() + defer s.c.L.Unlock() + debug.Printf("Release max %d free %d, n %d", s.max, s.free, n) + s.free += n + s.c.Broadcast() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/syncutil/sem_test.go b/vendor/github.com/camlistore/camlistore/pkg/syncutil/sem_test.go new file mode 100644 index 00000000..f6981afe --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/syncutil/sem_test.go @@ -0,0 +1,33 @@ +package syncutil_test + +import ( + "testing" + + "camlistore.org/pkg/syncutil" +) + +func TestSem(t *testing.T) { + s := syncutil.NewSem(5) + + if err := s.Acquire(2); err != nil { + t.Fatal(err) + } + if err := s.Acquire(2); err != nil { + t.Fatal(err) + } + + go func() { + s.Release(2) + s.Release(2) + }() + if err := s.Acquire(5); err != nil { + t.Fatal(err) + } +} + +func TestSemErr(t *testing.T) { + s := syncutil.NewSem(5) + if err := s.Acquire(6); err == nil { + t.Fatal("Didn't get expected error for large acquire.") + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/syncutil/syncutil_test.go b/vendor/github.com/camlistore/camlistore/pkg/syncutil/syncutil_test.go new file mode 100644 index 00000000..99332c74 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/syncutil/syncutil_test.go @@ -0,0 +1,30 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package syncutil + +import "testing" + +func TestGoroutineID(t *testing.T) { + c := make(chan int64, 2) + c <- GoroutineID() + go func() { + c <- GoroutineID() + }() + if a, b := <-c, <-c; a == b { + t.Errorf("both goroutine IDs were %d; expected different", a) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/asserts/asserts.go b/vendor/github.com/camlistore/camlistore/pkg/test/asserts/asserts.go new file mode 100644 index 00000000..d2546ce3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/asserts/asserts.go @@ -0,0 +1,108 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package asserts provides a bad implementation of test predicate +// helpers. This package should either go away or dramatically +// improve. +package asserts + +import ( + "strings" + "testing" +) + +// NOTE: THESE FUNCTIONS ARE DEPRECATED. PLEASE DO NOT USE THEM IN +// NEW CODE. + +func Expect(t *testing.T, got bool, what string) { + if !got { + t.Errorf("%s: got %v; expected %v", what, got, true) + } +} + +func Assert(t *testing.T, got bool, what string) { + if !got { + t.Fatalf("%s: got %v; expected %v", what, got, true) + } +} + +func ExpectErrorContains(t *testing.T, err error, substr, msg string) { + errorContains((*testing.T).Errorf, t, err, substr, msg) +} + +func AssertErrorContains(t *testing.T, err error, substr, msg string) { + errorContains((*testing.T).Fatalf, t, err, substr, msg) +} + +func errorContains(f func(*testing.T, string, ...interface{}), t *testing.T, err error, substr, msg string) { + if err == nil { + f(t, "%s: got nil error; expected error containing %q", msg, substr) + return + } + if !strings.Contains(err.Error(), substr) { + f(t, "%s: expected error containing %q; got instead error %q", msg, substr, err.Error()) + } +} + +func ExpectString(t *testing.T, expect, got string, what string) { + if expect != got { + t.Errorf("%s: got %q; expected %q", what, got, expect) + } +} + +func AssertString(t *testing.T, expect, got string, what string) { + if expect != got { + t.Fatalf("%s: got %q; expected %q", what, got, expect) + } +} + +func ExpectBool(t *testing.T, expect, got bool, what string) { + if expect != got { + t.Errorf("%s: got %v; expected %v", what, got, expect) + } +} + +func AssertBool(t *testing.T, expect, got bool, what string) { + if expect != got { + t.Fatalf("%s: got %v; expected %v", what, got, expect) + } +} + +func ExpectInt(t *testing.T, expect, got int, what string) { + if expect != got { + t.Errorf("%s: got %d; expected %d", what, got, expect) + } +} + +func AssertInt(t *testing.T, expect, got int, what string) { + if expect != got { + t.Fatalf("%s: got %d; expected %d", what, got, expect) + } +} + +func ExpectNil(t *testing.T, v interface{}, what string) { + if v == nil { + return + } + t.Errorf("%s: expected nil; got %v", what, v) +} + +func AssertNil(t *testing.T, v interface{}, what string) { + if v == nil { + return + } + t.Fatalf("%s: expected nil; got %v", what, v) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/blob.go b/vendor/github.com/camlistore/camlistore/pkg/test/blob.go new file mode 100644 index 00000000..9844d5de --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/blob.go @@ -0,0 +1,93 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "crypto/sha1" + "fmt" + "io" + "io/ioutil" + "strings" + "testing" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/types" +) + +// Blob is a utility class for unit tests. +type Blob struct { + Contents string // the contents of the blob +} + +func (tb *Blob) Blob() *blob.Blob { + s := tb.Contents + return blob.NewBlob(tb.BlobRef(), tb.Size(), func() types.ReadSeekCloser { + return struct { + io.ReadSeeker + io.Closer + }{ + io.NewSectionReader(strings.NewReader(s), 0, int64(len(s))), + ioutil.NopCloser(nil), + } + }) +} + +func (tb *Blob) BlobRef() blob.Ref { + h := sha1.New() + h.Write([]byte(tb.Contents)) + return blob.RefFromHash(h) +} + +func (tb *Blob) SizedRef() blob.SizedRef { + return blob.SizedRef{tb.BlobRef(), tb.Size()} +} + +func (tb *Blob) BlobRefSlice() []blob.Ref { + return []blob.Ref{tb.BlobRef()} +} + +func (tb *Blob) Size() uint32 { + // Check that it's not larger than a uint32 (possible with + // 64-bit ints). But while we're here, be more paranoid and + // check for over the default max blob size of 16 MB. + if len(tb.Contents) > 16<<20 { + panic(fmt.Sprintf("test blob of %d bytes is larger than max 16MB allowed in testing", len(tb.Contents))) + } + return uint32(len(tb.Contents)) +} + +func (tb *Blob) Reader() io.Reader { + return strings.NewReader(tb.Contents) +} + +func (tb *Blob) AssertMatches(t *testing.T, sb blob.SizedRef) { + if sb.Size != tb.Size() { + t.Fatalf("Got size %d; expected %d", sb.Size, tb.Size()) + } + if sb.Ref != tb.BlobRef() { + t.Fatalf("Got blob %q; expected %q", sb.Ref.String(), tb.BlobRef()) + } +} + +func (tb *Blob) MustUpload(t *testing.T, ds blobserver.BlobReceiver) { + sb, err := ds.ReceiveBlob(tb.BlobRef(), tb.Reader()) + if err != nil { + t.Fatalf("failed to upload blob %v (%q): %v", tb.BlobRef(), tb.Contents, err) + } + tb.AssertMatches(t, sb) // TODO: better error reporting +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/diff.go b/vendor/github.com/camlistore/camlistore/pkg/test/diff.go new file mode 100644 index 00000000..ccc5d208 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/diff.go @@ -0,0 +1,52 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" +) + +// Diff returns the unified diff (from running "diff -u") or +// returns an error string. +func Diff(a, b []byte) string { + if bytes.Equal(a, b) { + return "" + } + ta, err := ioutil.TempFile("", "") + if err != nil { + return err.Error() + } + tb, err := ioutil.TempFile("", "") + if err != nil { + return err.Error() + } + defer os.Remove(ta.Name()) + defer os.Remove(tb.Name()) + // Lqzy... + ta.Write(a) + tb.Write(b) + ta.Close() + tb.Close() + out, err := exec.Command("diff", "-u", ta.Name(), tb.Name()).CombinedOutput() + if err != nil && len(out) == 0 { + return err.Error() + } + return string(out) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/doc.go b/vendor/github.com/camlistore/camlistore/pkg/test/doc.go new file mode 100644 index 00000000..44a721a1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package test provides common Camlistore test objects. +package test diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/dockertest/docker.go b/vendor/github.com/camlistore/camlistore/pkg/test/dockertest/docker.go new file mode 100644 index 00000000..5137f063 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/dockertest/docker.go @@ -0,0 +1,275 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package dockertest contains helper functions for setting up and tearing down docker containers to aid in testing. +*/ +package dockertest + +import ( + "bytes" + "database/sql" + "encoding/json" + "errors" + "fmt" + "log" + "os/exec" + "strings" + "testing" + "time" + + "camlistore.org/pkg/netutil" +) + +// Debug, if set, prevents any container from being removed. +var Debug bool + +/// runLongTest checks all the conditions for running a docker container +// based on image. +func runLongTest(t *testing.T, image string) { + if testing.Short() { + t.Skip("skipping in short mode") + } + if !haveDocker() { + t.Skip("skipping test; 'docker' command not found") + } + if ok, err := haveImage(image); !ok || err != nil { + if err != nil { + t.Skipf("Error running docker to check for %s: %v", image, err) + } + log.Printf("Pulling docker image %s ...", image) + if err := Pull(image); err != nil { + t.Skipf("Error pulling %s: %v", image, err) + } + } +} + +// haveDocker returns whether the "docker" command was found. +func haveDocker() bool { + _, err := exec.LookPath("docker") + return err == nil +} + +func haveImage(name string) (ok bool, err error) { + out, err := exec.Command("docker", "images", "--no-trunc").Output() + if err != nil { + return + } + return bytes.Contains(out, []byte(name)), nil +} + +func run(args ...string) (containerID string, err error) { + cmd := exec.Command("docker", append([]string{"run"}, args...)...) + var stdout, stderr bytes.Buffer + cmd.Stdout, cmd.Stderr = &stdout, &stderr + if err = cmd.Run(); err != nil { + err = fmt.Errorf("%v%v", stderr.String(), err) + return + } + containerID = strings.TrimSpace(stdout.String()) + if containerID == "" { + return "", errors.New("unexpected empty output from `docker run`") + } + return +} + +func KillContainer(container string) error { + return exec.Command("docker", "kill", container).Run() +} + +// Pull retrieves the docker image with 'docker pull'. +func Pull(image string) error { + out, err := exec.Command("docker", "pull", image).CombinedOutput() + if err != nil { + err = fmt.Errorf("%v: %s", err, out) + } + return err +} + +// IP returns the IP address of the container. +func IP(containerID string) (string, error) { + out, err := exec.Command("docker", "inspect", containerID).Output() + if err != nil { + return "", err + } + type networkSettings struct { + IPAddress string + } + type container struct { + NetworkSettings networkSettings + } + var c []container + if err := json.NewDecoder(bytes.NewReader(out)).Decode(&c); err != nil { + return "", err + } + if len(c) == 0 { + return "", errors.New("no output from docker inspect") + } + if ip := c[0].NetworkSettings.IPAddress; ip != "" { + return ip, nil + } + return "", errors.New("could not find an IP. Not running?") +} + +type ContainerID string + +func (c ContainerID) IP() (string, error) { + return IP(string(c)) +} + +func (c ContainerID) Kill() error { + return KillContainer(string(c)) +} + +// Remove runs "docker rm" on the container +func (c ContainerID) Remove() error { + if Debug { + return nil + } + return exec.Command("docker", "rm", "-v", string(c)).Run() +} + +// KillRemove calls Kill on the container, and then Remove if there was +// no error. It logs any error to t. +func (c ContainerID) KillRemove(t *testing.T) { + if err := c.Kill(); err != nil { + t.Log(err) + return + } + if err := c.Remove(); err != nil { + t.Log(err) + } +} + +// lookup retrieves the ip address of the container, and tries to reach +// before timeout the tcp address at this ip and given port. +func (c ContainerID) lookup(port int, timeout time.Duration) (ip string, err error) { + ip, err = c.IP() + if err != nil { + err = fmt.Errorf("error getting IP: %v", err) + return + } + addr := fmt.Sprintf("%s:%d", ip, port) + err = netutil.AwaitReachable(addr, timeout) + return +} + +// setupContainer sets up a container, using the start function to run the given image. +// It also looks up the IP address of the container, and tests this address with the given +// port and timeout. It returns the container ID and its IP address, or makes the test +// fail on error. +func setupContainer(t *testing.T, image string, port int, timeout time.Duration, + start func() (string, error)) (c ContainerID, ip string) { + runLongTest(t, image) + + containerID, err := start() + if err != nil { + t.Fatalf("docker run: %v", err) + } + c = ContainerID(containerID) + ip, err = c.lookup(port, timeout) + if err != nil { + c.KillRemove(t) + t.Skipf("Skipping test for container %v: %v", c, err) + } + return +} + +const ( + mongoImage = "mpl7/mongo" + // TODO(mpl): there's now an official mysql image at + // https://registry.hub.docker.com/_/mysql/ . We should either directly use one from + // there or fetch one there anyway to host it at + // https://console.developers.google.com/project/camlistore-website + mysqlImage = "orchardup/mysql" + MySQLUsername = "root" + MySQLPassword = "root" + postgresImage = "nornagon/postgres" + PostgresUsername = "docker" // set up by the dockerfile of postgresImage + PostgresPassword = "docker" // set up by the dockerfile of postgresImage +) + +// SetupMongoContainer sets up a real MongoDB instance for testing purposes, +// using a Docker container. It returns the container ID and its IP address, +// or makes the test fail on error. +// Currently using https://index.docker.io/u/robinvdvleuten/mongo/ +func SetupMongoContainer(t *testing.T) (c ContainerID, ip string) { + return setupContainer(t, mongoImage, 27017, 10*time.Second, func() (string, error) { + return run("-d", mongoImage, "--nojournal") + }) +} + +// SetupMySQLContainer sets up a real MySQL instance for testing purposes, +// using a Docker container. It returns the container ID and its IP address, +// or makes the test fail on error. +// Currently using https://index.docker.io/u/orchardup/mysql/ +func SetupMySQLContainer(t *testing.T, dbname string) (c ContainerID, ip string) { + return setupContainer(t, mysqlImage, 3306, 10*time.Second, func() (string, error) { + return run("-d", "-e", "MYSQL_ROOT_PASSWORD="+MySQLPassword, "-e", "MYSQL_DATABASE="+dbname, mysqlImage) + }) +} + +// SetupPostgreSQLContainer sets up a real PostgreSQL instance for testing purposes, +// using a Docker container. It returns the container ID and its IP address, +// or makes the test fail on error. +// Currently using https://index.docker.io/u/nornagon/postgres +func SetupPostgreSQLContainer(t *testing.T, dbname string) (c ContainerID, ip string) { + c, ip = setupContainer(t, postgresImage, 5432, 15*time.Second, func() (string, error) { + return run("-d", postgresImage) + }) + cleanupAndDie := func(err error) { + c.KillRemove(t) + t.Fatal(err) + } + rootdb, err := sql.Open("postgres", + fmt.Sprintf("user=%s password=%s host=%s dbname=postgres sslmode=disable", PostgresUsername, PostgresPassword, ip)) + if err != nil { + cleanupAndDie(fmt.Errorf("Could not open postgres rootdb: %v", err)) + } + if _, err := sqlExecRetry(rootdb, + "CREATE DATABASE "+dbname+" LC_COLLATE = 'C' TEMPLATE = template0", + 50); err != nil { + cleanupAndDie(fmt.Errorf("Could not create database %v: %v", dbname, err)) + } + return +} + +// sqlExecRetry keeps calling http://golang.org/pkg/database/sql/#DB.Exec on db +// with stmt until it succeeds or until it has been tried maxTry times. +// It sleeps in between tries, twice longer after each new try, starting with +// 100 milliseconds. +func sqlExecRetry(db *sql.DB, stmt string, maxTry int) (sql.Result, error) { + if maxTry <= 0 { + return nil, errors.New("did not try at all") + } + interval := 100 * time.Millisecond + try := 0 + var err error + var result sql.Result + for { + result, err = db.Exec(stmt) + if err == nil { + return result, nil + } + try++ + if try == maxTry { + break + } + time.Sleep(interval) + interval *= 2 + } + return result, fmt.Errorf("failed %v times: %v", try, err) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/fakeindex.go b/vendor/github.com/camlistore/camlistore/pkg/test/fakeindex.go new file mode 100644 index 00000000..53eb23ac --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/fakeindex.go @@ -0,0 +1,225 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "fmt" + "log" + "os" + "strings" + "sync" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/context" + "camlistore.org/pkg/types/camtypes" +) + +var ClockOrigin = time.Unix(1322443956, 123456) + +// A FakeIndex implements parts of search.Index and provides methods +// to controls the results, such as AddMeta, AddClaim, +// AddSignerAttrValue. +type FakeIndex struct { + lk sync.Mutex + meta map[blob.Ref]camtypes.BlobMeta + claims map[blob.Ref][]camtypes.Claim // permanode -> claims + signerAttrValue map[string]blob.Ref // "\0\0" -> blobref + path map[string]*camtypes.Path // "\0\0" -> path + + cllk sync.RWMutex + clock time.Time +} + +func NewFakeIndex() *FakeIndex { + return &FakeIndex{ + meta: make(map[blob.Ref]camtypes.BlobMeta), + claims: make(map[blob.Ref][]camtypes.Claim), + signerAttrValue: make(map[string]blob.Ref), + path: make(map[string]*camtypes.Path), + clock: ClockOrigin, + } +} + +// +// Test methods +// + +func (fi *FakeIndex) nextDate() time.Time { + fi.cllk.Lock() + defer fi.cllk.Unlock() + fi.clock = fi.clock.Add(1 * time.Second) + return fi.clock.UTC() +} + +func (fi *FakeIndex) LastTime() time.Time { + fi.cllk.RLock() + defer fi.cllk.RUnlock() + return fi.clock +} + +func camliTypeFromMime(mime string) string { + if v := strings.TrimPrefix(mime, "application/json; camliType="); v != mime { + return v + } + return "" +} + +func (fi *FakeIndex) AddMeta(br blob.Ref, camliType string, size uint32) { + fi.lk.Lock() + defer fi.lk.Unlock() + fi.meta[br] = camtypes.BlobMeta{ + Ref: br, + Size: size, + CamliType: camliType, + } +} + +func (fi *FakeIndex) AddClaim(owner, permanode blob.Ref, claimType, attr, value string) { + fi.lk.Lock() + defer fi.lk.Unlock() + date := fi.nextDate() + + claim := camtypes.Claim{ + Permanode: permanode, + Signer: owner, + BlobRef: blob.Ref{}, + Date: date, + Type: claimType, + Attr: attr, + Value: value, + } + fi.claims[permanode] = append(fi.claims[permanode], claim) + + if claimType == "set-attribute" && strings.HasPrefix(attr, "camliPath:") { + suffix := attr[len("camliPath:"):] + path := &camtypes.Path{ + Target: blob.MustParse(value), + Suffix: suffix, + } + fi.path[fmt.Sprintf("%s\x00%s\x00%s", owner, permanode, suffix)] = path + } +} + +func (fi *FakeIndex) AddSignerAttrValue(signer blob.Ref, attr, val string, latest blob.Ref) { + fi.lk.Lock() + defer fi.lk.Unlock() + fi.signerAttrValue[fmt.Sprintf("%s\x00%s\x00%s", signer, attr, val)] = latest +} + +// +// Interface implementation +// + +func (fi *FakeIndex) KeyId(blob.Ref) (string, error) { + panic("NOIMPL") +} + +func (fi *FakeIndex) GetRecentPermanodes(dest chan<- camtypes.RecentPermanode, owner blob.Ref, limit int, before time.Time) error { + panic("NOIMPL") +} + +// TODO(mpl): write real tests +func (fi *FakeIndex) SearchPermanodesWithAttr(dest chan<- blob.Ref, request *camtypes.PermanodeByAttrRequest) error { + panic("NOIMPL") +} + +func (fi *FakeIndex) AppendClaims(dst []camtypes.Claim, permaNode blob.Ref, + signerFilter blob.Ref, + attrFilter string) ([]camtypes.Claim, error) { + fi.lk.Lock() + defer fi.lk.Unlock() + + for _, cl := range fi.claims[permaNode] { + if signerFilter.Valid() && cl.Signer != signerFilter { + continue + } + if attrFilter != "" && cl.Attr != attrFilter { + continue + } + dst = append(dst, cl) + } + return dst, nil +} + +func (fi *FakeIndex) GetBlobMeta(br blob.Ref) (camtypes.BlobMeta, error) { + fi.lk.Lock() + defer fi.lk.Unlock() + bm, ok := fi.meta[br] + if !ok { + return camtypes.BlobMeta{}, os.ErrNotExist + } + return bm, nil +} + +func (fi *FakeIndex) ExistingFileSchemas(bytesRef blob.Ref) ([]blob.Ref, error) { + panic("NOIMPL") +} + +func (fi *FakeIndex) GetFileInfo(fileRef blob.Ref) (camtypes.FileInfo, error) { + panic("NOIMPL") +} + +func (fi *FakeIndex) GetImageInfo(fileRef blob.Ref) (camtypes.ImageInfo, error) { + panic("NOIMPL") +} + +func (fi *FakeIndex) GetMediaTags(fileRef blob.Ref) (tags map[string]string, err error) { + panic("NOIMPL") +} + +func (fi *FakeIndex) GetDirMembers(dir blob.Ref, dest chan<- blob.Ref, limit int) error { + panic("NOIMPL") +} + +func (fi *FakeIndex) PermanodeOfSignerAttrValue(signer blob.Ref, attr, val string) (blob.Ref, error) { + fi.lk.Lock() + defer fi.lk.Unlock() + if b, ok := fi.signerAttrValue[fmt.Sprintf("%s\x00%s\x00%s", signer, attr, val)]; ok { + return b, nil + } + return blob.Ref{}, os.ErrNotExist +} + +func (fi *FakeIndex) PathsOfSignerTarget(signer, target blob.Ref) ([]*camtypes.Path, error) { + panic("NOIMPL") +} + +func (fi *FakeIndex) PathsLookup(signer, base blob.Ref, suffix string) ([]*camtypes.Path, error) { + panic("NOIMPL") +} + +func (fi *FakeIndex) PathLookup(signer, base blob.Ref, suffix string, at time.Time) (*camtypes.Path, error) { + if !at.IsZero() { + panic("PathLookup with non-zero 'at' time not supported") + } + fi.lk.Lock() + defer fi.lk.Unlock() + if p, ok := fi.path[fmt.Sprintf("%s\x00%s\x00%s", signer, base, suffix)]; ok { + return p, nil + } + log.Printf("PathLookup miss for signer %q, base %q, suffix %q", signer, base, suffix) + return nil, os.ErrNotExist +} + +func (fi *FakeIndex) EdgesTo(ref blob.Ref, opts *camtypes.EdgesToOpts) ([]*camtypes.Edge, error) { + panic("NOIMPL") +} + +func (fi *FakeIndex) EnumerateBlobMeta(ctx *context.Context, ch chan<- camtypes.BlobMeta) error { + panic("NOIMPL") +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/fetcher.go b/vendor/github.com/camlistore/camlistore/pkg/test/fetcher.go new file mode 100644 index 00000000..4a5d1b44 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/fetcher.go @@ -0,0 +1,89 @@ +/* +Copyright 2011 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "io" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/blobserver/memory" +) + +// Fetcher is an in-memory implementation of the blobserver Storage +// interface. It started as just a fetcher and grew. It also includes +// other convenience methods for testing. +type Fetcher struct { + memory.Storage + + // ReceiveErr optionally returns the error to return on receive. + ReceiveErr error + + // FetchErr, if non-nil, specifies the error to return on the next fetch call. + // If it returns nil, fetches proceed as normal. + FetchErr func() error +} + +var ( + _ blobserver.Storage = (*Fetcher)(nil) + _ blobserver.BlobStreamer = (*Fetcher)(nil) +) + +func (tf *Fetcher) Fetch(ref blob.Ref) (file io.ReadCloser, size uint32, err error) { + if tf.FetchErr != nil { + if err = tf.FetchErr(); err != nil { + return + } + } + file, size, err = tf.Storage.Fetch(ref) + if err != nil { + return + } + return file, size, nil +} + +func (tf *Fetcher) SubFetch(ref blob.Ref, offset, length int64) (io.ReadCloser, error) { + if tf.FetchErr != nil { + if err := tf.FetchErr(); err != nil { + return nil, err + } + } + rc, err := tf.Storage.SubFetch(ref, offset, length) + if err != nil { + return rc, err + } + return rc, nil +} + +func (tf *Fetcher) ReceiveBlob(br blob.Ref, source io.Reader) (blob.SizedRef, error) { + sb, err := tf.Storage.ReceiveBlob(br, source) + if err != nil { + return sb, err + } + if err := tf.ReceiveErr; err != nil { + tf.RemoveBlobs([]blob.Ref{br}) + return sb, err + } + return sb, nil +} + +func (tf *Fetcher) AddBlob(b *Blob) { + _, err := tf.ReceiveBlob(b.BlobRef(), b.Reader()) + if err != nil { + panic(err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/fetcher_test.go b/vendor/github.com/camlistore/camlistore/pkg/test/fetcher_test.go new file mode 100644 index 00000000..3fef9bda --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/fetcher_test.go @@ -0,0 +1,31 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test_test + +import ( + "testing" + + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/blobserver/storagetest" + "camlistore.org/pkg/test" +) + +func TestFetcher(t *testing.T) { + storagetest.Test(t, func(t *testing.T) (sto blobserver.Storage, cleanup func()) { + return new(test.Fetcher), func() {} + }) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/integration/camget_test.go b/vendor/github.com/camlistore/camlistore/pkg/test/integration/camget_test.go new file mode 100644 index 00000000..fd2357f4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/integration/camget_test.go @@ -0,0 +1,215 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "camlistore.org/pkg/test" + "camlistore.org/pkg/test/asserts" +) + +// Test that `camget -o' can restore a symlink correctly. +func TestCamgetSymlink(t *testing.T) { + w := test.GetWorld(t) + + srcDir, err := ioutil.TempDir("", "camget-test-") + if err != nil { + t.Fatalf("ioutil.TempDir(): %v", err) + } + defer os.RemoveAll(srcDir) + + targetBase := "a" + target := filepath.Join(srcDir, targetBase) + targetFD, err := os.Create(target) + if err != nil { + t.Fatalf("os.Create(): %v", err) + } + targetFD.Close() + + subdirBase := "child" + subdirName := filepath.Join(srcDir, subdirBase) + linkBase := "b" + linkName := filepath.Join(subdirName, linkBase) + err = os.Mkdir(subdirName, 0777) + if err != nil { + t.Fatalf("os.Mkdir(): %v", err) + } + + err = os.Symlink("../"+targetBase, linkName) + if err != nil { + t.Fatalf("os.Symlink(): %v", err) + } + + out := test.MustRunCmd(t, w.Cmd("camput", "file", srcDir)) + // TODO(mpl): rm call and delete pkg. + asserts.ExpectBool(t, true, out != "", "camput") + br := strings.Split(out, "\n")[0] + dstDir, err := ioutil.TempDir("", "camget-test-") + if err != nil { + t.Fatalf("ioutil.TempDir(): %v", err) + } + defer os.RemoveAll(dstDir) + + // Now restore the symlink + _ = test.MustRunCmd(t, w.Cmd("camget", "-o", dstDir, br)) + + symlink := filepath.Join(dstDir, filepath.Base(srcDir), subdirBase, + linkBase) + link, err := os.Readlink(symlink) + if err != nil { + t.Fatalf("os.Readlink(): %v", err) + } + expected := "../a" + if expected != link { + t.Fatalf("os.Readlink(): Expected: %s, got %s", expected, + link) + } + + // Ensure that the link is not broken + _, err = os.Stat(symlink) + if err != nil { + t.Fatalf("os.Stat(): %v", err) + } +} + +// Test that `camget -o' can restore a fifo correctly. +func TestCamgetFIFO(t *testing.T) { + if runtime.GOOS == "windows" { + t.SkipNow() + } + + fifo, cleanup := mkTmpFIFO(t) + defer cleanup() + + // Upload the fifo + w := test.GetWorld(t) + out := test.MustRunCmd(t, w.Cmd("camput", "file", fifo)) + br := strings.Split(out, "\n")[0] + + // Try and get it back + tdir, err := ioutil.TempDir("", "fifo-test-") + if err != nil { + t.Fatalf("ioutil.TempDir(): %v", err) + } + defer os.RemoveAll(tdir) + test.MustRunCmd(t, w.Cmd("camget", "-o", tdir, br)) + + // Ensure it is actually a fifo + name := filepath.Join(tdir, filepath.Base(fifo)) + fi, err := os.Lstat(name) + if err != nil { + t.Fatalf("os.Lstat(): %v", err) + } + if mask := fi.Mode() & os.ModeNamedPipe; mask == 0 { + t.Fatalf("Retrieved file %s: Not a FIFO", name) + } +} + +// Test that `camget -o' can restore a socket correctly. +func TestCamgetSocket(t *testing.T) { + if runtime.GOOS == "windows" { + t.SkipNow() + } + + socket, cleanup := mkTmpSocket(t) + defer cleanup() + + // Upload the socket + w := test.GetWorld(t) + out := test.MustRunCmd(t, w.Cmd("camput", "file", socket)) + br := strings.Split(out, "\n")[0] + + // Try and get it back + tdir, err := ioutil.TempDir("", "socket-test-") + if err != nil { + t.Fatalf("ioutil.TempDir(): %v", err) + } + defer os.RemoveAll(tdir) + test.MustRunCmd(t, w.Cmd("camget", "-o", tdir, br)) + + // Ensure it is actually a socket + name := filepath.Join(tdir, filepath.Base(socket)) + fi, err := os.Lstat(name) + if err != nil { + t.Fatalf("os.Lstat(): %v", err) + } + if mask := fi.Mode() & os.ModeSocket; mask == 0 { + t.Fatalf("Retrieved file %s: Not a socket", name) + } +} + +// Test that: +// 1) `camget -contents' can restore a regular file correctly. +// 2) if the file already exists, and has the same size as the one held by the server, +// stop early and do not even fetch it from the server. +func TestCamgetFile(t *testing.T) { + dirName, err := ioutil.TempDir("", "camli-TestCamgetFile") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dirName) + f, err := os.Create(filepath.Join(dirName, "test.txt")) + if err != nil { + t.Fatal(err) + } + filename := f.Name() + contents := "not empty anymore" + if _, err := f.Write([]byte(contents)); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + outDir := filepath.Join(dirName, "fetched") + if err := os.Mkdir(outDir, 0700); err != nil { + t.Fatal(err) + } + + w := test.GetWorld(t) + out := test.MustRunCmd(t, w.Cmd("camput", "file", filename)) + + br := strings.Split(out, "\n")[0] + _ = test.MustRunCmd(t, w.Cmd("camget", "-o", outDir, "-contents", br)) + + fetchedName := filepath.Join(outDir, "test.txt") + b, err := ioutil.ReadFile(fetchedName) + if err != nil { + t.Fatal(err) + } + if string(b) != contents { + t.Fatalf("fetched file different from original file, got contents %q, wanted %q", b, contents) + } + + var stderr bytes.Buffer + c := w.Cmd("camget", "-o", outDir, "-contents", "-verbose", br) + c.Stderr = &stderr + if err := c.Run(); err != nil { + t.Fatalf("running second camget: %v", err) + } + if !strings.Contains(stderr.String(), fmt.Sprintf("Skipping %s; already exists.", fetchedName)) { + t.Fatal(errors.New("Was expecting info message about local file already existing")) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/integration/camlistore_test.go b/vendor/github.com/camlistore/camlistore/pkg/test/integration/camlistore_test.go new file mode 100644 index 00000000..5b9370b1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/integration/camlistore_test.go @@ -0,0 +1,242 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration + +import ( + "bufio" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "camlistore.org/pkg/test" + "camlistore.org/third_party/github.com/gorilla/websocket" +) + +// Test that running: +// $ camput permanode +// ... creates and uploads a permanode, and that we can camget it back. +func TestCamputPermanode(t *testing.T) { + w := test.GetWorld(t) + br := w.NewPermanode(t) + + out := test.MustRunCmd(t, w.Cmd("camget", br.String())) + mustHave := []string{ + `{"camliVersion": 1,`, + `"camliSigner": "`, + `"camliType": "permanode",`, + `random": "`, + `,"camliSig":"`, + } + for _, str := range mustHave { + if !strings.Contains(out, str) { + t.Errorf("Expected permanode response to contain %q; it didn't. Got: %s", str, out) + } + } +} + +func TestWebsocketQuery(t *testing.T) { + w := test.GetWorld(t) + pn := w.NewPermanode(t) + test.MustRunCmd(t, w.Cmd("camput", "attr", pn.String(), "tag", "foo")) + + check := func(err error) { + if err != nil { + t.Fatal(err) + } + } + + const bufSize = 1 << 20 + + c, err := net.Dial("tcp", w.Addr()) + if err != nil { + t.Fatalf("Dial: %v", err) + } + + wc, _, err := websocket.NewClient(c, &url.URL{Host: w.Addr(), Path: w.SearchHandlerPath() + "ws"}, nil, bufSize, bufSize) + check(err) + + msg, err := wc.NextWriter(websocket.TextMessage) + check(err) + + _, err = msg.Write([]byte(`{"tag": "foo", "query": { "expression": "tag:foo" }}`)) + check(err) + check(msg.Close()) + + errc := make(chan error, 1) + go func() { + inType, inMsg, err := wc.ReadMessage() + if err != nil { + errc <- err + return + } + if !strings.HasPrefix(string(inMsg), `{"tag":"_status"`) { + errc <- fmt.Errorf("unexpected message type=%d msg=%q, wanted status update", inType, inMsg) + return + } + inType, inMsg, err = wc.ReadMessage() + if err != nil { + errc <- err + return + } + if strings.Contains(string(inMsg), pn.String()) { + errc <- nil + return + } + errc <- fmt.Errorf("unexpected message type=%d msg=%q", inType, inMsg) + }() + select { + case err := <-errc: + if err != nil { + t.Error(err) + } + case <-time.After(5 * time.Second): + t.Error("timeout") + } +} + +func TestInternalHandler(t *testing.T) { + w := test.GetWorld(t) + tests := map[string]int{ + "/no-http-storage/": 401, + "/no-http-handler/": 401, + "/good-status/": 200, + "/bs-and-maybe-also-index/camli": 400, + "/bs/camli/sha1-b2201302e129a4396a323cb56283cddeef11bbe8": 404, + "/no-http-storage/camli/sha1-b2201302e129a4396a323cb56283cddeef11bbe8": 401, + } + for suffix, want := range tests { + res, err := http.Get(w.ServerBaseURL() + suffix) + if err != nil { + t.Fatalf("On %s: %v", suffix, err) + } + if res.StatusCode != want { + t.Errorf("For %s: Status = %d; want %d", suffix, res.StatusCode, want) + } + res.Body.Close() + } +} + +func mustTempDir(t *testing.T) (name string, cleanup func()) { + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + return dir, func() { os.RemoveAll(dir) } +} + +func mustWriteFile(t *testing.T, path, contents string) { + err := ioutil.WriteFile(path, []byte(contents), 0644) + if err != nil { + t.Fatal(err) + } +} + +// Run camput in the environment it runs in under the Android app. +// This matches how camput is used in UploadThread.java. +func TestAndroidCamputFile(t *testing.T) { + w := test.GetWorld(t) + // UploadThread.java sets: + // CAMLI_AUTH (set by w.CmdWithEnv) + // CAMLI_TRUSTED_CERT (not needed) + // CAMLI_CACHE_DIR + // CAMPUT_ANDROID_OUTPUT=1 + cacheDir, clean := mustTempDir(t) + defer clean() + env := []string{ + "CAMPUT_ANDROID_OUTPUT=1", + "CAMLI_CACHE_DIR=" + cacheDir, + } + cmd := w.CmdWithEnv("camput", + env, + "--server="+w.ServerBaseURL(), + "file", + "-stdinargs", + "-vivify") + cmd.Stderr = os.Stderr + in, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + out, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := w.Ping(); err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + defer cmd.Process.Kill() + + srcDir, clean := mustTempDir(t) + defer clean() + + file1 := filepath.Join(srcDir, "file1.txt") + mustWriteFile(t, file1, "contents 1") + file2 := filepath.Join(srcDir, "file2.txt") + mustWriteFile(t, file2, "contents 2 longer length") + + go func() { + fmt.Fprintf(in, "%s\n", file1) + fmt.Fprintf(in, "%s\n", file2) + }() + + waitc := make(chan error) + go func() { + sc := bufio.NewScanner(out) + fileUploaded := 0 + for sc.Scan() { + t.Logf("Got: %q", sc.Text()) + f := strings.Fields(sc.Text()) + if len(f) == 0 { + t.Logf("empty text?") + continue + } + if f[0] == "FILE_UPLOADED" { + fileUploaded++ + if fileUploaded == 2 { + break + } + } + } + in.Close() + if err := sc.Err(); err != nil { + t.Error(err) + } + }() + + defer cmd.Process.Kill() + go func() { + waitc <- cmd.Wait() + }() + select { + case <-time.After(5 * time.Second): + t.Fatal("timeout waiting for camput to end") + case err := <-waitc: + if err != nil { + t.Errorf("camput exited uncleanly: %v", err) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/integration/camput_test.go b/vendor/github.com/camlistore/camlistore/pkg/test/integration/camput_test.go new file mode 100644 index 00000000..a60e1733 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/integration/camput_test.go @@ -0,0 +1,105 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration + +import ( + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/test" +) + +// mkTmpFIFO makes a fifo in a temporary directory and returns the +// path it and a function to clean-up when done. +func mkTmpFIFO(t *testing.T) (path string, cleanup func()) { + tdir, err := ioutil.TempDir("", "fifo-test-") + if err != nil { + t.Fatalf("iouti.TempDir(): %v", err) + } + cleanup = func() { + os.RemoveAll(tdir) + } + + path = filepath.Join(tdir, "fifo") + err = osutil.Mkfifo(path, 0660) + if err != nil { + t.Fatalf("osutil.mkfifo(): %v", err) + } + + return +} + +// Test that `camput' can upload fifos correctly. +func TestCamputFIFO(t *testing.T) { + if runtime.GOOS == "windows" { + t.SkipNow() + } + + fifo, cleanup := mkTmpFIFO(t) + defer cleanup() + + // Can we successfully upload a fifo? + w := test.GetWorld(t) + out := test.MustRunCmd(t, w.Cmd("camput", "file", fifo)) + + br := strings.Split(out, "\n")[0] + out = test.MustRunCmd(t, w.Cmd("camget", br)) + t.Logf("Retrieved stored fifo schema: %s", out) +} + +// mkTmpSocket makes a socket in a temporary directory and returns the +// path to it and a function to clean-up when done. +func mkTmpSocket(t *testing.T) (path string, cleanup func()) { + tdir, err := ioutil.TempDir("", "socket-test-") + if err != nil { + t.Fatalf("iouti.TempDir(): %v", err) + } + cleanup = func() { + os.RemoveAll(tdir) + } + + path = filepath.Join(tdir, "socket") + err = osutil.Mksocket(path) + if err != nil { + t.Fatalf("osutil.Mksocket(): %v", err) + } + + return +} + +// Test that `camput' can upload sockets correctly. +func TestCamputSocket(t *testing.T) { + if runtime.GOOS == "windows" { + t.SkipNow() + } + + socket, cleanup := mkTmpSocket(t) + defer cleanup() + + // Can we successfully upload a socket? + w := test.GetWorld(t) + out := test.MustRunCmd(t, w.Cmd("camput", "file", socket)) + + br := strings.Split(out, "\n")[0] + out = test.MustRunCmd(t, w.Cmd("camget", br)) + t.Logf("Retrieved stored socket schema: %s", out) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/integration/diskpacked_test.go b/vendor/github.com/camlistore/camlistore/pkg/test/integration/diskpacked_test.go new file mode 100644 index 00000000..d6846ac6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/integration/diskpacked_test.go @@ -0,0 +1,101 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration + +import ( + "bufio" + "math/rand" + "os" + "path/filepath" + "testing" + + "camlistore.org/pkg/test" +) + +var ( + testFileRel = filepath.Join("pkg", "test", "integration", "100M.dat") + testFileSize = 100 * 1024 * 1024 +) + +func BenchmarkLocal(b *testing.B) { + benchmarkWrite(b, "bench-localdisk-server-config.json") +} + +func BenchmarkDiskpacked(b *testing.B) { + benchmarkWrite(b, "bench-diskpacked-server-config.json") +} + +func benchmarkWrite(b *testing.B, cfg string) { + w, err := test.WorldFromConfig(cfg) + if err != nil { + b.Fatalf("could not create server for config: %v\nError: %v", cfg, err) + } + testFile := filepath.Join(w.CamliSourceRoot(), testFileRel) + createTestFile(b, testFile, testFileSize) + defer os.Remove(testFile) + b.ResetTimer() + b.StopTimer() + for i := 0; i < b.N; i++ { + err = w.Start() + if err != nil { + b.Fatalf("could not start server for config: %v\nError: %v", cfg, err) + } + b.StartTimer() + test.MustRunCmd(b, w.Cmd("camput", "file", testFile)) + b.StopTimer() + w.Stop() + } + + b.SetBytes(int64(testFileSize)) +} + +func createTestFile(tb testing.TB, file string, n int) { + f, err := os.Create(file) + if err != nil { + tb.Fatal(err) + } + w := bufio.NewWriter(f) + tot := 0 + var b [8]byte + for tot < n { + c := rand.Int63() + b = [8]byte{ + byte(c), + byte(c >> 8), + byte(c >> 16), + byte(c >> 24), + byte(c >> 32), + byte(c >> 40), + byte(c >> 48), + byte(c >> 56), + } + wn, err := w.Write(b[:]) + if err != nil { + tb.Fatal(err) + } + if wn < len(b) { + tb.Fatalf("short write, got %d expected %d", wn, len(b)) + } + tot += wn + } + if err := w.Flush(); err != nil { + tb.Fatal(err) + } + if err := f.Close(); err != nil { + tb.Fatal(err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/integration/integration.go b/vendor/github.com/camlistore/camlistore/pkg/test/integration/integration.go new file mode 100644 index 00000000..a4dd88ba --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/integration/integration.go @@ -0,0 +1,3 @@ +package integration + +// Dummy stub file. Required as of Go tip (pre-Go 1.3)? diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/integration/non-utf8_test.go b/vendor/github.com/camlistore/camlistore/pkg/test/integration/non-utf8_test.go new file mode 100644 index 00000000..5e1b80c1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/integration/non-utf8_test.go @@ -0,0 +1,121 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration + +import ( + "bytes" + "encoding/hex" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "camlistore.org/pkg/test" +) + +var nonUTF8 = "416c697ae965202d204d6f69204c6f6c6974612e6d7033" // hex-encoding + +func tempDir(t *testing.T) (path string, cleanup func()) { + path, err := ioutil.TempDir("", "camtest-") + if err != nil { + t.Fatalf("ioutil.TempDir(): %v", err) + } + + cleanup = func() { + os.RemoveAll(path) + } + + return +} + +// Test that we can camput and camget a file whose name is not utf8, +// that we don't panic in the process and that the results are +// correct. +func TestNonUTF8FileName(t *testing.T) { + srcDir, cleanup := tempDir(t) + defer cleanup() + + base, err := hex.DecodeString(nonUTF8) + if err != nil { + t.Fatalf("hex.DecodeString(): %v", err) + } + + fd, err := os.Create(filepath.Join(srcDir, string(base))) + if err != nil { + t.Fatalf("os.Create(): %v", err) + } + fd.Close() + + w := test.GetWorld(t) + out := test.MustRunCmd(t, w.Cmd("camput", "file", fd.Name())) + br := strings.Split(out, "\n")[0] + + // camput was a success. Can we get the file back in another directory? + dstDir, cleanup := tempDir(t) + defer cleanup() + + _ = test.MustRunCmd(t, w.Cmd("camget", "-o", dstDir, br)) + _, err = os.Lstat(filepath.Join(dstDir, string(base))) + if err != nil { + t.Fatalf("Failed to stat file %s in directory %s", + fd.Name(), dstDir) + } +} + +// Test that we can camput and camget a symbolic link whose target is +// not utf8, that we do no panic in the process and that the results +// are correct. +func TestNonUTF8SymlinkTarget(t *testing.T) { + srcDir, cleanup := tempDir(t) + defer cleanup() + + base, err := hex.DecodeString(nonUTF8) + if err != nil { + t.Fatalf("hex.DecodeString(): %v", err) + } + + fd, err := os.Create(filepath.Join(srcDir, string(base))) + if err != nil { + t.Fatalf("os.Create(): %v", err) + } + defer fd.Close() + + err = os.Symlink(string(base), filepath.Join(srcDir, "link")) + if err != nil { + t.Fatalf("os.Symlink(): %v", err) + } + + w := test.GetWorld(t) + out := test.MustRunCmd(t, w.Cmd("camput", "file", filepath.Join(srcDir, "link"))) + br := strings.Split(out, "\n")[0] + + // See if we can camget it back correctly + dstDir, cleanup := tempDir(t) + defer cleanup() + + _ = test.MustRunCmd(t, w.Cmd("camget", "-o", dstDir, br)) + target, err := os.Readlink(filepath.Join(dstDir, "link")) + if err != nil { + t.Fatalf("os.Readlink(): %v", err) + } + + if !bytes.Equal([]byte(target), base) { + t.Fatalf("Retrieved symlink contains points to unexpected target") + } + +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/integration/share_test.go b/vendor/github.com/camlistore/camlistore/pkg/test/integration/share_test.go new file mode 100644 index 00000000..06c38a66 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/integration/share_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "camlistore.org/pkg/test" +) + +func TestFileSharing(t *testing.T) { + share(t, "share_test.go") +} + +func TestDirSharing(t *testing.T) { + share(t, filepath.FromSlash("../integration")) +} + +func share(t *testing.T, file string) { + w := test.GetWorld(t) + out := test.MustRunCmd(t, w.Cmd("camput", "file", file)) + fileRef := strings.Split(out, "\n")[0] + + out = test.MustRunCmd(t, w.Cmd("camput", "share", "-transitive", fileRef)) + shareRef := strings.Split(out, "\n")[0] + + testDir, err := ioutil.TempDir("", "camli-share-test-") + if err != nil { + t.Fatalf("ioutil.TempDir(): %v", err) + } + defer os.RemoveAll(testDir) + + // test that we can get it through the share + test.MustRunCmd(t, w.Cmd("camget", "-o", testDir, "-shared", fmt.Sprintf("%v/share/%v", w.ServerBaseURL(), shareRef))) + filePath := filepath.Join(testDir, filepath.Base(file)) + fi, err := os.Stat(filePath) + if err != nil { + t.Fatalf("camget -shared failed to get %v: %v", file, err) + } + if fi.IsDir() { + // test that we also get the dir contents + d, err := os.Open(filePath) + if err != nil { + t.Fatal(err) + } + defer d.Close() + names, err := d.Readdirnames(-1) + if err != nil { + t.Fatal(err) + } + if len(names) == 0 { + t.Fatalf("camget did not fetch contents of directory %v", file) + } + } + + // test that we're not allowed to get it directly + fileURL := fmt.Sprintf("%v/share/%v", w.ServerBaseURL(), fileRef) + _, err = test.RunCmd(w.Cmd("camget", "-shared", fileURL)) + if err == nil { + t.Fatal("Was expecting error for 'camget -shared " + fileURL + "'") + } + if !strings.Contains(err.Error(), "client: got status code 401") { + t.Fatalf("'camget -shared %v': got error %v, was expecting 401", fileURL, err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/integration/z_test.go b/vendor/github.com/camlistore/camlistore/pkg/test/integration/z_test.go new file mode 100644 index 00000000..0f657d3d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/integration/z_test.go @@ -0,0 +1,32 @@ +/* +Copyright 2013 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration + +import ( + "testing" + + "camlistore.org/pkg/test" +) + +// Make sure that the camlistored process started +// by the World gets terminated when all the tests +// are done. +// This works only as long as TestZLastTest is the +// last test to run in the package. +func TestZLastTest(t *testing.T) { + test.GetWorldMaybe(t).Stop() +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/loader.go b/vendor/github.com/camlistore/camlistore/pkg/test/loader.go new file mode 100644 index 00000000..1415b8af --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/loader.go @@ -0,0 +1,100 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "errors" + "log" + "strings" + "sync" + + "camlistore.org/pkg/blobserver" +) + +// NewLoader +func NewLoader() *Loader { + return &Loader{} +} + +type Loader struct { + mu sync.Mutex + sto map[string]blobserver.Storage +} + +var _ blobserver.Loader = (*Loader)(nil) + +func (ld *Loader) FindHandlerByType(handlerType string) (prefix string, handler interface{}, err error) { + panic("NOIMPL") +} + +func (ld *Loader) AllHandlers() (map[string]string, map[string]interface{}) { + panic("NOIMPL") +} + +func (ld *Loader) MyPrefix() string { + return "/lies/" +} + +func (ld *Loader) BaseURL() string { + return "http://localhost:1234" +} + +func (ld *Loader) GetHandlerType(prefix string) string { + log.Printf("test.Loader: GetHandlerType called but not implemented.") + return "" +} + +func (ld *Loader) GetHandler(prefix string) (interface{}, error) { + log.Printf("test.Loader: GetHandler called but not implemented.") + return nil, errors.New("doesn't exist") +} + +func (ld *Loader) SetStorage(prefix string, s blobserver.Storage) { + ld.mu.Lock() + defer ld.mu.Unlock() + if ld.sto == nil { + ld.sto = make(map[string]blobserver.Storage) + } + ld.sto[prefix] = s +} + +func (ld *Loader) GetStorage(prefix string) (blobserver.Storage, error) { + ld.mu.Lock() + defer ld.mu.Unlock() + if bs, ok := ld.sto[prefix]; ok { + return bs, nil + } + if ld.sto == nil { + ld.sto = make(map[string]blobserver.Storage) + } + sto, err := ld.genStorage(prefix) + if err != nil { + return nil, err + } + ld.sto[prefix] = sto + return sto, nil +} + +func (ld *Loader) genStorage(prefix string) (blobserver.Storage, error) { + if strings.HasPrefix(prefix, "/good") { + return &Fetcher{}, nil + } + if strings.HasPrefix(prefix, "/fail") { + return &Fetcher{ReceiveErr: errors.New("test.Loader intentional failure for /fail storage handler")}, nil + } + panic("test.Loader.GetStorage: unrecognized prefix type") +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/test.go b/vendor/github.com/camlistore/camlistore/pkg/test/test.go new file mode 100644 index 00000000..19e4ff8b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/test.go @@ -0,0 +1,70 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "log" + "os" + "strconv" + "strings" + "testing" +) + +// BrokenTest marks the test as broken and calls t.Skip, unless the environment +// variable RUN_BROKEN_TESTS is set to 1 (or some other boolean true value). +func BrokenTest(t *testing.T) { + if v, _ := strconv.ParseBool(os.Getenv("RUN_BROKEN_TESTS")); !v { + t.Skipf("Skipping broken tests without RUN_BROKEN_TESTS=1") + } +} + +// TLog changes the log package's output to log to t and returns a function +// to reset it back to stderr. +func TLog(t testing.TB) func() { + log.SetOutput(twriter{t: t}) + return func() { + log.SetOutput(os.Stderr) + } +} + +type twriter struct { + t testing.TB + quietPhrases []string +} + +func (w twriter) Write(p []byte) (n int, err error) { + if len(w.quietPhrases) > 0 { + s := string(p) + for _, phrase := range w.quietPhrases { + if strings.Contains(s, phrase) { + return len(p), nil + } + } + } + if w.t != nil { + w.t.Log(strings.TrimSuffix(string(p), "\n")) + } + return len(p), nil +} + +// NewLogger returns a logger that logs to t with the given prefix. +// +// The optional quietPhrases are substrings to match in writes to +// determine whether those log messages are muted. +func NewLogger(t *testing.T, prefix string, quietPhrases ...string) *log.Logger { + return log.New(twriter{t: t, quietPhrases: quietPhrases}, prefix, log.LstdFlags) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/test_test.go b/vendor/github.com/camlistore/camlistore/pkg/test/test_test.go new file mode 100644 index 00000000..169f6330 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/test_test.go @@ -0,0 +1,56 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test_test + +import ( + "log" + "reflect" + "testing" + + "camlistore.org/pkg/index" + . "camlistore.org/pkg/test" +) + +var _ index.Interface = (*FakeIndex)(nil) + +type tbLogger struct { + testing.TB + log []string +} + +func (l *tbLogger) Log(args ...interface{}) { + l.log = append(l.log, args[0].(string)) +} + +func TestTLog(t *testing.T) { + tb := new(tbLogger) + defer TLog(tb)() + defer log.SetFlags(log.Flags()) + log.SetFlags(0) + + log.Printf("hello") + log.Printf("hello\n") + log.Printf("some text\nand more text\n") + want := []string{ + "hello", + "hello", + "some text\nand more text", + } + if !reflect.DeepEqual(tb.log, want) { + t.Errorf("Got %q; want %q", tb.log, want) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/testdata/bench-diskpacked-server-config.json b/vendor/github.com/camlistore/camlistore/pkg/test/testdata/bench-diskpacked-server-config.json new file mode 100644 index 00000000..7a26fe00 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/testdata/bench-diskpacked-server-config.json @@ -0,0 +1,20 @@ +{ + "handlerConfig": true, + "auth": "userpass:testuser:passTestWorld:+localhost", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/bs/": { + "handler": "storage-diskpacked", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT}"] + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/testdata/bench-localdisk-server-config.json b/vendor/github.com/camlistore/camlistore/pkg/test/testdata/bench-localdisk-server-config.json new file mode 100644 index 00000000..e3d45c32 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/testdata/bench-localdisk-server-config.json @@ -0,0 +1,20 @@ +{ + "handlerConfig": true, + "auth": "userpass:testuser:passTestWorld:+localhost", + "https": false, + "listen": "localhost:3179", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "blobRoot": "/bs/" + } + }, + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT}"] + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/testdata/server-config.json b/vendor/github.com/camlistore/camlistore/pkg/test/testdata/server-config.json new file mode 100644 index 00000000..9ab2ea9b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/testdata/server-config.json @@ -0,0 +1,90 @@ +{ "_for-emacs": "-*- mode: js2;-*-", + "handlerConfig": true, + "https": false, + "baseURL": ["_env", "${CAMLI_BASE_URL}"], + "auth": "userpass:testuser:passTestWorld:+localhost", + "prefixes": { + "/": { + "handler": "root", + "handlerArgs": { + "ownerName": "test", + "blobRoot": "/bs-and-maybe-also-index/", + "helpRoot": "/help/", + "statusRoot": "/status/", + "searchRoot": "/my-search/", + "stealth": false + } + }, + + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": ["/bs/", "/index-mem/"] + } + }, + + "/no-http-storage/": { + "internal": true, + "handler": "storage-replica", + "handlerArgs": { + "backends": ["/bs/"] + } + }, + + "/no-http-handler/": { + "internal": true, + "handler": "status" + }, + + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "write": { + "if": "isSchema", + "then": "/bs-and-index/", + "else": "/bs/" + }, + "read": "/bs/" + } + }, + + "/bs/": { + "handler": "storage-filesystem", + "handlerArgs": { + "path": ["_env", "${CAMLI_ROOT}"] + } + }, + + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "secretRing": ["_env", "${CAMLI_SECRET_RING}"], + "keyId": "26F5ABDA", + "publicKeyDest": "/bs/" + } + }, + + "/index-mem/": { + "handler": "storage-memory-only-dev-indexer", + "handlerArgs": { + "blobSource": "/bs/" + } + }, + + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/index-mem/", + "slurpToMemory": true, + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4" + } + }, + + "/share/": { + "handler": "share", + "handlerArgs": { + "blobRoot": "/bs/" + } + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/testdep.go b/vendor/github.com/camlistore/camlistore/pkg/test/testdep.go new file mode 100644 index 00000000..7e693231 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/testdep.go @@ -0,0 +1,34 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "os" + "strconv" + "testing" +) + +// DependencyErrorOrSkip is called when a test's dependency +// isn't found. It either skips the current test (if SKIP_DEP_TESTS is set), +// or calls t.Error with an error. +func DependencyErrorOrSkip(t *testing.T) { + b, _ := strconv.ParseBool(os.Getenv("SKIP_DEP_TESTS")) + if b { + t.Skip("SKIP_DEP_TESTS is set; skipping test.") + } + t.Error("External test dependencies not found, and environment SKIP_DEP_TESTS not set.") +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/wait.go b/vendor/github.com/camlistore/camlistore/pkg/test/wait.go new file mode 100644 index 00000000..a26faa30 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/wait.go @@ -0,0 +1,33 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import "time" + +// WaitFor returns true if condition returns true before maxWait. +// It is checked immediately, and then every checkInterval. +func WaitFor(condition func() bool, maxWait, checkInterval time.Duration) bool { + t0 := time.Now() + tmax := t0.Add(maxWait) + for time.Now().Before(tmax) { + if condition() { + return true + } + time.Sleep(checkInterval) + } + return false +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/test/world.go b/vendor/github.com/camlistore/camlistore/pkg/test/world.go new file mode 100644 index 00000000..b3c4b79f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/test/world.go @@ -0,0 +1,377 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync/atomic" + "testing" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/osutil" +) + +// World defines an integration test world. +// +// It's used to run the actual Camlistore binaries (camlistored, +// camput, camget, camtool, etc) together in large tests, including +// building them, finding them, and wiring them up in an isolated way. +type World struct { + camRoot string // typically $GOPATH[0]/src/camlistore.org + config string // server config file relative to pkg/test/testdata + tempDir string + listener net.Listener // randomly chosen 127.0.0.1 port for the server + port int + + server *exec.Cmd + isRunning int32 // state of the camlistored server. Access with sync/atomic only. + serverErr error + + cammount *os.Process +} + +// CamliSourceRoot returns the root of the source tree, or an error. +func camliSourceRoot() (string, error) { + if os.Getenv("GOPATH") == "" { + return "", errors.New("GOPATH environment variable isn't set; required to run Camlistore integration tests") + } + root, err := osutil.GoPackagePath("camlistore.org") + if err == os.ErrNotExist { + return "", errors.New("Directory \"camlistore.org\" not found under GOPATH/src; can't run Camlistore integration tests.") + } + return root, nil +} + +// NewWorld returns a new test world. +// It requires that GOPATH is set to find the "camlistore.org" root. +func NewWorld() (*World, error) { + return WorldFromConfig("server-config.json") +} + +// WorldFromConfig returns a new test world based on the given configuration file. +// This cfg is the server config relative to pkg/test/testdata. +// It requires that GOPATH is set to find the "camlistore.org" root. +func WorldFromConfig(cfg string) (*World, error) { + root, err := camliSourceRoot() + if err != nil { + return nil, err + } + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + + return &World{ + camRoot: root, + config: cfg, + listener: ln, + port: ln.Addr().(*net.TCPAddr).Port, + }, nil +} + +func (w *World) Addr() string { + return w.listener.Addr().String() +} + +// CamliSourceRoot returns the root of the source tree. +func (w *World) CamliSourceRoot() string { + return w.camRoot +} + +// Start builds the Camlistore binaries and starts a server. +func (w *World) Start() error { + var err error + w.tempDir, err = ioutil.TempDir("", "camlistore-test-") + if err != nil { + return err + } + // Build. + { + targs := []string{ + "camget", + "camput", + "camtool", + "camlistored", + } + // TODO(mpl): investigate why we still rebuild camlistored everytime if run through devcam test. + // it looks like it's because we always resync the UI files and hence redo the embeds. Next CL. + var latestModtime time.Time + for _, target := range targs { + binPath := filepath.Join(w.camRoot, "bin", target) + fi, err := os.Stat(binPath) + if err != nil { + if !os.IsNotExist(err) { + return fmt.Errorf("could not stat %v: %v", binPath, err) + } + } else { + modTime := fi.ModTime() + if modTime.After(latestModtime) { + latestModtime = modTime + } + } + } + cmd := exec.Command("go", "run", "make.go", + fmt.Sprintf("--if_mods_since=%d", latestModtime.Unix()), + ) + if testing.Verbose() { + // TODO(mpl): do the same when -verbose with devcam test. Even better: see if testing.Verbose + // can be made true if devcam test -verbose ? + cmd.Args = append(cmd.Args, "-v=true") + } + cmd.Dir = w.camRoot + log.Print("Running make.go to build camlistore binaries for testing...") + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("Error building world: %v, %s", err, string(out)) + } + if testing.Verbose() { + log.Printf("%s\n", out) + } + log.Print("Ran make.go.") + } + // Start camlistored. + { + w.server = exec.Command( + filepath.Join(w.camRoot, "bin", "camlistored"), + "--openbrowser=false", + "--configfile="+filepath.Join(w.camRoot, "pkg", "test", "testdata", w.config), + "--listen=FD:3", + "--pollparent=true", + ) + var buf bytes.Buffer + if testing.Verbose() { + w.server.Stdout = os.Stdout + w.server.Stderr = os.Stderr + } else { + w.server.Stdout = &buf + w.server.Stderr = &buf + } + w.server.Dir = w.tempDir + w.server.Env = append(os.Environ(), + "CAMLI_DEBUG=1", + "CAMLI_ROOT="+w.tempDir, + "CAMLI_SECRET_RING="+filepath.Join(w.camRoot, filepath.FromSlash("pkg/jsonsign/testdata/test-secring.gpg")), + "CAMLI_BASE_URL=http://127.0.0.1:"+strconv.Itoa(w.port), + ) + listenerFD, err := w.listener.(*net.TCPListener).File() + if err != nil { + return err + } + w.server.ExtraFiles = []*os.File{listenerFD} + if err := w.server.Start(); err != nil { + w.serverErr = fmt.Errorf("starting camlistored: %v", err) + return w.serverErr + } + + atomic.StoreInt32(&w.isRunning, 1) + waitc := make(chan error, 1) + go func() { + err := w.server.Wait() + w.serverErr = fmt.Errorf("%v: %s", err, buf.String()) + atomic.StoreInt32(&w.isRunning, 0) + waitc <- w.serverErr + }() + upc := make(chan bool) + timeoutc := make(chan bool) + go func() { + for i := 0; i < 100; i++ { + res, err := http.Get("http://127.0.0.1:" + strconv.Itoa(w.port)) + if err == nil { + res.Body.Close() + upc <- true + return + } + time.Sleep(50 * time.Millisecond) + } + w.serverErr = errors.New(buf.String()) + atomic.StoreInt32(&w.isRunning, 0) + timeoutc <- true + }() + + select { + case <-waitc: + return fmt.Errorf("server exited: %v", w.serverErr) + case <-timeoutc: + return fmt.Errorf("server never became reachable: %v", w.serverErr) + case <-upc: + if err := w.Ping(); err != nil { + return err + } + // Success. + } + } + return nil +} + +// Ping returns an error if the world's camlistored is not running. +func (w *World) Ping() error { + if atomic.LoadInt32(&w.isRunning) != 1 { + return fmt.Errorf("camlistored not running: %v", w.serverErr) + } + return nil +} + +func (w *World) Stop() { + if w == nil { + return + } + if err := w.server.Process.Kill(); err != nil { + log.Fatalf("killed failed: %v", err) + } + + if d := w.tempDir; d != "" { + os.RemoveAll(d) + } +} + +func (w *World) NewPermanode(t *testing.T) blob.Ref { + if err := w.Ping(); err != nil { + t.Fatal(err) + } + out := MustRunCmd(t, w.Cmd("camput", "permanode")) + br, ok := blob.Parse(strings.TrimSpace(out)) + if !ok { + t.Fatalf("Expected permanode in camput stdout; got %q", out) + } + return br +} + +func (w *World) Cmd(binary string, args ...string) *exec.Cmd { + return w.CmdWithEnv(binary, os.Environ(), args...) +} + +func (w *World) CmdWithEnv(binary string, env []string, args ...string) *exec.Cmd { + hasVerbose := func() bool { + for _, v := range args { + if v == "-verbose" || v == "--verbose" { + return true + } + } + return false + } + var cmd *exec.Cmd + switch binary { + case "camget", "camput", "camtool", "cammount": + // TODO(mpl): lift the camput restriction when we have a unified logging mechanism + if binary == "camput" && !hasVerbose() { + // camput and camtool are the only ones to have a -verbose flag through cmdmain + // but camtool is never used. (and cammount does not even have a -verbose). + args = append([]string{"-verbose"}, args...) + } + cmd = exec.Command(filepath.Join(w.camRoot, "bin", binary), args...) + clientConfigDir := filepath.Join(w.camRoot, "config", "dev-client-dir") + cmd.Env = append([]string{ + "CAMLI_CONFIG_DIR=" + clientConfigDir, + // Respected by env expansions in config/dev-client-dir/client-config.json: + "CAMLI_SERVER=" + w.ServerBaseURL(), + "CAMLI_SECRET_RING=" + w.SecretRingFile(), + "CAMLI_KEYID=" + w.ClientIdentity(), + "CAMLI_AUTH=userpass:testuser:passTestWorld", + }, env...) + default: + panic("Unknown binary " + binary) + } + return cmd +} + +func (w *World) ServerBaseURL() string { + return fmt.Sprintf("http://127.0.0.1:%d", w.port) +} + +var theWorld *World + +// GetWorld returns (creating if necessary) a test singleton world. +// It calls Fatal on the provided test if there are problems. +func GetWorld(t *testing.T) *World { + w := theWorld + if w == nil { + var err error + w, err = NewWorld() + if err != nil { + t.Fatalf("Error finding test world: %v", err) + } + err = w.Start() + if err != nil { + t.Fatalf("Error starting test world: %v", err) + } + theWorld = w + } + return w +} + +// GetWorldMaybe returns the current World. It might be nil. +func GetWorldMaybe(t *testing.T) *World { + return theWorld +} + +// RunCmd runs c (which is assumed to be something short-lived, like a +// camput or camget command), capturing its stdout for return, and +// also capturing its stderr, just in the case of errors. +// If there's an error, the return error fully describes the command and +// all output. +func RunCmd(c *exec.Cmd) (output string, err error) { + var stdout, stderr bytes.Buffer + if testing.Verbose() { + c.Stderr = io.MultiWriter(os.Stderr, &stderr) + c.Stdout = io.MultiWriter(os.Stdout, &stdout) + } else { + c.Stderr = &stderr + c.Stdout = &stdout + } + err = c.Run() + if err != nil { + return "", fmt.Errorf("Error running command %+v: Stdout:\n%s\nStderr:\n%s\n", c, stdout.String(), stderr.String()) + } + return stdout.String(), nil +} + +// MustRunCmd wraps RunCmd, failing t if RunCmd returns an error. +func MustRunCmd(t testing.TB, c *exec.Cmd) string { + out, err := RunCmd(c) + if err != nil { + t.Fatal(err) + } + return out +} + +// ClientIdentity returns the GPG identity to use in World tests, suitable +// for setting in CAMLI_KEYID. +func (w *World) ClientIdentity() string { + return "26F5ABDA" +} + +// SecretRingFile returns the GnuPG secret ring, suitable for setting +// in CAMLI_SECRET_RING. +func (w *World) SecretRingFile() string { + return filepath.Join(w.camRoot, "pkg", "jsonsign", "testdata", "test-secring.gpg") +} + +// SearchHandlerPath returns the path to the search handler, with trailing slash. +func (w *World) SearchHandlerPath() string { return "/my-search/" } diff --git a/vendor/github.com/camlistore/camlistore/pkg/throttle/throttle.go b/vendor/github.com/camlistore/camlistore/pkg/throttle/throttle.go new file mode 100644 index 00000000..5c697234 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/throttle/throttle.go @@ -0,0 +1,137 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package throttle provides a net.Listener that returns +// artificially-delayed connections for testing real-world +// connectivity. +package throttle + +import ( + "fmt" + "net" + "sync" + "time" +) + +const unitSize = 1400 // read/write chunk size. ~MTU size. + +type Rate struct { + KBps int // or 0, to not rate-limit bandwidth + Latency time.Duration +} + +// byteTime returns the time required for n bytes. +func (r Rate) byteTime(n int) time.Duration { + if r.KBps == 0 { + return 0 + } + return time.Duration(float64(n)/1024/float64(r.KBps)) * time.Second +} + +type Listener struct { + net.Listener + Down Rate // server Writes to Client + Up Rate // server Reads from client +} + +func (ln *Listener) Accept() (net.Conn, error) { + c, err := ln.Listener.Accept() + time.Sleep(ln.Up.Latency) + if err != nil { + return nil, err + } + tc := &conn{Conn: c, Down: ln.Down, Up: ln.Up} + tc.start() + return tc, nil +} + +type nErr struct { + n int + err error +} + +type writeReq struct { + writeAt time.Time + p []byte + resc chan nErr +} + +type conn struct { + net.Conn + Down Rate // for reads + Up Rate // for writes + + wchan chan writeReq + closeOnce sync.Once + closeErr error +} + +func (c *conn) start() { + c.wchan = make(chan writeReq, 1024) + go c.writeLoop() +} + +func (c *conn) writeLoop() { + for req := range c.wchan { + time.Sleep(req.writeAt.Sub(time.Now())) + var res nErr + for len(req.p) > 0 && res.err == nil { + writep := req.p + if len(writep) > unitSize { + writep = writep[:unitSize] + } + n, err := c.Conn.Write(writep) + time.Sleep(c.Up.byteTime(len(writep))) + res.n += n + res.err = err + req.p = req.p[n:] + } + req.resc <- res + } +} + +func (c *conn) Close() error { + c.closeOnce.Do(func() { + err := c.Conn.Close() + close(c.wchan) + c.closeErr = err + }) + return c.closeErr +} + +func (c *conn) Write(p []byte) (n int, err error) { + defer func() { + if e := recover(); e != nil { + n = 0 + err = fmt.Errorf("%v", err) + return + } + }() + resc := make(chan nErr, 1) + c.wchan <- writeReq{time.Now().Add(c.Up.Latency), p, resc} + res := <-resc + return res.n, res.err +} + +func (c *conn) Read(p []byte) (n int, err error) { + const max = 1024 + if len(p) > max { + p = p[:max] + } + n, err = c.Conn.Read(p) + time.Sleep(c.Down.byteTime(n)) + return +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/atomics.go b/vendor/github.com/camlistore/camlistore/pkg/types/atomics.go new file mode 100644 index 00000000..27669527 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/atomics.go @@ -0,0 +1,55 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +import ( + "sync/atomic" +) + +// AtomicBool is an atomic boolean. +// It can be accessed from concurrent goroutines. +type AtomicBool struct { + v uint32 // 0 or 1, atomically +} + +func (b *AtomicBool) Get() bool { + return atomic.LoadUint32(&b.v) != 0 +} + +func (b *AtomicBool) Set(v bool) { + if v { + atomic.StoreUint32(&b.v, 1) + return + } + atomic.StoreUint32(&b.v, 0) +} + +type AtomicInt64 struct { + v int64 +} + +func (a *AtomicInt64) Get() int64 { + return atomic.LoadInt64(&a.v) +} + +func (a *AtomicInt64) Set(v int64) { + atomic.StoreInt64(&a.v, v) +} + +func (a *AtomicInt64) Add(delta int64) int64 { + return atomic.AddInt64(&a.v, delta) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/camtypes.go b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/camtypes.go new file mode 100644 index 00000000..438a9930 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/camtypes.go @@ -0,0 +1,20 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package camtypes is like the types package, but higher-level and contains +// Camlistore-specific types. It exists mostly to break circular dependencies +// between index, search, and schema. +package camtypes diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/discovery.go b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/discovery.go new file mode 100644 index 00000000..471b25e3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/discovery.go @@ -0,0 +1,103 @@ +/* +Copyright 2015 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package camtypes + +import ( + "camlistore.org/pkg/blob" + "camlistore.org/pkg/types" +) + +// Discovery is the JSON response for discovery requests. +type Discovery struct { + BlobRoot string `json:"blobRoot"` + JSONSignRoot string `json:"jsonSignRoot"` + HelpRoot string `json:"helpRoot"` + ImporterRoot string `json:"importerRoot"` + SearchRoot string `json:"searchRoot"` + StatusRoot string `json:"statusRoot"` + + OwnerName string `json:"ownerName"` // Name of the owner. + UserName string `json:"userName"` // Name of the user. + + // StorageGeneration is the UUID for the storage generation. + StorageGeneration string `json:"storageGeneration,omitempty"` + // StorageGenerationError is the error that occurred on generating the storage, if any. + StorageGenerationError string `json:"storageGenerationError,omitempty"` + // StorageInitTime is the initialization time of the storage. + StorageInitTime types.Time3339 `json:"storageInitTime,omitempty"` + + ThumbVersion string `json:"thumbVersion"` // Thumbnailing version. + WSAuthToken string `json:"wsAuthToken"` // Authentication token for the WebSocket. + + // SyncHandlers lists discovery information about the available sync handlers. + SyncHandlers []SyncHandlerDiscovery `json:"syncHanlders,omitempty"` + // Signing contains discovery information for signing. + Signing *SignDiscovery `json:"signing,omitempty"` + // UIDiscovery contains discovery information for the UI. + *UIDiscovery +} + +// SignDiscovery contains discovery information for jsonsign. +// It is part of the server's JSON response for discovery requests. +type SignDiscovery struct { + // PublicKey is the path to the public signing key. + PublicKey string `json:"publicKey,omitempty"` + // PublicKeyBlobRef is the blob.Ref for the public key. + PublicKeyBlobRef blob.Ref `json:"publicKeyBlobRef,omitempty"` + // PublicKeyID is the ID of the public key. + PublicKeyID string `json:"publicKeyId"` + // SignHandler is the URL path prefix to the signing handler. + SignHandler string `json:"signHandler"` + // VerifyHandler it the URL path prefix to the signature verification handler. + VerifyHandler string `json:"verifyHandler"` +} + +// SyncHandlerDiscovery contains discovery information about a sync handler. +// It is part of the JSON response to discovery requests. +type SyncHandlerDiscovery struct { + // From is the source of the sync handler. + From string `json:"from"` + // To is the destination of the sync handler. + To string `json:"to"` + // ToIndex is true if the sync is from a blob storage to an index. + ToIndex bool `json:"toIndex"` +} + +// UIDiscovery contains discovery information for the user interface. +// It is part of the JSON response to discovery requests. +type UIDiscovery struct { + // UIRoot is the URL prefix path to the UI handler. + UIRoot string `json:"uiRoot"` + // UploadHelper is the path to the upload helper. + UploadHelper string `json:"uploadHelper"` + // DirectoryHelper is the path to the directory helper. + DirectoryHelper string `json:"directoryHelper"` + // DownloaderHelper is the path to the downloader helper. + DownloadHelper string `json:"downloadHelper"` + // PublishRoots lists discovery information for all publishing roots, + // mapped by the respective root name. + PublishRoots map[string]*PublishRootDiscovery `json:"publishRoots"` +} + +// PublishRootDiscovery contains discovery information for the publish roots. +type PublishRootDiscovery struct { + Name string `json:"name"` + // Prefix lists prefixes belonging to the publishing root. + Prefix []string `json:"prefix"` + // CurrentPermanode is the permanode associated with the publishing root. + CurrentPermanode blob.Ref `json:"currentPermanode"` +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/errors.go b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/errors.go new file mode 100644 index 00000000..0e96fab8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/errors.go @@ -0,0 +1,86 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package camtypes + +import ( + "fmt" + "log" + + "camlistore.org/pkg/osutil" +) + +// TODO(mpl): move pkg/camerrors stuff in here + +var camErrors = map[string]*camErr{} + +var ( + ErrClientNoServer = addCamError("client-no-server", funcStr(func() string { + return fmt.Sprintf("No valid server defined. It can be set with the CAMLI_SERVER environment variable, or the --server flag, or in the \"servers\" section of %q (see https://camlistore.org/docs/client-config).", osutil.UserClientConfigPath()) + })) + ErrClientNoPublicKey = addCamError("client-no-public-key", str("No public key configured: see 'camput init'.")) +) + +type str string + +func (s str) String() string { return string(s) } + +type funcStr func() string + +func (f funcStr) String() string { return f() } + +type camErr struct { + key string + des fmt.Stringer +} + +func (ce *camErr) Error() string { + return ce.des.String() +} + +func (ce *camErr) Fatal() { + log.Fatalf("%v error. See %v", ce.key, ce.URL()) +} + +func (ce *camErr) Warn() { + log.Printf("%v error. See %v.", ce.key, ce.URL()) +} + +func (ce *camErr) URL() string { + return fmt.Sprintf("https://camlistore.org/err/%s", ce.key) +} + +// Err returns the error registered for key. +// It panics for an unregistered key. +func Err(key string) error { + v, ok := camErrors[key] + if !ok { + panic(fmt.Sprintf("unknown/unregistered error key %v", key)) + } + return v +} + +func addCamError(key string, des fmt.Stringer) *camErr { + if e, ok := camErrors[key]; ok { + panic(fmt.Sprintf("error %v already registered as %q", key, e.Error())) + } + e := &camErr{ + key: key, + des: des, + } + camErrors[key] = e + return e +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/search.go b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/search.go new file mode 100644 index 00000000..c132680f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/search.go @@ -0,0 +1,254 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package camtypes + +import ( + "bytes" + "fmt" + "path/filepath" + "strings" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/types" +) + +type RecentPermanode struct { + Permanode blob.Ref + Signer blob.Ref // may be zero (!Valid()) + LastModTime time.Time +} + +func (a RecentPermanode) Equal(b RecentPermanode) bool { + return a.Permanode == b.Permanode && + a.Signer == b.Signer && + a.LastModTime.Equal(b.LastModTime) +} + +type Claim struct { + // TODO: document/decide how to represent "multi" claims here. One Claim each? Add Multi in here? + // Move/merge this in with the schema package? + + BlobRef, Signer blob.Ref + + Date time.Time + Type string // "set-attribute", "add-attribute", etc + + // If an attribute modification + Attr, Value string + Permanode blob.Ref + + // If a DeleteClaim or a ShareClaim + Target blob.Ref +} + +func (c *Claim) String() string { + return fmt.Sprintf( + "camtypes.Claim{BlobRef: %s, Signer: %s, Permanode: %s, Date: %s, Type: %s, Attr: %s, Value: %s}", + c.BlobRef, c.Signer, c.Permanode, c.Date, c.Type, c.Attr, c.Value) +} + +type ClaimPtrsByDate []*Claim + +func (cl ClaimPtrsByDate) Len() int { return len(cl) } +func (cl ClaimPtrsByDate) Less(i, j int) bool { return cl[i].Date.Before(cl[j].Date) } +func (cl ClaimPtrsByDate) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] } + +type ClaimsByDate []Claim + +func (cl ClaimsByDate) Len() int { return len(cl) } +func (cl ClaimsByDate) Less(i, j int) bool { return cl[i].Date.Before(cl[j].Date) } +func (cl ClaimsByDate) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] } + +func (cl ClaimsByDate) String() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "[%d claims: ", len(cl)) + for _, r := range cl { + buf.WriteString(r.String()) + } + buf.WriteString("]") + return buf.String() +} + +// FileInfo describes a file or directory. +type FileInfo struct { + // FileName is the base name of the file or directory. + FileName string `json:"fileName"` + + // TODO(mpl): I've noticed that Size is actually set to the + // number of entries in the dir. fix the doc or the behaviour? + + // Size is the size of file. It is not set for directories. + Size int64 `json:"size"` + + // MIMEType may be set for files, but never for directories. + MIMEType string `json:"mimeType,omitempty"` + + // Time is the earliest of any modtime, creation time, or EXIF + // original/modification times found. It may be omitted (zero) + // if unknown. + Time *types.Time3339 `json:"time,omitempty"` + + // ModTime is the latest of any modtime, creation time, or EXIF + // original/modification times found. If ModTime doesn't differ + // from Time, ModTime is omitted (zero). + ModTime *types.Time3339 `json:"modTime,omitempty"` + + // WholeRef is the digest of the entire file contents. + // This will be zero for non-regular files, and may also be zero + // for files above a certain size threshold. + WholeRef blob.Ref `json:"wholeRef,omitempty"` +} + +func (fi *FileInfo) IsImage() bool { + return strings.HasPrefix(fi.MIMEType, "image/") +} + +var videoExtensions = map[string]bool{ + "3gp": true, + "avi": true, + "flv": true, + "m1v": true, + "m2v": true, + "m4v": true, + "mkv": true, + "mov": true, + "mp4": true, + "mpeg": true, + "mpg": true, + "ogv": true, + "wmv": true, +} + +func (fi *FileInfo) IsVideo() bool { + if strings.HasPrefix(fi.MIMEType, "video/") { + return true + } + + var ext string + if e := filepath.Ext(fi.FileName); strings.HasPrefix(e, ".") { + ext = e[1:] + } else { + return false + } + + // Case-insensitive lookup. + // Optimistically assume a short ASCII extension and be + // allocation-free in that case. + var buf [10]byte + lower := buf[:0] + const utf8RuneSelf = 0x80 // from utf8 package, but not importing it. + for i := 0; i < len(ext); i++ { + c := ext[i] + if c >= utf8RuneSelf { + // Slow path. + return videoExtensions[strings.ToLower(ext)] + } + if 'A' <= c && c <= 'Z' { + lower = append(lower, c+('a'-'A')) + } else { + lower = append(lower, c) + } + } + // The conversion from []byte to string doesn't allocate in + // a map lookup. + return videoExtensions[string(lower)] +} + +// ImageInfo describes an image file. +// +// The Width and Height are uint16s to save memory in index/corpus.go, and that's +// the max size of a JPEG anyway. If we want to deal with larger sizes, we can use +// MaxUint16 as a sentinel to mean to look elsewhere. Or ditch this optimization. +type ImageInfo struct { + // Width is the visible width of the image (after any necessary EXIF rotation). + Width uint16 `json:"width"` + // Height is the visible height of the image (after any necessary EXIF rotation). + Height uint16 `json:"height"` +} + +type Path struct { + Claim, Base, Target blob.Ref + ClaimDate time.Time + Suffix string // ?? +} + +func (p *Path) String() string { + return fmt.Sprintf("Path{Claim: %v, %v; Base: %v + Suffix %q => Target %v}", + p.Claim, p.ClaimDate, p.Base, p.Suffix, p.Target) +} + +type PermanodeByAttrRequest struct { + Signer blob.Ref + + // Attribute to search. currently supported: "tag", "title" + // If FuzzyMatch is set, this can be blank to search all + // attributes. + Attribute string + + // The attribute value to find exactly (or roughly, if + // FuzzyMatch is set) + // If blank, the permanodes with Attribute as an attribute + // (set to any value) are searched. + Query string + + FuzzyMatch bool // by default, an exact match is required + MaxResults int // optional max results +} + +type EdgesToOpts struct { + Max int + // TODO: filter by type? +} + +type Edge struct { + From blob.Ref + FromType string // "permanode", "directory", etc + FromTitle string // name of source permanode or directory + To blob.Ref + BlobRef blob.Ref // the blob responsible for the edge relationship +} + +func (e *Edge) String() string { + return fmt.Sprintf("[edge from:%s to:%s type:%s title:%s]", e.From, e.To, e.FromType, e.FromTitle) +} + +// BlobMeta is the metadata kept for each known blob in the in-memory +// search index. It's kept as small as possible to save memory. +type BlobMeta struct { + Ref blob.Ref + Size uint32 + + // CamliType is non-empty if this blob is a Camlistore JSON + // schema blob. If so, this is its "camliType" attribute. + CamliType string + + // TODO(bradfitz): change CamliTypethis *string to save 8 bytes +} + +// SearchErrorResponse is the JSON error response for a search request. +type SearchErrorResponse struct { + Error string `json:"error,omitempty"` // The error message. + ErrorType string `json:"errorType,omitempty"` // The type of the error. +} + +// FileSearchResponse is the JSON response to a file search request. +type FileSearchResponse struct { + SearchErrorResponse + + Files []blob.Ref `json:"files"` // Refs of the result files. Never nil. +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/search_test.go b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/search_test.go new file mode 100644 index 00000000..dd009570 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/search_test.go @@ -0,0 +1,42 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package camtypes + +import "testing" + +var fileInfoVideoTable = []struct { + fi *FileInfo + video bool +}{ + {&FileInfo{FileName: "some.mp4", MIMEType: "application/octet-stream"}, true}, + {&FileInfo{FileName: "IMG_1231.MOV", MIMEType: "application/octet-stream"}, true}, + {&FileInfo{FileName: "movie.mkv", MIMEType: "application/octet-stream"}, true}, + {&FileInfo{FileName: "movie.məv", MIMEType: "application/octet-stream"}, false}, + {&FileInfo{FileName: "tape", MIMEType: "video/webm"}, true}, + {&FileInfo{FileName: "tape", MIMEType: "application/ogg"}, false}, + {&FileInfo{FileName: "IMG_12312.jpg", MIMEType: "application/octet-stream"}, false}, + {&FileInfo{FileName: "IMG_12312.jpg", MIMEType: "image/jpeg"}, false}, +} + +func TestIsVideo(t *testing.T) { + for _, example := range fileInfoVideoTable { + if example.fi.IsVideo() != example.video { + t.Errorf("IsVideo failed video=%t filename=%s mimetype=%s", + example.video, example.fi.FileName, example.fi.MIMEType) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/sign.go b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/sign.go new file mode 100644 index 00000000..d0a17803 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/sign.go @@ -0,0 +1,29 @@ +/* +Copyright 2015 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package camtypes + +// VerifyResponse is the JSON response for a signature verification request. +type VerifyResponse struct { + // SignatureValid is true if the signature is valid. + SignatureValid bool `json:"signatureValid"` + // ErrorMessage contains the error that occurred, if any. + ErrorMessage string `json:"errorMessage,omitempty"` + // SignerKeyId is the ID of the signing key. + SignerKeyId string `json:"signerKeyId,omitempty"` + // VerifiedData contains the JSON values from the payload that we signed. + VerifiedData map[string]interface{} `json:"verifiedData,omitempty"` +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/statustype.go b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/statustype.go new file mode 100644 index 00000000..74705ab5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/camtypes/statustype.go @@ -0,0 +1,22 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package camtypes + +type StatusError struct { + Error string `json:"error"` + URL string `json:"url,omitempty"` // optional +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/clientconfig/config.go b/vendor/github.com/camlistore/camlistore/pkg/types/clientconfig/config.go new file mode 100644 index 00000000..53e6fc70 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/clientconfig/config.go @@ -0,0 +1,163 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package clientconfig provides types related to the client configuration +// file. +package clientconfig + +import ( + "errors" + "fmt" + "strings" + + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/wkfs" +) + +// Config holds the values from the JSON client config file. +type Config struct { + Servers map[string]*Server `json:"servers"` // maps server alias to server config. + Identity string `json:"identity"` // GPG identity. + IdentitySecretRing string `json:"identitySecretRing,omitempty"` // location of the secret ring file. + IgnoredFiles []string `json:"ignoredFiles,omitempty"` // list of files that camput should ignore. +} + +// Server holds the values specific to each server found in the JSON client +// config file. +type Server struct { + Server string `json:"server"` // server URL (scheme + hostname). + Auth string `json:"auth"` // auth scheme and values (ex: userpass:foo:bar). + IsDefault bool `json:"default,omitempty"` // whether this server is the default one. + TrustedCerts []string `json:"trustedCerts,omitempty"` // list of trusted certificates fingerprints. +} + +// Alias returns the alias of the server from conf that matches server, or the +// empty string if no match. A match means the server from the config is a +// prefix of the input server. The longest match prevails. +func (conf *Config) Alias(server string) string { + longestMatch := "" + serverAlias := "" + for alias, serverConf := range conf.Servers { + if strings.HasPrefix(server, serverConf.Server) { + if len(serverConf.Server) > len(longestMatch) { + longestMatch = serverConf.Server + serverAlias = alias + } + } + } + return serverAlias +} + +// GenerateClientConfig retuns a client configuration which can be used to +// access a server defined by the provided low-level server configuration. +func GenerateClientConfig(serverConfig jsonconfig.Obj) (*Config, error) { + missingConfig := func(param string) (*Config, error) { + return nil, fmt.Errorf("required value for '%s' not found", param) + } + + if serverConfig == nil { + return nil, errors.New("server config is a required parameter") + } + param := "auth" + auth := serverConfig.OptionalString(param, "") + if auth == "" { + return missingConfig(param) + } + + listen := serverConfig.OptionalString("listen", "") + baseURL := serverConfig.OptionalString("baseURL", "") + if listen == "" { + listen = baseURL + } + if listen == "" { + return nil, errors.New("required value for 'listen' or 'baseURL' not found") + } + + https := serverConfig.OptionalBool("https", false) + if !strings.HasPrefix(listen, "http://") && !strings.HasPrefix(listen, "https://") { + if !https { + listen = "http://" + listen + } else { + listen = "https://" + listen + } + } + + param = "httpsCert" + httpsCert := serverConfig.OptionalString(param, "") + if https && httpsCert == "" { + return missingConfig(param) + } + + // TODO(mpl): See if we can detect that the cert is not self-signed,and in + // that case not add it to the trustedCerts + var trustedList []string + if https && httpsCert != "" { + certPEMBlock, err := wkfs.ReadFile(httpsCert) + if err != nil { + return nil, fmt.Errorf("could not read certificate: %v", err) + } + sig, err := httputil.CertFingerprint(certPEMBlock) + if err != nil { + return nil, fmt.Errorf("could not get fingerprints of certificate: %v", err) + } + trustedList = []string{sig} + } + + param = "prefixes" + prefixes := serverConfig.OptionalObject(param) + if len(prefixes) == 0 { + return missingConfig(param) + } + + param = "/sighelper/" + sighelper := prefixes.OptionalObject(param) + if len(sighelper) == 0 { + return missingConfig(param) + } + + param = "handlerArgs" + handlerArgs := sighelper.OptionalObject(param) + if len(handlerArgs) == 0 { + return missingConfig(param) + } + + param = "keyId" + keyId := handlerArgs.OptionalString(param, "") + if keyId == "" { + return missingConfig(param) + } + + param = "secretRing" + secretRing := handlerArgs.OptionalString(param, "") + if secretRing == "" { + return missingConfig(param) + } + + return &Config{ + Servers: map[string]*Server{ + "default": { + Server: listen, + Auth: auth, + IsDefault: true, + TrustedCerts: trustedList, + }, + }, + Identity: keyId, + IdentitySecretRing: secretRing, + IgnoredFiles: []string{".DS_Store"}, + }, nil +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/example_test.go b/vendor/github.com/camlistore/camlistore/pkg/types/example_test.go new file mode 100644 index 00000000..44553880 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/example_test.go @@ -0,0 +1,22 @@ +package types + +import ( + "expvar" + "fmt" + "io" + "io/ioutil" + "strings" +) + +func ExampleNewStatsReader() { + var ( + // r is the io.Reader we'd like to count read from. + r = strings.NewReader("Hello world") + v = expvar.NewInt("read-bytes") + sw = NewStatsReader(v, r) + ) + // Read from the wrapped io.Reader, StatReader will count the bytes. + io.Copy(ioutil.Discard, sw) + fmt.Printf("Read %s bytes\n", v.String()) + // Output: Read 11 bytes +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/fakeseeker.go b/vendor/github.com/camlistore/camlistore/pkg/types/fakeseeker.go new file mode 100644 index 00000000..f0ee776a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/fakeseeker.go @@ -0,0 +1,70 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +import ( + "errors" + "fmt" + "io" + "os" +) + +// fakeSeeker can seek to the ends but any read not at the current +// position will fail. +type fakeSeeker struct { + r io.Reader + size int64 + + fakePos int64 + realPos int64 +} + +// NewFakeSeeker returns a ReadSeeker that can pretend to Seek (based +// on the provided total size of the reader's content), but any reads +// will fail if the fake seek position doesn't match reality. +func NewFakeSeeker(r io.Reader, size int64) io.ReadSeeker { + return &fakeSeeker{r: r, size: size} +} + +func (fs *fakeSeeker) Seek(offset int64, whence int) (int64, error) { + var newo int64 + switch whence { + default: + return 0, errors.New("invalid whence") + case os.SEEK_SET: + newo = offset + case os.SEEK_CUR: + newo = fs.fakePos + offset + case os.SEEK_END: + newo = fs.size + offset + } + if newo < 0 { + return 0, errors.New("negative seek") + } + fs.fakePos = newo + return newo, nil +} + +func (fs *fakeSeeker) Read(p []byte) (n int, err error) { + if fs.fakePos != fs.realPos { + return 0, fmt.Errorf("attempt to read from fake seek offset %d; real offset is %d", fs.fakePos, fs.realPos) + } + n, err = fs.r.Read(p) + fs.fakePos += int64(n) + fs.realPos += int64(n) + return +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/fakeseeker_test.go b/vendor/github.com/camlistore/camlistore/pkg/types/fakeseeker_test.go new file mode 100644 index 00000000..aa2b55a5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/fakeseeker_test.go @@ -0,0 +1,55 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +import ( + "os" + "strings" + "testing" +) + +func TestFakeSeeker(t *testing.T) { + rs := NewFakeSeeker(strings.NewReader("foobar"), 6) + if pos, err := rs.Seek(0, os.SEEK_END); err != nil || pos != 6 { + t.Fatalf("SEEK_END = %d, %v; want 6, nil", pos, err) + } + if pos, err := rs.Seek(0, os.SEEK_CUR); err != nil || pos != 6 { + t.Fatalf("SEEK_CUR = %d, %v; want 6, nil", pos, err) + } + if pos, err := rs.Seek(0, os.SEEK_SET); err != nil || pos != 0 { + t.Fatalf("SEEK_SET = %d, %v; want 0, nil", pos, err) + } + + buf := make([]byte, 3) + if n, err := rs.Read(buf); n != 3 || err != nil || string(buf) != "foo" { + t.Fatalf("First read = %d, %v (buf = %q); want foo", n, err, buf) + } + if pos, err := rs.Seek(0, os.SEEK_CUR); err != nil || pos != 3 { + t.Fatalf("Seek cur pos after first read = %d, %v; want 3, nil", pos, err) + } + if n, err := rs.Read(buf); n != 3 || err != nil || string(buf) != "bar" { + t.Fatalf("Second read = %d, %v (buf = %q); want foo", n, err, buf) + } + + if pos, err := rs.Seek(1, os.SEEK_SET); err != nil || pos != 1 { + t.Fatalf("SEEK_SET = %d, %v; want 1, nil", pos, err) + } + const msg = "attempt to read from fake seek offset" + if _, err := rs.Read(buf); err == nil || !strings.Contains(err.Error(), msg) { + t.Fatalf("bogus Read after seek = %v; want something containing %q", err, msg) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/serverconfig/config.go b/vendor/github.com/camlistore/camlistore/pkg/types/serverconfig/config.go new file mode 100644 index 00000000..a6d29062 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/serverconfig/config.go @@ -0,0 +1,115 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package serverconfig provides types related to the server configuration file. +package serverconfig + +import ( + "camlistore.org/pkg/types" +) + +// Config holds the values from the JSON (high-level) server config +// file that is exposed to users (and is by default at +// osutil.UserServerConfigPath). From this simpler configuration, a +// complete, low-level one, is generated by +// serverinit.genLowLevelConfig, and used to configure the various +// Camlistore components. +type Config struct { + Auth string `json:"auth"` // auth scheme and values (ex: userpass:foo:bar). + BaseURL string `json:"baseURL,omitempty"` // Base URL the server advertizes. For when behind a proxy. + Listen string `json:"listen"` // address (of the form host|ip:port) on which the server will listen on. + Identity string `json:"identity"` // GPG identity. + IdentitySecretRing string `json:"identitySecretRing"` // path to the secret ring file. + // alternative source tree, to override the embedded ui and/or closure resources. + // If non empty, the ui files will be expected at + // sourceRoot + "/server/camlistored/ui" and the closure library at + // sourceRoot + "/third_party/closure/lib" + // Also used by the publish handler. + SourceRoot string `json:"sourceRoot,omitempty"` + OwnerName string `json:"ownerName,omitempty"` + + // Blob storage. + MemoryStorage bool `json:"memoryStorage,omitempty"` // do not store anything (blobs or queues) on localdisk, use memory instead. + BlobPath string `json:"blobPath,omitempty"` // path to the directory containing the blobs. + PackBlobs bool `json:"packBlobs,omitempty"` // use "diskpacked" instead of the default filestorage. (exclusive with PackRelated) + PackRelated bool `json:"packRelated,omitempty"` // use "blobpacked" instead of the default storage (exclusive with PackBlobs) + S3 string `json:"s3,omitempty"` // Amazon S3 credentials: access_key_id:secret_access_key:bucket[:hostname]. + GoogleCloudStorage string `json:"googlecloudstorage,omitempty"` // Google Cloud credentials: clientId:clientSecret:refreshToken:bucket[/optional/dir] or ":bucket[/optional/dir/]" for auto on GCE + GoogleDrive string `json:"googledrive,omitempty"` // Google Drive credentials: clientId:clientSecret:refreshToken:parentId. + ShareHandler bool `json:"shareHandler,omitempty"` // enable the share handler. If true, and shareHandlerPath is empty then shareHandlerPath will default to "/share/" when generating the low-level config. + ShareHandlerPath string `json:"shareHandlerPath,omitempty"` // URL prefix for the share handler. If set, overrides shareHandler. + + // HTTPS. + HTTPS bool `json:"https,omitempty"` // enable HTTPS. + HTTPSCert string `json:"httpsCert,omitempty"` // path to the HTTPS certificate file. + HTTPSKey string `json:"httpsKey,omitempty"` // path to the HTTPS key file. + + // Index. + RunIndex types.InvertedBool `json:"runIndex,omitempty"` // if logically false: no search, no UI, etc. + CopyIndexToMemory types.InvertedBool `json:"copyIndexToMemory,omitempty"` // copy disk-based index to memory on start-up. + MemoryIndex bool `json:"memoryIndex,omitempty"` // use memory-only indexer. + DBName string `json:"dbname,omitempty"` // name of the database for mysql, postgres, mongo. + LevelDB string `json:"levelDB,omitempty"` // path to the levelDB directory, for indexing with github.com/syndtr/goleveldb. + KVFile string `json:"kvIndexFile,omitempty"` // path to the kv file, for indexing with github.com/cznic/kv. + MySQL string `json:"mysql,omitempty"` // MySQL credentials (username@host:password), for indexing with MySQL. + Mongo string `json:"mongo,omitempty"` // MongoDB credentials ([username:password@]host), for indexing with MongoDB. + PostgreSQL string `json:"postgres,omitempty"` // PostgreSQL credentials (username@host:password), for indexing with PostgreSQL. + SQLite string `json:"sqlite,omitempty"` // path to the SQLite file, for indexing with SQLite. + + // DBNames lists which database names to use for various types of key/value stores. The keys may be: + // "index" (overrides 'dbname' key above) + // "queue-sync-to-index" (the sync queue to index things) + // "queue-sync-to-s3" (the sync queue to replicate to s3) + // "blobpacked_index" (the index for blobpacked, the 'packRelated' option) + // "ui_thumbcache" + DBNames map[string]string `json:"dbNames"` + + ReplicateTo []interface{} `json:"replicateTo,omitempty"` // NOOP for now. + // Publish maps a URL prefix path used as a root for published paths (a.k.a. a camliRoot path), to the configuration of the publish handler that serves all the published paths under this root. + Publish map[string]*Publish `json:"publish,omitempty"` + + // TODO(mpl): map of importers instead? + Flickr string `json:"flickr,omitempty"` // flicker importer. + Picasa string `json:"picasa,omitempty"` // picasa importer. +} + +// Publish holds the server configuration values specific to a publisher, i.e. to a publish prefix. +type Publish struct { + // Program is the server app program to run as the publisher. + // Defaults to "publisher". + Program string `json:"program"` + + // CamliRoot value that defines our root permanode for this + // publisher. The root permanode is used as the root for all the + // paths served by this publisher. + CamliRoot string `json:"camliRoot"` + + // Base URL the app will run at. + BaseURL string `json:"baseURL,omitempty"` + + // GoTemplate is the name of the Go template file used by this + // publisher to represent the data. This file should live in + // app/publisher/. + GoTemplate string `json:"goTemplate"` + + // CacheRoot is the path that will be used as the root for the + // caching blobserver (for images). No caching if empty. + // An example value is Config.BlobPath + "/cache". + CacheRoot string `json:"cacheRoot,omitempty"` + + HTTPSCert string `json:"httpsCert,omitempty"` // path to the HTTPS certificate file. + HTTPSKey string `json:"httpsKey,omitempty"` // path to the HTTPS key file. +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/types.go b/vendor/github.com/camlistore/camlistore/pkg/types/types.go new file mode 100644 index 00000000..7b31bc6f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/types.go @@ -0,0 +1,260 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package types provides various common types. +package types + +import ( + "bytes" + "encoding/json" + "expvar" + "fmt" + "io" + "io/ioutil" + "math" + "regexp" + "runtime" + "strings" + "sync" + "time" +) + +var ( + goVersion = runtime.Version() + dotNumbers = regexp.MustCompile(`\.\d+`) + null_b = []byte("null") +) + +// NopCloser is an io.Closer that does nothing. +var NopCloser io.Closer = ioutil.NopCloser(nil) + +// EmptyBody is a ReadCloser that returns EOF on Read and does nothing +// on Close. +var EmptyBody io.ReadCloser = ioutil.NopCloser(strings.NewReader("")) + +// Time3339 is a time.Time which encodes to and from JSON +// as an RFC 3339 time in UTC. +type Time3339 time.Time + +var ( + _ json.Marshaler = Time3339{} + _ json.Unmarshaler = (*Time3339)(nil) +) + +func (t Time3339) String() string { + return time.Time(t).UTC().Format(time.RFC3339Nano) +} + +func (t Time3339) MarshalJSON() ([]byte, error) { + if t.Time().IsZero() { + return null_b, nil + } + return json.Marshal(t.String()) +} + +func (t *Time3339) UnmarshalJSON(b []byte) error { + if bytes.Equal(b, null_b) { + *t = Time3339{} + return nil + } + if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' { + return fmt.Errorf("types: failed to unmarshal non-string value %q as an RFC 3339 time", b) + } + s := string(b[1 : len(b)-1]) + if s == "" { + *t = Time3339{} + return nil + } + tm, err := time.Parse(time.RFC3339Nano, s) + if err != nil { + if strings.HasPrefix(s, "0000-00-00T00:00:00") { + *t = Time3339{} + return nil + } + return err + } + *t = Time3339(tm) + return nil +} + +// ParseTime3339OrZero parses a string in RFC3339 format. If it's invalid, +// the zero time value is returned instead. +func ParseTime3339OrZero(v string) Time3339 { + t, err := time.Parse(time.RFC3339Nano, v) + if err != nil { + return Time3339{} + } + return Time3339(t) +} + +func ParseTime3339OrNil(v string) *Time3339 { + t, err := time.Parse(time.RFC3339Nano, v) + if err != nil { + return nil + } + tm := Time3339(t) + return &tm +} + +// Time returns the time as a time.Time with slightly less stutter +// than a manual conversion. +func (t Time3339) Time() time.Time { + return time.Time(t) +} + +// IsZero returns whether the time is Go zero or Unix zero. +func (t *Time3339) IsZero() bool { + return t == nil || time.Time(*t).IsZero() || time.Time(*t).Unix() == 0 +} + +// ByTime sorts times. +type ByTime []time.Time + +func (s ByTime) Len() int { return len(s) } +func (s ByTime) Less(i, j int) bool { return s[i].Before(s[j]) } +func (s ByTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// A ReadSeekCloser can Read, Seek, and Close. +type ReadSeekCloser interface { + io.Reader + io.Seeker + io.Closer +} + +type ReaderAtCloser interface { + io.ReaderAt + io.Closer +} + +type SizeReaderAt interface { + io.ReaderAt + Size() int64 +} + +// TODO(wathiede): make sure all the stat readers work with code that +// type asserts ReadFrom/WriteTo. + +type varStatReader struct { + *expvar.Int + r io.Reader +} + +// NewReaderStats returns an io.Reader that will have the number of bytes +// read from r added to v. +func NewStatsReader(v *expvar.Int, r io.Reader) io.Reader { + return &varStatReader{v, r} +} + +func (v *varStatReader) Read(p []byte) (int, error) { + n, err := v.r.Read(p) + v.Int.Add(int64(n)) + return n, err +} + +type varStatReadSeeker struct { + *expvar.Int + rs io.ReadSeeker +} + +// NewReaderStats returns an io.ReadSeeker that will have the number of bytes +// read from rs added to v. +func NewStatsReadSeeker(v *expvar.Int, r io.ReadSeeker) io.ReadSeeker { + return &varStatReadSeeker{v, r} +} + +func (v *varStatReadSeeker) Read(p []byte) (int, error) { + n, err := v.rs.Read(p) + v.Int.Add(int64(n)) + return n, err +} + +func (v *varStatReadSeeker) Seek(offset int64, whence int) (int64, error) { + return v.rs.Seek(offset, whence) +} + +// InvertedBool is a bool that marshals to and from JSON with the opposite of its in-memory value. +type InvertedBool bool + +func (ib InvertedBool) MarshalJSON() ([]byte, error) { + return json.Marshal(!bool(ib)) +} + +func (ib *InvertedBool) UnmarshalJSON(b []byte) error { + var bo bool + if err := json.Unmarshal(b, &bo); err != nil { + return err + } + *ib = InvertedBool(!bo) + return nil +} + +// Get returns the logical value of ib. +func (ib InvertedBool) Get() bool { + return !bool(ib) +} + +// U32 converts n to an uint32, or panics if n is out of range +func U32(n int64) uint32 { + if n < 0 || n > math.MaxUint32 { + panic("bad size " + fmt.Sprint(n)) + } + return uint32(n) +} + +// NewOnceCloser returns a Closer wrapping c which only calls Close on c +// once. Subsequent calls to Close return nil. +func NewOnceCloser(c io.Closer) io.Closer { + return &onceCloser{c: c} +} + +type onceCloser struct { + mu sync.Mutex + c io.Closer +} + +func (c *onceCloser) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + if c.c == nil { + return nil + } + err := c.c.Close() + c.c = nil + return err +} + +// TB is a copy of testing.TB so things can take a TB without linking +// in the testing package (which defines its own flags, etc). +type TB interface { + Error(args ...interface{}) + Errorf(format string, args ...interface{}) + Fail() + FailNow() + Failed() bool + Fatal(args ...interface{}) + Fatalf(format string, args ...interface{}) + Log(args ...interface{}) + Logf(format string, args ...interface{}) + Skip(args ...interface{}) + SkipNow() + Skipf(format string, args ...interface{}) + Skipped() bool +} + +// CloseFunc implements io.Closer with a function. +type CloseFunc func() error + +func (fn CloseFunc) Close() error { return fn() } diff --git a/vendor/github.com/camlistore/camlistore/pkg/types/types_test.go b/vendor/github.com/camlistore/camlistore/pkg/types/types_test.go new file mode 100644 index 00000000..8708c7dd --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/types/types_test.go @@ -0,0 +1,152 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +import ( + "encoding/json" + "strings" + "testing" + "time" +) + +func TestTime3339(t *testing.T) { + tm := time.Unix(123, 456) + t3 := Time3339(tm) + type O struct { + SomeTime Time3339 `json:"someTime"` + } + o := &O{SomeTime: t3} + got, err := json.Marshal(o) + if err != nil { + t.Fatal(err) + } + goodEnc := "{\"someTime\":\"1970-01-01T00:02:03.000000456Z\"}" + if string(got) != goodEnc { + t.Errorf("Encoding wrong.\n Got: %q\nWant: %q", got, goodEnc) + } + ogot := &O{} + err = json.Unmarshal([]byte(goodEnc), ogot) + if err != nil { + t.Fatal(err) + } + if !tm.Equal(ogot.SomeTime.Time()) { + t.Errorf("Unmarshal got time %v; want %v", ogot.SomeTime.Time(), tm) + } +} + +func TestTime3339_Marshal(t *testing.T) { + tests := []struct { + in time.Time + want string + }{ + {time.Time{}, "null"}, + {time.Unix(1, 0), `"1970-01-01T00:00:01Z"`}, + } + for i, tt := range tests { + got, err := Time3339(tt.in).MarshalJSON() + if err != nil { + t.Errorf("%d. marshal(%v) got error: %v", i, tt.in, err) + continue + } + if string(got) != tt.want { + t.Errorf("%d. marshal(%v) = %q; want %q", i, tt.in, got, tt.want) + } + } +} + +func TestTime3339_empty(t *testing.T) { + tests := []struct { + enc string + z bool + }{ + {enc: "null", z: true}, + {enc: `""`, z: true}, + {enc: "0000-00-00T00:00:00Z", z: true}, + {enc: "0001-01-01T00:00:00Z", z: true}, + {enc: "1970-01-01T00:00:00Z", z: true}, + {enc: "2001-02-03T04:05:06Z", z: false}, + {enc: "2001-02-03T04:05:06+06:00", z: false}, + {enc: "2001-02-03T04:05:06-06:00", z: false}, + {enc: "2001-02-03T04:05:06.123456789Z", z: false}, + {enc: "2001-02-03T04:05:06.123456789+06:00", z: false}, + {enc: "2001-02-03T04:05:06.123456789-06:00", z: false}, + } + for _, tt := range tests { + var tm Time3339 + enc := tt.enc + if strings.Contains(enc, "T") { + enc = "\"" + enc + "\"" + } + err := json.Unmarshal([]byte(enc), &tm) + if err != nil { + t.Errorf("unmarshal %q = %v", enc, err) + } + if tm.IsZero() != tt.z { + t.Errorf("unmarshal %q = %v (%d), %v; zero=%v; want %v", tt.enc, tm.Time(), tm.Time().Unix(), err, + !tt.z, tt.z) + } + } +} + +func TestInvertedBool_Unmarshal(t *testing.T) { + tests := []struct { + json string + want bool + }{ + {json: `{}`, want: true}, + {json: `{"key": true}`, want: true}, + {json: `{"key": false}`, want: false}, + } + type O struct { + Key InvertedBool + } + for _, tt := range tests { + obj := &O{} + if err := json.Unmarshal([]byte(tt.json), obj); err != nil { + t.Fatalf("Could not unmarshal %s: %v", tt.json, err) + } + if obj.Key.Get() != tt.want { + t.Errorf("Unmarshaled %s as InvertedBool; got %v, wanted %v", tt.json, obj.Key.Get(), tt.want) + } + } +} + +func TestInvertedBool_Marshal(t *testing.T) { + tests := []struct { + internalVal bool + want string + }{ + {internalVal: true, want: `{"key":false}`}, + {internalVal: false, want: `{"key":true}`}, + } + type O struct { + Key InvertedBool `json:"key"` + } + for _, tt := range tests { + + obj := &O{ + Key: InvertedBool(tt.internalVal), + } + b, err := json.Marshal(obj) + if err != nil { + t.Fatalf("Could not marshal %v: %v", tt.internalVal, err) + } + if string(b) != tt.want { + t.Errorf("Marshaled InvertedBool %v; got %v, wanted %v", tt.internalVal, string(b), tt.want) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/handler.go b/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/handler.go new file mode 100644 index 00000000..16e2778e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/handler.go @@ -0,0 +1,79 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package thumbnail + +import ( + "log" + "net/http" + "strings" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/schema" +) + +// serveRef gets the file at ref from fetcher and serves its contents. +// It is used by Service as a one time handler to serve to the thumbnail child process on localhost. +func serveRef(rw http.ResponseWriter, req *http.Request, ref blob.Ref, fetcher blob.Fetcher) { + + if !httputil.IsGet(req) { + http.Error(rw, "Invalid download method.", 400) + return + } + + if !httputil.IsLocalhost(req) { + http.Error(rw, "Forbidden.", 403) + return + } + + parts := strings.Split(req.URL.Path, "/") + if len(parts) < 2 { + http.Error(rw, "Malformed GET URL.", 400) + return + } + + blobRef, ok := blob.Parse(parts[1]) + if !ok { + http.Error(rw, "Malformed GET URL.", 400) + return + } + + // only serves its ref + if blobRef != ref { + log.Printf("videothumbnail: access to %v forbidden; wrong blobref for handler", blobRef) + http.Error(rw, "Forbidden.", 403) + return + } + + rw.Header().Set("Content-Type", "application/octet-stream") + + fr, err := schema.NewFileReader(fetcher, ref) + if err != nil { + httputil.ServeError(rw, req, err) + return + } + defer fr.Close() + + http.ServeContent(rw, req, "", time.Now(), fr) +} + +func createVideothumbnailHandler(ref blob.Ref, fetcher blob.Fetcher) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + serveRef(rw, req, ref, fetcher) + }) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/handler_test.go b/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/handler_test.go new file mode 100644 index 00000000..3dc077cb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/handler_test.go @@ -0,0 +1,75 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package thumbnail + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/test" +) + +func TestHandlerWrongRef(t *testing.T) { + storage := new(test.Fetcher) + ref := blob.MustParse("sha1-f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") + wrongRefString := "sha1-e242ed3bffccdf271b7fbaf34ed72d089537b42f" + ts := httptest.NewServer(createVideothumbnailHandler(ref, storage)) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/" + wrongRefString) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 403 { + t.Fatalf("excepted forbidden status when the wrong ref is requested") + } +} + +func TestHandlerRightRef(t *testing.T) { + b := test.Blob{Contents: "Foo"} + storage := new(test.Fetcher) + ref, err := schema.WriteFileFromReader(storage, "", b.Reader()) + if err != nil { + t.Fatal(err) + } + if err != nil { + t.Fatal(err) + } + + ts := httptest.NewServer(createVideothumbnailHandler(ref, storage)) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/" + ref.String()) + + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 200 { + t.Fatalf("expected 200 status: %v", resp) + } + content, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if string(content) != b.Contents { + t.Errorf("excepted handler to serve data") + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/service.go b/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/service.go new file mode 100644 index 00000000..1d0c7b35 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/service.go @@ -0,0 +1,161 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package thumbnail generates image thumbnails from videos. + +(*Service).Generate spawns an HTTP server listening on a local random +port to serve the video to an external program (see Thumbnailer interface). +The external program is expected to output the thumbnail image on its +standard output. + +The default implementation uses ffmpeg. + +See ServiceFromConfig for accepted configuration. +*/ +package thumbnail + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/netutil" + "camlistore.org/pkg/syncutil" +) + +// A Service controls the generation of video thumbnails. +type Service struct { + thumbnailer Thumbnailer + // Timeout is the maximum duration for the thumbnailing subprocess execution. + timeout time.Duration + gate *syncutil.Gate // of subprocesses. +} + +// ServiceFromConfig builds a new Service from configuration. +// Example expected configuration object (all keys are optional) : +// { +// // command defaults to FFmpegThumbnailer and $uri is replaced by +// // the real value at runtime. +// "command": ["/opt/local/bin/ffmpeg", "-i", "$uri", "pipe:1"], +// // Maximun number of milliseconds for running the thumbnailing subprocess. +// // A zero or negative timeout means no timeout. +// "timeout": 2000, +// // Maximum number of thumbnailing subprocess running at same time. +// // A zero or negative maxProcs means no limit. +// "maxProcs": 5 +// } +func ServiceFromConfig(conf jsonconfig.Obj) (*Service, error) { + th := thumbnailerFromConfig(conf) + timeout := conf.OptionalInt("timeout", 5000) + maxProc := conf.OptionalInt("maxProcs", 5) + + err := conf.Validate() + if err != nil { + return nil, err + } + + return NewService(th, time.Millisecond*time.Duration(timeout), maxProc), nil +} + +// NewService builds a new Service. Zero timeout or maxProcs means no limit. +func NewService(th Thumbnailer, timeout time.Duration, maxProcs int) *Service { + + var g *syncutil.Gate + if maxProcs > 0 { + g = syncutil.NewGate(maxProcs) + } + + return &Service{ + thumbnailer: th, + timeout: timeout, + gate: g, + } +} + +var errTimeout = errors.New("timeout.") + +// Generate reads the video given by videoRef from src and writes its thumbnail image to w. +func (s *Service) Generate(videoRef blob.Ref, w io.Writer, src blob.Fetcher) error { + + if s.gate != nil { + s.gate.Start() + defer s.gate.Done() + } + + ln, err := netutil.ListenOnLocalRandomPort() + if err != nil { + return err + } + defer ln.Close() + + videoUri := &url.URL{ + Scheme: "http", + Host: ln.Addr().String(), + Path: videoRef.String(), + } + + cmdErrc := make(chan error, 1) + cmd := buildCmd(s.thumbnailer, videoUri, w) + cmdErrOut, err := cmd.StderrPipe() + if err != nil { + return err + } + if err := cmd.Start(); err != nil { + return err + } + defer cmd.Process.Kill() + go func() { + out, err := ioutil.ReadAll(cmdErrOut) + if err != nil { + cmdErrc <- err + return + } + cmd.Wait() + if cmd.ProcessState.Success() { + cmdErrc <- nil + return + } + cmdErrc <- fmt.Errorf("thumbnail subprocess failed:\n%s", out) + }() + + servErrc := make(chan error, 1) + go func() { + servErrc <- http.Serve(ln, createVideothumbnailHandler(videoRef, src)) + }() + + select { + case err := <-cmdErrc: + return err + case err := <-servErrc: + return err + case <-s.timer(): + return errTimeout + } +} + +func (s *Service) timer() <-chan time.Time { + if s.timeout <= 0 { + return make(<-chan time.Time) + } + return time.After(s.timeout) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/service_test.go b/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/service_test.go new file mode 100644 index 00000000..340cf73a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/service_test.go @@ -0,0 +1,154 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package thumbnail + +import ( + "bytes" + "io/ioutil" + "net/url" + "os" + "os/exec" + "testing" + "time" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/magic" + "camlistore.org/pkg/schema" + "camlistore.org/pkg/test" +) + +const testFilepath = "testdata/small.webm" + +func storageAndBlobRef(t *testing.T) (blobserver.Storage, blob.Ref) { + storage := new(test.Fetcher) + inFile, err := os.Open(testFilepath) + if err != nil { + t.Fatal(err) + } + ref, err := schema.WriteFileFromReader(storage, "small.webm", inFile) + if err != nil { + t.Fatal(err) + } + return storage, ref +} + +func TestStorage(t *testing.T) { + store, ref := storageAndBlobRef(t) + fr, err := schema.NewFileReader(store, ref) + if err != nil { + t.Fatal(err) + } + inFile, err := os.Open(testFilepath) + if err != nil { + t.Fatal(err) + } + data, err := ioutil.ReadAll(inFile) + if err != nil { + t.Fatal(err) + } + bd, err := ioutil.ReadAll(fr) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(bd, data) { + t.Error("expected to be the same") + } +} + +func TestMakeThumbnail(t *testing.T) { + if _, err := exec.LookPath("ffmpeg"); err != nil { + t.Skip(err) + } + + store, ref := storageAndBlobRef(t) + tmpFile, _ := ioutil.TempFile(os.TempDir(), "camlitest") + defer tmpFile.Close() + service := NewService(DefaultThumbnailer, 2*time.Second, 5) + err := service.Generate(ref, tmpFile, store) + + if err != nil { + t.Fatal(err) + } + + tmpFile.Seek(0, 0) + + typ, _ := magic.MIMETypeFromReader(tmpFile) + if typ != "image/png" { + t.Errorf("excepted thumbnail mimetype to be `image/png` was `%s`", typ) + } + +} + +func TestMakeThumbnailWithZeroMaxProcsAndTimeout(t *testing.T) { + if _, err := exec.LookPath("ffmpeg"); err != nil { + t.Skip(err) + } + + store, ref := storageAndBlobRef(t) + tmpFile, _ := ioutil.TempFile(os.TempDir(), "camlitest") + defer tmpFile.Close() + service := NewService(DefaultThumbnailer, 0, 0) + err := service.Generate(ref, tmpFile, store) + + if err != nil { + t.Fatal(err) + } +} + +type failingThumbnailer struct{} + +func (failingThumbnailer) Command(*url.URL) (string, []string) { + return "failcommand", []string{} +} + +func TestMakeThumbnailFailure(t *testing.T) { + if _, err := exec.LookPath("ffmpeg"); err != nil { + t.Skip(err) + } + + store, ref := storageAndBlobRef(t) + service := NewService(failingThumbnailer{}, 2*time.Second, 5) + err := service.Generate(ref, ioutil.Discard, store) + + if err == nil { + t.Error("expected to fail.") + } + t.Logf("err output: %v", err) + +} + +type sleepyThumbnailer struct{} + +func (sleepyThumbnailer) Command(*url.URL) (string, []string) { + return "bash", []string{"-c", `echo "MAY SHOW" 1>&2; sleep 10; echo "SHOULD NEVER SHOW" 1>&2`} +} + +func TestThumbnailGenerateTimeout(t *testing.T) { + + if _, err := exec.LookPath("bash"); err != nil { + t.Skip("bash not in PATH.") + } + + store, ref := storageAndBlobRef(t) + service := NewService(sleepyThumbnailer{}, time.Duration(time.Millisecond), 5) + err := service.Generate(ref, ioutil.Discard, store) + + if err != errTimeout { + t.Errorf("expected to timeout: %v", err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/testdata/small.webm b/vendor/github.com/camlistore/camlistore/pkg/video/thumbnail/testdata/small.webm new file mode 100644 index 0000000000000000000000000000000000000000..da946da5290ec1ed99392c1a56ad5b4e95baa2e9 GIT binary patch literal 229455 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�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 O&=P(h2)qo!xcYy?GT: needs to be provided to start listening") + } + + var err error + s.listener, err = listen.Listen(addr) + if err != nil { + return fmt.Errorf("Failed to listen on %s: %v", addr, err) + } + base := s.ListenURL() + if doLog { + s.printf("Starting to listen on %s\n", base) + } + + if s.enableTLS { + config := &tls.Config{ + Rand: rand.Reader, + Time: time.Now, + NextProtos: []string{http2.NextProtoTLS, "http/1.1"}, + } + config.Certificates = make([]tls.Certificate, 1) + + config.Certificates[0], err = loadX509KeyPair(s.tlsCertFile, s.tlsKeyFile) + if err != nil { + return fmt.Errorf("Failed to load TLS cert: %v", err) + } + s.listener = tls.NewListener(s.listener, config) + } + + if doLog && strings.HasSuffix(base, ":0") { + s.printf("Now listening on %s\n", s.ListenURL()) + } + + return nil +} + +func (s *Server) throttleListener() net.Listener { + kBps, _ := strconv.Atoi(os.Getenv("DEV_THROTTLE_KBPS")) + ms, _ := strconv.Atoi(os.Getenv("DEV_THROTTLE_LATENCY_MS")) + if kBps == 0 && ms == 0 { + return s.listener + } + rate := throttle.Rate{ + KBps: kBps, + Latency: time.Duration(ms) * time.Millisecond, + } + return &throttle.Listener{ + Listener: s.listener, + Down: rate, + Up: rate, // TODO: separate rates? + } +} + +func (s *Server) Serve() { + if err := s.Listen(""); err != nil { + s.fatalf("Listen error: %v", err) + } + go runTestHarnessIntegration(s.listener) + + srv := &http.Server{ + Handler: s, + } + // TODO: allow configuring src.ErrorLog (and plumb through to + // Google Cloud Logging when run on GCE, eventually) + + // Setup the NPN NextProto map for HTTP/2 support: + http2.ConfigureServer(srv, &s.H2Server) + + err := srv.Serve(s.throttleListener()) + if err != nil { + s.printf("Error in http server: %v\n", err) + os.Exit(1) + } +} + +// Signals the test harness that we've started listening. +// TODO: write back the port number that we randomly selected? +// For now just writes back a single byte. +func runTestHarnessIntegration(listener net.Listener) { + writePipe, err := pipeFromEnvFd("TESTING_PORT_WRITE_FD") + if err != nil { + return + } + readPipe, _ := pipeFromEnvFd("TESTING_CONTROL_READ_FD") + + if writePipe != nil { + writePipe.Write([]byte(listener.Addr().String() + "\n")) + } + + if readPipe != nil { + bufr := bufio.NewReader(readPipe) + for { + line, err := bufr.ReadString('\n') + if err == io.EOF || line == "EXIT\n" { + os.Exit(0) + } + return + } + } +} + +// loadX509KeyPair is a copy of tls.LoadX509KeyPair but using wkfs. +func loadX509KeyPair(certFile, keyFile string) (cert tls.Certificate, err error) { + certPEMBlock, err := wkfs.ReadFile(certFile) + if err != nil { + return + } + keyPEMBlock, err := wkfs.ReadFile(keyFile) + if err != nil { + return + } + return tls.X509KeyPair(certPEMBlock, keyPEMBlock) +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/wkfs/gcs/gcs.go b/vendor/github.com/camlistore/camlistore/pkg/wkfs/gcs/gcs.go new file mode 100644 index 00000000..c7489a1e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/wkfs/gcs/gcs.go @@ -0,0 +1,204 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package gcs registers a Google Cloud Storage filesystem at the +// well-known /gcs/ filesystem path if the current machine is running +// on Google Compute Engine. +package gcs + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "strings" + "sync" + "time" + + "camlistore.org/pkg/googlestorage" + "camlistore.org/pkg/wkfs" + "google.golang.org/cloud/compute/metadata" +) + +// Max size for all files read or written. This filesystem is only +// supposed to be for configuration data only, so this is very +// generous. +const maxSize = 1 << 20 + +func init() { + if !metadata.OnGCE() { + return + } + client, err := googlestorage.NewServiceClient() + wkfs.RegisterFS("/gcs/", &gcsFS{client, err}) +} + +type gcsFS struct { + client *googlestorage.Client + err error // sticky error +} + +func (fs *gcsFS) parseName(name string) (bucket, key string, err error) { + if fs.err != nil { + return "", "", fs.err + } + name = strings.TrimPrefix(name, "/gcs/") + i := strings.Index(name, "/") + if i < 0 { + return name, "", nil + } + return name[:i], name[i+1:], nil +} + +func (fs *gcsFS) Open(name string) (wkfs.File, error) { + bucket, key, err := fs.parseName(name) + if err != nil { + return nil, fs.err + } + rc, size, err := fs.client.GetObject(&googlestorage.Object{ + Bucket: bucket, + Key: key, + }) + if err != nil { + return nil, err + } + defer rc.Close() + if size > maxSize { + return nil, fmt.Errorf("file %s too large (%d bytes) for /gcs/ filesystem", name, size) + } + slurp, err := ioutil.ReadAll(io.LimitReader(rc, size)) + if err != nil { + return nil, err + } + return &file{ + name: name, + Reader: bytes.NewReader(slurp), + }, nil +} + +func (fs *gcsFS) Stat(name string) (os.FileInfo, error) { return fs.Lstat(name) } +func (fs *gcsFS) Lstat(name string) (os.FileInfo, error) { + bucket, key, err := fs.parseName(name) + if err != nil { + return nil, err + } + size, exists, err := fs.client.StatObject(&googlestorage.Object{ + Bucket: bucket, + Key: key, + }) + if err != nil { + return nil, err + } + if !exists { + return nil, os.ErrNotExist + } + return &statInfo{ + name: name, + size: size, + }, nil +} + +func (fs *gcsFS) MkdirAll(path string, perm os.FileMode) error { return nil } + +func (fs *gcsFS) OpenFile(name string, flag int, perm os.FileMode) (wkfs.FileWriter, error) { + bucket, key, err := fs.parseName(name) + if err != nil { + return nil, err + } + switch flag { + case os.O_WRONLY | os.O_CREATE | os.O_EXCL: + case os.O_WRONLY | os.O_CREATE | os.O_TRUNC: + default: + return nil, fmt.Errorf("Unsupported OpenFlag flag mode %d on Google Cloud Storage", flag) + } + if flag&os.O_EXCL != 0 { + if _, err := fs.Stat(name); err == nil { + return nil, os.ErrExist + } + } + return &fileWriter{ + fs: fs, + name: name, + bucket: bucket, + key: key, + flag: flag, + perm: perm, + }, nil +} + +type fileWriter struct { + fs *gcsFS + name, bucket, key string + flag int + perm os.FileMode + + buf bytes.Buffer + + mu sync.Mutex + closed bool +} + +func (w *fileWriter) Write(p []byte) (n int, err error) { + if len(p)+w.buf.Len() > maxSize { + return 0, &os.PathError{ + Op: "Write", + Path: w.name, + Err: errors.New("file too large"), + } + } + return w.buf.Write(p) +} + +func (w *fileWriter) Close() (err error) { + w.mu.Lock() + defer w.mu.Unlock() + if w.closed { + return nil + } + w.closed = true + return w.fs.client.PutObject(&googlestorage.Object{ + Bucket: w.bucket, + Key: w.key, + }, ioutil.NopCloser(bytes.NewReader(w.buf.Bytes()))) +} + +type statInfo struct { + name string + size int64 + isDir bool + modtime time.Time +} + +func (si *statInfo) IsDir() bool { return si.isDir } +func (si *statInfo) ModTime() time.Time { return si.modtime } +func (si *statInfo) Mode() os.FileMode { return 0644 } +func (si *statInfo) Name() string { return path.Base(si.name) } +func (si *statInfo) Size() int64 { return si.size } +func (si *statInfo) Sys() interface{} { return nil } + +type file struct { + name string + *bytes.Reader +} + +func (*file) Close() error { return nil } +func (f *file) Name() string { return path.Base(f.name) } +func (f *file) Stat() (os.FileInfo, error) { + panic("Stat not implemented on /gcs/ files yet") +} diff --git a/vendor/github.com/camlistore/camlistore/pkg/wkfs/wkfs.go b/vendor/github.com/camlistore/camlistore/pkg/wkfs/wkfs.go new file mode 100644 index 00000000..502a893c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/pkg/wkfs/wkfs.go @@ -0,0 +1,132 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package wkfs implements the pluggable "well-known filesystem" abstraction layer. +// +// Instead of accessing files directly through the operating system +// using os.Open or os.Stat, code should use wkfs.Open or wkfs.Stat, +// which first try to intercept paths at well-known top-level +// directories representing previously-registered mount types, +// otherwise fall through to the operating system paths. +// +// Example of top-level well-known directories that might be +// registered include /gcs/bucket/object for Google Cloud Storage or +// /s3/bucket/object for AWS S3. +package wkfs + +import ( + "io" + "io/ioutil" + "os" + "strings" +) + +type File interface { + io.Reader + io.ReaderAt + io.Closer + io.Seeker + Name() string + Stat() (os.FileInfo, error) +} + +type FileWriter interface { + io.Writer + io.Closer +} + +func Open(name string) (File, error) { return fs(name).Open(name) } +func Stat(name string) (os.FileInfo, error) { return fs(name).Stat(name) } +func Lstat(name string) (os.FileInfo, error) { return fs(name).Lstat(name) } +func MkdirAll(path string, perm os.FileMode) error { return fs(path).MkdirAll(path, perm) } +func OpenFile(name string, flag int, perm os.FileMode) (FileWriter, error) { + return fs(name).OpenFile(name, flag, perm) +} +func Create(name string) (FileWriter, error) { + // like os.Create but WRONLY instead of RDWR because we don't + // expose a Reader here. + return OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) +} + +func fs(name string) FileSystem { + for pfx, fs := range wkFS { + if strings.HasPrefix(name, pfx) { + return fs + } + } + return osFS{} +} + +type osFS struct{} + +func (osFS) Open(name string) (File, error) { return os.Open(name) } +func (osFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) } +func (osFS) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) } +func (osFS) MkdirAll(path string, perm os.FileMode) error { return os.MkdirAll(path, perm) } +func (osFS) OpenFile(name string, flag int, perm os.FileMode) (FileWriter, error) { + return os.OpenFile(name, flag, perm) +} + +type FileSystem interface { + Open(name string) (File, error) + OpenFile(name string, flag int, perm os.FileMode) (FileWriter, error) + Stat(name string) (os.FileInfo, error) + Lstat(name string) (os.FileInfo, error) + MkdirAll(path string, perm os.FileMode) error +} + +// well-known filesystems +var wkFS = map[string]FileSystem{} + +// RegisterFS registers a well-known filesystem. It intercepts +// anything beginning with prefix (which must start and end with a +// forward slash) and forwards it to fs. +func RegisterFS(prefix string, fs FileSystem) { + if !strings.HasPrefix(prefix, "/") || !strings.HasSuffix(prefix, "/") { + panic("bogus prefix: " + prefix) + } + if _, dup := wkFS[prefix]; dup { + panic("duplication registration of " + prefix) + } + wkFS[prefix] = fs +} + +// WriteFile writes data to a file named by filename. +// If the file does not exist, WriteFile creates it with permissions perm; +// otherwise WriteFile truncates it before writing. +func WriteFile(filename string, data []byte, perm os.FileMode) error { + f, err := OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) + if err != nil { + return err + } + n, err := f.Write(data) + if err == nil && n < len(data) { + err = io.ErrShortWrite + } + if err1 := f.Close(); err == nil { + err = err1 + } + return err +} + +func ReadFile(filename string) ([]byte, error) { + f, err := Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + return ioutil.ReadAll(f) +} diff --git a/vendor/github.com/camlistore/camlistore/server/.gitignore b/vendor/github.com/camlistore/camlistore/server/.gitignore new file mode 100644 index 00000000..f3c7a7c5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/.gitignore @@ -0,0 +1 @@ +Makefile diff --git a/vendor/github.com/camlistore/camlistore/server/appengine/README b/vendor/github.com/camlistore/camlistore/server/appengine/README new file mode 100644 index 00000000..3512b0e2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/appengine/README @@ -0,0 +1,9 @@ +We typically just use the "devcam appengine" command to hack on this code. (To build devcam: go install ../../dev/devcam). + +But to run by hand: + +$ dev_appserver.py --high_replication . + +Other useful flags: + -a 0.0.0.0 (listen on all addresses) + -c (wipe the datastore) diff --git a/vendor/github.com/camlistore/camlistore/server/appengine/app.yaml b/vendor/github.com/camlistore/camlistore/server/appengine/app.yaml new file mode 100644 index 00000000..bb8cd554 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/appengine/app.yaml @@ -0,0 +1,8 @@ +application: camlistore +version: 1 +runtime: go +api_version: go1 + +handlers: +- url: /.* + script: _go_app diff --git a/vendor/github.com/camlistore/camlistore/server/appengine/build_test.go b/vendor/github.com/camlistore/camlistore/server/appengine/build_test.go new file mode 100644 index 00000000..4bfcf55b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/appengine/build_test.go @@ -0,0 +1,119 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package appengine_test + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "camlistore.org/pkg/osutil" +) + +func TestAppEngineBuilds(t *testing.T) { + t.Skip("Currently broken until App Engine supports Go 1.3") + if runtime.GOOS == "windows" { + t.Skip("skipping on Windows; don't want to deal with escaping backslashes") + } + camRoot, err := osutil.GoPackagePath("camlistore.org") + if err != nil { + t.Errorf("No camlistore.org package in GOPATH: %v", err) + } + sdkLink := filepath.Join(camRoot, "appengine-sdk") + if _, err := os.Lstat(sdkLink); os.IsNotExist(err) { + t.Skipf("Skipping test; no App Engine SDK symlink at %s pointing to App Engine SDK.", sdkLink) + } + sdk, err := os.Readlink(sdkLink) + if err != nil { + t.Fatal(err) + } + + td, err := ioutil.TempDir("", "camli-appengine") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(td) + + gab := filepath.Join(sdk, "goroot", "bin", "go-app-builder") + if runtime.GOOS == "windows" { + gab += ".exe" + } + + appBase := filepath.Join(camRoot, "server", "appengine") + f, err := os.Open(filepath.Join(appBase, "camli")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + srcFilesAll, err := f.Readdirnames(-1) + if err != nil { + t.Fatal(err) + } + + appenginePkg := filepath.Join(sdk, "goroot", "pkg", runtime.GOOS+"_"+runtime.GOARCH+"_appengine") + cmd := exec.Command(gab, + "-app_base", appBase, + "-arch", archChar(), + "-binary_name", "_go_app", + "-dynamic", + "-extra_imports", "appengine_internal/init", + "-goroot", filepath.Join(sdk, "goroot"), + "-gcflags", "-I,"+appenginePkg, + "-ldflags", "-L,"+appenginePkg, + "-nobuild_files", "^^$", + "-unsafe", + "-work_dir", td, + "-gopath", os.Getenv("GOPATH"), + // "-v", + ) + for _, f := range srcFilesAll { + if strings.HasSuffix(f, ".go") { + cmd.Args = append(cmd.Args, filepath.Join("camli", f)) + } + } + for _, pair := range os.Environ() { + if strings.HasPrefix(pair, "GOROOT=") { + continue + } + cmd.Env = append(cmd.Env, pair) + } + + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Error: %v\n%s", err, out) + } + target := filepath.Join(td, "_go_app") + if _, err := os.Stat(target); os.IsNotExist(err) { + t.Errorf("target binary doesn't exist") + } +} + +func archChar() string { + switch runtime.GOARCH { + case "386": + return "8" + case "amd64": + return "6" + case "arm": + return "5" + } + panic("unknown arch " + runtime.GOARCH) +} diff --git a/vendor/github.com/camlistore/camlistore/server/appengine/camli/aeindex.go b/vendor/github.com/camlistore/camlistore/server/appengine/camli/aeindex.go new file mode 100644 index 00000000..234936f6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/appengine/camli/aeindex.go @@ -0,0 +1,227 @@ +// +build appengine + +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package appengine + +import ( + "io" + + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/index" + "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/sorted" + + "appengine" + "appengine/datastore" +) + +const indexDebug = false + +var ( + indexRowKind = "IndexRow" +) + +// A row of the index. Keyed by "|" +type indexRowEnt struct { + Value []byte +} + +type indexStorage struct { + ns string +} + +func (is *indexStorage) key(c appengine.Context, key string) *datastore.Key { + return datastore.NewKey(c, indexRowKind, key, 0, datastore.NewKey(c, indexRowKind, is.ns, 0, nil)) +} + +func (is *indexStorage) BeginBatch() sorted.BatchMutation { + return sorted.NewBatchMutation() +} + +func (is *indexStorage) CommitBatch(bm sorted.BatchMutation) error { + type mutationser interface { + Mutations() []sorted.Mutation + } + var muts []sorted.Mutation + if m, ok := bm.(mutationser); ok { + muts = m.Mutations() + } else { + panic("unexpected type") + } + tryFunc := func(c appengine.Context) error { + for _, m := range muts { + dk := is.key(c, m.Key()) + if m.IsDelete() { + if err := datastore.Delete(c, dk); err != nil { + return err + } + } else { + // A put. + ent := &indexRowEnt{ + Value: []byte(m.Value()), + } + if _, err := datastore.Put(c, dk, ent); err != nil { + return err + } + } + } + return nil + } + c := ctxPool.Get() + defer c.Return() + return datastore.RunInTransaction(c, tryFunc, crossGroupTransaction) +} + +func (is *indexStorage) Get(key string) (string, error) { + c := ctxPool.Get() + defer c.Return() + row := new(indexRowEnt) + err := datastore.Get(c, is.key(c, key), row) + if indexDebug { + c.Infof("indexStorage.Get(%q) = %q, %v", key, row.Value, err) + } + if err != nil { + if err == datastore.ErrNoSuchEntity { + err = sorted.ErrNotFound + } + return "", err + } + return string(row.Value), nil +} + +func (is *indexStorage) Set(key, value string) error { + c := ctxPool.Get() + defer c.Return() + row := &indexRowEnt{ + Value: []byte(value), + } + _, err := datastore.Put(c, is.key(c, key), row) + return err +} + +func (is *indexStorage) Delete(key string) error { + c := ctxPool.Get() + defer c.Return() + return datastore.Delete(c, is.key(c, key)) +} + +func (is *indexStorage) Find(start, end string) sorted.Iterator { + c := ctxPool.Get() + if indexDebug { + c.Infof("IndexStorage Find(%q, %q)", start, end) + } + it := &iter{ + is: is, + cl: c, + after: start, + endKey: end, + nsk: datastore.NewKey(c, indexRowKind, is.ns, 0, nil), + } + it.Closer = &onceCloser{fn: func() { + c.Return() + it.nsk = nil + }} + return it +} + +func (is *indexStorage) Close() error { return nil } + +type iter struct { + cl ContextLoan + after string + endKey string // optional + io.Closer + nsk *datastore.Key + is *indexStorage + + it *datastore.Iterator + n int // rows seen for this batch + + key, value string + end bool +} + +func (it *iter) Next() bool { + if it.nsk == nil { + // already closed + return false + } + if it.it == nil { + q := datastore.NewQuery(indexRowKind).Filter("__key__>=", it.is.key(it.cl, it.after)) + if it.endKey != "" { + q = q.Filter("__key__<", it.is.key(it.cl, it.endKey)) + } + it.it = q.Run(it.cl) + it.n = 0 + } + var ent indexRowEnt + key, err := it.it.Next(&ent) + if indexDebug { + it.cl.Infof("For after %q; key = %#v, err = %v", it.after, key, err) + } + if err == datastore.Done { + if it.n == 0 { + return false + } + return it.Next() + } + if err != nil { + it.cl.Warningf("Error iterating over index after %q: %v", it.after, err) + return false + } + it.n++ + it.key = key.StringID() + it.value = string(ent.Value) + it.after = it.key + return true +} + +func (it *iter) Key() string { return it.key } +func (it *iter) Value() string { return it.value } + +// TODO(bradfit): optimize the string<->[]byte copies in this iterator, as done in the other +// sorted.KeyValue iterators. +func (it *iter) KeyBytes() []byte { return []byte(it.key) } +func (it *iter) ValueBytes() []byte { return []byte(it.value) } + +func indexFromConfig(ld blobserver.Loader, config jsonconfig.Obj) (storage blobserver.Storage, err error) { + is := &indexStorage{} + var ( + blobPrefix = config.RequiredString("blobSource") + ns = config.OptionalString("namespace", "") + ) + if err := config.Validate(); err != nil { + return nil, err + } + sto, err := ld.GetStorage(blobPrefix) + if err != nil { + return nil, err + } + is.ns, err = sanitizeNamespace(ns) + if err != nil { + return nil, err + } + + ix, err := index.New(is) + if err != nil { + return nil, err + } + ix.BlobSource = sto + ix.KeyFetcher = ix.BlobSource // TODO(bradfitz): global search? something else? + return ix, nil +} diff --git a/vendor/github.com/camlistore/camlistore/server/appengine/camli/common.go b/vendor/github.com/camlistore/camlistore/server/appengine/camli/common.go new file mode 100644 index 00000000..0fe97104 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/appengine/camli/common.go @@ -0,0 +1,39 @@ +// +build appengine + +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package appengine + +import ( + "fmt" + "strings" +) + +func sanitizeNamespace(ns string) (outns string, err error) { + outns = ns + switch { + case strings.Contains(ns, "|"): + err = fmt.Errorf("no pipe allowed in namespace %q", ns) + case strings.Contains(ns, "\x00"): + err = fmt.Errorf("no zero byte allowed in namespace %q", ns) + case ns == "-": + err = fmt.Errorf("reserved namespace %q", ns) + case ns == "": + outns = "-" + } + return +} diff --git a/vendor/github.com/camlistore/camlistore/server/appengine/camli/contextpool.go b/vendor/github.com/camlistore/camlistore/server/appengine/camli/contextpool.go new file mode 100644 index 00000000..175e7bd1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/appengine/camli/contextpool.go @@ -0,0 +1,115 @@ +// +build appengine + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package appengine + +import ( + "sync" + + "appengine" +) + +type ContextPool struct { + mu sync.Mutex // guards live + + // Live HTTP requests + live map[appengine.Context]*sync.WaitGroup +} + +// HandlerBegin notes that the provided context is beginning and it can be +// shared until HandlerEnd is called. +func (p *ContextPool) HandlerBegin(c appengine.Context) { + p.mu.Lock() + defer p.mu.Unlock() + if p.live == nil { + p.live = make(map[appengine.Context]*sync.WaitGroup) + } + if _, ok := p.live[c]; ok { + // dup; ignore. + return + } + p.live[c] = new(sync.WaitGroup) +} + +// HandlerEnd notes that the provided context is about to go out of service, +// removes it from the pool of available contexts, and blocks until everybody +// is done using it. +func (p *ContextPool) HandlerEnd(c appengine.Context) { + p.mu.Lock() + wg := p.live[c] + delete(p.live, c) + p.mu.Unlock() + if wg != nil { + wg.Wait() + } +} + +// A ContextLoan is a superset of a Context, so can passed anywhere +// that needs an appengine.Context. +// +// When done, Return it. +type ContextLoan interface { + appengine.Context + + // Return returns the Context to the pool. + // Return must be called exactly once. + Return() +} + +// Get returns a valid App Engine context from some active HTTP request +// which is guaranteed to stay valid. Be sure to return it. +// +// Typical use: +// ctx := pool.Get() +// defer ctx.Return() +func (p *ContextPool) Get() ContextLoan { + p.mu.Lock() + defer p.mu.Unlock() + + // Pick a random active context. TODO: pick the "right" one, + // using some TLS-like-guess/hack from runtume.Stacks. + var c appengine.Context + var wg *sync.WaitGroup + for c, wg = range p.live { + break + } + if c == nil { + panic("ContextPool.Get called with no live HTTP requests") + } + wg.Add(1) + cl := &contextLoan{Context: c, wg: wg} + // TODO: set warning finalizer on this? + return cl +} + +type contextLoan struct { + appengine.Context + + mu sync.Mutex + wg *sync.WaitGroup +} + +func (cl *contextLoan) Return() { + cl.mu.Lock() + defer cl.mu.Unlock() + if cl.wg == nil { + panic("Return called twice") + } + cl.wg.Done() + cl.wg = nil +} diff --git a/vendor/github.com/camlistore/camlistore/server/appengine/camli/main.go b/vendor/github.com/camlistore/camlistore/server/appengine/camli/main.go new file mode 100644 index 00000000..62572221 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/appengine/camli/main.go @@ -0,0 +1,108 @@ +// +build appengine + +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package appengine + +import ( + "fmt" + "net/http" + "sync" + + "appengine" + + "camlistore.org/pkg/blobserver" // storage interface definition + _ "camlistore.org/pkg/blobserver/cond" + _ "camlistore.org/pkg/blobserver/replica" + _ "camlistore.org/pkg/blobserver/shard" + _ "camlistore.org/pkg/server" // handlers: UI, publish, thumbnailing, etc + "camlistore.org/pkg/serverinit" // wiring up the world from a JSON description + + // TODO(bradfitz): uncomment these config setup + // Both require an App Engine context to make HTTP requests too. + //_ "camlistore.org/pkg/blobserver/remote" + //_ "camlistore.org/pkg/blobserver/s3" +) + +// lazyInit is our root handler for App Engine. We don't have an App Engine +// context until the first request and we need that context to figure out +// our serving URL. So we use this to defer setting up our environment until +// the first request. +type lazyInit struct { + mu sync.Mutex + ready bool + mux *http.ServeMux +} + +func (li *lazyInit) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c := appengine.NewContext(r) + ctxPool.HandlerBegin(c) + defer ctxPool.HandlerEnd(c) + + li.mu.Lock() + if !li.ready { + li.ready = realInit(w, r) + } + li.mu.Unlock() + if li.ready { + li.mux.ServeHTTP(w, r) + } +} + +var ctxPool ContextPool + +var root = new(lazyInit) + +func init() { + // TODO(bradfitz): rename some of this to be consistent + blobserver.RegisterStorageConstructor("appengine", blobserver.StorageConstructor(newFromConfig)) + blobserver.RegisterStorageConstructor("aeindex", blobserver.StorageConstructor(indexFromConfig)) + http.Handle("/", root) +} + +func realInit(w http.ResponseWriter, r *http.Request) bool { + ctx := appengine.NewContext(r) + + errf := func(format string, args ...interface{}) bool { + ctx.Errorf("In init: "+format, args...) + http.Error(w, fmt.Sprintf(format, args...), 500) + return false + } + + config, err := serverinit.Load("./config.json") + if err != nil { + return errf("Could not load server config: %v", err) + } + + // Update the config to use the URL path derived from the first App Engine request. + // TODO(bslatkin): Support hostnames that aren't x.appspot.com + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + + baseURL := fmt.Sprintf("%s://%s/", scheme, appengine.DefaultVersionHostname(ctx)) + ctx.Infof("baseurl = %q", baseURL) + + root.mux = http.NewServeMux() + _, err = config.InstallHandlers(root.mux, baseURL, false, r) + if err != nil { + return errf("Error installing handlers: %v", err) + } + + return true +} diff --git a/vendor/github.com/camlistore/camlistore/server/appengine/camli/ownerauth.go b/vendor/github.com/camlistore/camlistore/server/appengine/camli/ownerauth.go new file mode 100644 index 00000000..95b6a000 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/appengine/camli/ownerauth.go @@ -0,0 +1,82 @@ +// +build appengine + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package appengine + +import ( + "net/http" + + "camlistore.org/pkg/auth" + "camlistore.org/pkg/httputil" + + "appengine" + "appengine/user" +) + +func init() { + auth.RegisterAuth("appengine_app_owner", newOwnerAuth) +} + +type ownerAuth struct { + fallback auth.AuthMode +} + +var _ auth.UnauthorizedSender = (*ownerAuth)(nil) + +func newOwnerAuth(arg string) (auth.AuthMode, error) { + m := &ownerAuth{} + if arg != "" { + f, err := auth.FromConfig(arg) + if err != nil { + return nil, err + } + m.fallback = f + } + return m, nil +} + +func (o *ownerAuth) AllowedAccess(req *http.Request) auth.Operation { + c := appengine.NewContext(req) + if user.IsAdmin(c) { + return auth.OpAll + } + if o.fallback != nil { + return o.fallback.AllowedAccess(req) + } + return 0 +} + +func (o *ownerAuth) SendUnauthorized(rw http.ResponseWriter, req *http.Request) bool { + if !httputil.IsGet(req) { + return false + } + c := appengine.NewContext(req) + loginURL, err := user.LoginURL(c, req.URL.String()) + if err != nil { + c.Errorf("Fetching LoginURL: %v", err) + return false + } + http.Redirect(rw, req, loginURL, http.StatusFound) + return true +} + +func (o *ownerAuth) AddAuthHeader(req *http.Request) { + // TODO(bradfitz): split the auth interface into a server part + // and a client part. + panic("Not applicable. should not be called.") +} diff --git a/vendor/github.com/camlistore/camlistore/server/appengine/camli/storage.go b/vendor/github.com/camlistore/camlistore/server/appengine/camli/storage.go new file mode 100644 index 00000000..25d8a911 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/appengine/camli/storage.go @@ -0,0 +1,377 @@ +// +build appengine + +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package appengine + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "sync" + + "appengine" + "appengine/blobstore" + "appengine/datastore" + + "camlistore.org/pkg/blob" + "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/context" + "camlistore.org/pkg/jsonconfig" +) + +const ( + blobKind = "Blob" + memKind = "NsBlobMember" // blob membership in a namespace +) + +var _ blobserver.Storage = (*appengineStorage)(nil) + +type appengineStorage struct { + namespace string // never empty; config initializes to at least "-" +} + +// blobEnt is stored once per unique blob, keyed by blobref. +type blobEnt struct { + Size int64 `datastore:"Size,noindex"` + BlobKey appengine.BlobKey `datastore:"BlobKey,noindex"` + Namespaces string `datastore:"Namespaces,noindex"` // |-separated string of namespaces + + // TODO(bradfitz): IsCamliSchemaBlob bool? ... probably want + // on enumeration (memEnt) too. +} + +// memEnt is stored once per blob in a namespace, keyed by "ns|blobref" +type memEnt struct { + Size int64 `datastore:"Size,noindex"` +} + +func byteDecSize(b []byte) (int64, error) { + var size int64 + n, err := fmt.Fscanf(bytes.NewBuffer(b), "%d", &size) + if n != 1 || err != nil { + return 0, fmt.Errorf("invalid Size column in datastore: %q", string(b)) + } + return size, nil +} + +func (b *blobEnt) inNamespace(ns string) (out bool) { + for _, in := range strings.Split(b.Namespaces, "|") { + if ns == in { + return true + } + } + return false +} + +func entKey(c appengine.Context, br blob.Ref) *datastore.Key { + return datastore.NewKey(c, blobKind, br.String(), 0, nil) +} + +func (s *appengineStorage) memKey(c appengine.Context, br blob.Ref) *datastore.Key { + return datastore.NewKey(c, memKind, fmt.Sprintf("%s|%s", s.namespace, br.String()), 0, nil) +} + +func fetchEnt(c appengine.Context, br blob.Ref) (*blobEnt, error) { + row := new(blobEnt) + err := datastore.Get(c, entKey(c, br), row) + if err != nil { + return nil, err + } + return row, nil +} + +func newFromConfig(ld blobserver.Loader, config jsonconfig.Obj) (storage blobserver.Storage, err error) { + sto := &appengineStorage{ + namespace: config.OptionalString("namespace", ""), + } + if err := config.Validate(); err != nil { + return nil, err + } + sto.namespace, err = sanitizeNamespace(sto.namespace) + if err != nil { + return nil, err + } + return sto, nil +} + +func (sto *appengineStorage) Fetch(br blob.Ref) (file io.ReadCloser, size uint32, err error) { + loan := ctxPool.Get() + ctx := loan + defer func() { + if loan != nil { + loan.Return() + } + }() + + row, err := fetchEnt(ctx, br) + if err == datastore.ErrNoSuchEntity { + err = os.ErrNotExist + return + } + if err != nil { + return + } + if !row.inNamespace(sto.namespace) { + err = os.ErrNotExist + return + } + + closeLoan := loan + var c io.Closer = &onceCloser{fn: func() { closeLoan.Return() }} + loan = nil // take it, so it's not defer-closed + + reader := blobstore.NewReader(ctx, appengine.BlobKey(string(row.BlobKey))) + type readCloser struct { + io.Reader + io.Closer + } + return readCloser{reader, c}, uint32(row.Size), nil +} + +type onceCloser struct { + once sync.Once + fn func() +} + +func (oc *onceCloser) Close() error { + oc.once.Do(oc.fn) + return nil +} + +var crossGroupTransaction = &datastore.TransactionOptions{XG: true} + +func (sto *appengineStorage) ReceiveBlob(br blob.Ref, in io.Reader) (sb blob.SizedRef, err error) { + loan := ctxPool.Get() + defer loan.Return() + ctx := loan + + var b bytes.Buffer + written, err := io.Copy(&b, in) + if err != nil { + return + } + + // bkey is non-empty once we've uploaded the blob. + var bkey appengine.BlobKey + + // uploadBlob uploads the blob, unless it's already been done. + uploadBlob := func(ctx appengine.Context) error { + if len(bkey) > 0 { + return nil // already done in previous transaction attempt + } + bw, err := blobstore.Create(ctx, "application/octet-stream") + if err != nil { + return err + } + _, err = io.Copy(bw, &b) + if err != nil { + // TODO(bradfitz): try to clean up; close it, see if we can find the key, delete it. + ctx.Errorf("blobstore Copy error: %v", err) + return err + } + err = bw.Close() + if err != nil { + // TODO(bradfitz): try to clean up; see if we can find the key, delete it. + ctx.Errorf("blobstore Close error: %v", err) + return err + } + k, err := bw.Key() + if err == nil { + bkey = k + } + return err + } + + tryFunc := func(tc appengine.Context) error { + row, err := fetchEnt(tc, br) + switch err { + case datastore.ErrNoSuchEntity: + if err := uploadBlob(tc); err != nil { + tc.Errorf("uploadBlob failed: %v", err) + return err + } + row = &blobEnt{ + Size: written, + BlobKey: bkey, + Namespaces: sto.namespace, + } + _, err = datastore.Put(tc, entKey(tc, br), row) + if err != nil { + return err + } + case nil: + if row.inNamespace(sto.namespace) { + // Nothing to do + return nil + } + row.Namespaces = row.Namespaces + "|" + sto.namespace + _, err = datastore.Put(tc, entKey(tc, br), row) + if err != nil { + return err + } + default: + return err + } + + // Add membership row + _, err = datastore.Put(tc, sto.memKey(tc, br), &memEnt{ + Size: written, + }) + return err + } + err = datastore.RunInTransaction(ctx, tryFunc, crossGroupTransaction) + if err != nil { + if len(bkey) > 0 { + // If we just created this blob but we + // ultimately failed, try our best to delete + // it so it's not orphaned. + blobstore.Delete(ctx, bkey) + } + return + } + return blob.SizedRef{br, uint32(written)}, nil +} + +// NOTE(bslatkin): No fucking clue if this works. +func (sto *appengineStorage) RemoveBlobs(blobs []blob.Ref) error { + loan := ctxPool.Get() + defer loan.Return() + ctx := loan + + tryFunc := func(tc appengine.Context, br blob.Ref) error { + // TODO(bslatkin): Make the DB gets in this a multi-get. + // Remove the namespace from the blobEnt + row, err := fetchEnt(tc, br) + switch err { + case datastore.ErrNoSuchEntity: + // Doesn't exist, that means there should be no memEnt, but let's be + // paranoid and double check anyways. + case nil: + // blobEnt exists, remove our namespace from it if possible. + newNS := []string{} + for _, val := range strings.Split(string(row.Namespaces), "|") { + if val != sto.namespace { + newNS = append(newNS, val) + } + } + if v := strings.Join(newNS, "|"); v != row.Namespaces { + row.Namespaces = v + _, err = datastore.Put(tc, entKey(tc, br), row) + if err != nil { + return err + } + } + default: + return err + } + + // Blindly delete the memEnt. + err = datastore.Delete(tc, sto.memKey(tc, br)) + return err + } + + for _, br := range blobs { + ret := datastore.RunInTransaction( + ctx, + func(tc appengine.Context) error { + return tryFunc(tc, br) + }, + crossGroupTransaction) + if ret != nil { + return ret + } + } + return nil +} + +func (sto *appengineStorage) StatBlobs(dest chan<- blob.SizedRef, blobs []blob.Ref) error { + loan := ctxPool.Get() + defer loan.Return() + ctx := loan + + var ( + keys = make([]*datastore.Key, 0, len(blobs)) + out = make([]interface{}, 0, len(blobs)) + errs = make([]error, len(blobs)) + ) + for _, br := range blobs { + keys = append(keys, sto.memKey(ctx, br)) + out = append(out, new(memEnt)) + } + err := datastore.GetMulti(ctx, keys, out) + if merr, ok := err.(appengine.MultiError); ok { + errs = []error(merr) + err = nil + } + if err != nil { + return err + } + for i, br := range blobs { + thisErr := errs[i] + if thisErr == datastore.ErrNoSuchEntity { + continue + } + if thisErr != nil { + err = errs[i] // just return last one found? + continue + } + ent := out[i].(*memEnt) + dest <- blob.SizedRef{br, uint32(ent.Size)} + } + return err +} + +func (sto *appengineStorage) EnumerateBlobs(ctx *context.Context, dest chan<- blob.SizedRef, after string, limit int) error { + defer close(dest) + + loan := ctxPool.Get() + defer loan.Return() + actx := loan + + prefix := sto.namespace + "|" + keyBegin := datastore.NewKey(actx, memKind, prefix+after, 0, nil) + keyEnd := datastore.NewKey(actx, memKind, sto.namespace+"~", 0, nil) + + q := datastore.NewQuery(memKind).Limit(int(limit)).Filter("__key__>", keyBegin).Filter("__key__<", keyEnd) + it := q.Run(actx) + var row memEnt + for { + key, err := it.Next(&row) + if err == datastore.Done { + break + } + if err != nil { + return err + } + select { + case dest <- blob.SizedRef{blob.ParseOrZero(key.StringID()[len(prefix):]), uint32(row.Size)}: + case <-ctx.Done(): + return context.ErrCanceled + } + } + return nil +} + +// TODO(bslatkin): sync does not work on App Engine yet because there are no +// background threads to do the sync loop. The plan is to break the +// syncer code up into two parts: 1) accepts notifications of new blobs to +// sync, 2) does one unit of work enumerating recent blobs and syncing them. +// In App Engine land, 1) will result in a task to be enqueued, and 2) will +// be called from within that queue context. diff --git a/vendor/github.com/camlistore/camlistore/server/appengine/config.json b/vendor/github.com/camlistore/camlistore/server/appengine/config.json new file mode 100644 index 00000000..c3ed7390 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/appengine/config.json @@ -0,0 +1,96 @@ +{ "_for-emacs": "-*- mode: js2;-*-", + "handlerConfig": true, + "auth": "appengine_app_owner:userpass:camlistore:pass3179", + "prefixes": { + + "/": { + "handler": "root", + "handlerArgs": { + "ownerName": "TODO:AppEngineOwnerName", + "blobRoot": "/bs-and-maybe-also-index/", + "helpRoot": "/help/", + "statusRoot": "/status/", + "searchRoot": "/my-search/", + "stealth": false + } + }, + + "/ui/": { + "handler": "ui", + "handlerArgs": { + "jsonSignRoot": "/sighelper/" + } + }, + + "/status/": { + "handler": "status" + }, + + "/bs-and-maybe-also-index/": { + "handler": "storage-cond", + "handlerArgs": { + "write": { + "if": "isSchema", + "then": "/bs-and-index/", + "else": "/bs/" + }, + "read": "/bs/" + } + }, + + "/bs-and-index/": { + "handler": "storage-replica", + "handlerArgs": { + "backends": ["/bs/", "/indexer/"] + } + }, + + "/sighelper/": { + "handler": "jsonsign", + "handlerArgs": { + "secretRing": "test-secring.gpg", + "keyId": "26F5ABDA", + "publicKeyDest": "/bs/" + } + }, + + "/bs/": { + "handler": "storage-appengine", + "handlerArgs": { + } + }, + + "/bs2/": { + "handler": "storage-appengine", + "handlerArgs": { + "namespace": "two" + } + }, + + "/sync/": { + "enabled": false, + "handler": "sync", + "handlerArgs": { + "from": "/bs/", + "to": "/indexer/" + } + }, + + "/indexer/": { + "handler": "storage-aeindex", + "handlerArgs": { + "namespace": "idx1", + "blobSource": "/bs/" + } + }, + + "/my-search/": { + "handler": "search", + "handlerArgs": { + "index": "/indexer/", + "owner": "sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4" + } + } + + } +} diff --git a/vendor/github.com/camlistore/camlistore/server/appengine/test-secring.gpg b/vendor/github.com/camlistore/camlistore/server/appengine/test-secring.gpg new file mode 120000 index 00000000..9518746e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/appengine/test-secring.gpg @@ -0,0 +1 @@ +../../pkg/jsonsign/testdata/test-secring.gpg \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/.gitignore b/vendor/github.com/camlistore/camlistore/server/camlistored/.gitignore new file mode 100644 index 00000000..48821930 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/.gitignore @@ -0,0 +1 @@ +camlistored diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/README b/vendor/github.com/camlistore/camlistore/server/camlistored/README new file mode 100644 index 00000000..6d6695b1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/README @@ -0,0 +1,7 @@ +This is the main Camlistore server. + +See also: + - The storage interface is in /pkg/blobserver + - The storage implementations are under that e.g. /pkg/blobserver/localdisk + - The HTTP handlers are implemented in /pkg/blobserver/handlers + - The UI code is in /server/camlistored/ui diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/camlistored.go b/vendor/github.com/camlistore/camlistore/server/camlistored/camlistored.go new file mode 100644 index 00000000..5fa3f9e9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/camlistored.go @@ -0,0 +1,415 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// The camlistored binary is the Camlistore server. +package main + +import ( + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "os/signal" + "path/filepath" + "runtime" + "strconv" + "strings" + "syscall" + "time" + + "camlistore.org/pkg/buildinfo" + "camlistore.org/pkg/env" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/legal/legalprint" + "camlistore.org/pkg/netutil" + "camlistore.org/pkg/osutil" + "camlistore.org/pkg/serverinit" + "camlistore.org/pkg/webserver" + "camlistore.org/pkg/wkfs" + + // VM environments: + "camlistore.org/pkg/osutil/gce" // for init side-effects + LogWriter + + // Storage options: + _ "camlistore.org/pkg/blobserver/blobpacked" + _ "camlistore.org/pkg/blobserver/cond" + _ "camlistore.org/pkg/blobserver/diskpacked" + _ "camlistore.org/pkg/blobserver/encrypt" + _ "camlistore.org/pkg/blobserver/google/cloudstorage" + _ "camlistore.org/pkg/blobserver/google/drive" + _ "camlistore.org/pkg/blobserver/localdisk" + _ "camlistore.org/pkg/blobserver/mongo" + _ "camlistore.org/pkg/blobserver/proxycache" + _ "camlistore.org/pkg/blobserver/remote" + _ "camlistore.org/pkg/blobserver/replica" + _ "camlistore.org/pkg/blobserver/s3" + _ "camlistore.org/pkg/blobserver/shard" + // Indexers: (also present themselves as storage targets) + "camlistore.org/pkg/index" + // KeyValue implementations: + _ "camlistore.org/pkg/sorted/kvfile" + _ "camlistore.org/pkg/sorted/leveldb" + _ "camlistore.org/pkg/sorted/mongo" + _ "camlistore.org/pkg/sorted/mysql" + _ "camlistore.org/pkg/sorted/postgres" + "camlistore.org/pkg/sorted/sqlite" // for sqlite.CompiledIn() + + // Handlers: + _ "camlistore.org/pkg/search" + _ "camlistore.org/pkg/server" // UI, publish, etc + + // Importers: + _ "camlistore.org/pkg/importer/allimporters" +) + +var ( + flagVersion = flag.Bool("version", false, "show version") + flagConfigFile = flag.String("configfile", "", + "Config file to use, relative to the Camlistore configuration directory root. "+ + "If blank, the default is used or auto-generated. "+ + "If it starts with 'http:' or 'https:', it is fetched from the network.") + flagListen = flag.String("listen", "", "host:port to listen on, or :0 to auto-select. If blank, the value in the config will be used instead.") + flagOpenBrowser = flag.Bool("openbrowser", true, "Launches the UI on startup") + flagReindex = flag.Bool("reindex", false, "Reindex all blobs on startup") + flagPollParent bool +) + +func init() { + if debug, _ := strconv.ParseBool(os.Getenv("CAMLI_DEBUG")); debug { + flag.BoolVar(&flagPollParent, "pollparent", false, "Camlistored regularly polls its parent process to detect if it has been orphaned, and terminates in that case. Mainly useful for tests.") + } +} + +func exitf(pattern string, args ...interface{}) { + if !strings.HasSuffix(pattern, "\n") { + pattern = pattern + "\n" + } + fmt.Fprintf(os.Stderr, pattern, args...) + osExit(1) +} + +func slurpURL(urls string, limit int64) ([]byte, error) { + res, err := http.Get(urls) + if err != nil { + return nil, err + } + defer res.Body.Close() + return ioutil.ReadAll(io.LimitReader(res.Body, limit)) +} + +// loadConfig returns the server's parsed config file, locating it using the provided arg. +// +// The arg may be of the form: +// - empty, to mean automatic (will write a default high-level config if +// no cloud config is available) +// - a filepath absolute or relative to the user's configuration directory, +// - a URL +func loadConfig(arg string) (conf *serverinit.Config, isNewConfig bool, err error) { + if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") { + contents, err := slurpURL(arg, 256<<10) + if err != nil { + return nil, false, err + } + conf, err = serverinit.Load(contents) + return conf, false, err + } + var absPath string + switch { + case arg == "": + absPath = osutil.UserServerConfigPath() + _, err = wkfs.Stat(absPath) + if err != nil { + if !os.IsNotExist(err) { + return + } + conf, err = serverinit.DefaultEnvConfig() + if err != nil || conf != nil { + return + } + err = wkfs.MkdirAll(osutil.CamliConfigDir(), 0700) + if err != nil { + return + } + log.Printf("Generating template config file %s", absPath) + if err = serverinit.WriteDefaultConfigFile(absPath, sqlite.CompiledIn()); err == nil { + isNewConfig = true + } + } + case filepath.IsAbs(arg): + absPath = arg + default: + absPath = filepath.Join(osutil.CamliConfigDir(), arg) + } + conf, err = serverinit.LoadFile(absPath) + return +} + +// 1) We do not want to force the user to buy a cert. +// 2) We still want our client (camput) to be able to +// verify the cert's authenticity. +// 3) We want to avoid MITM attacks and warnings in +// the browser. +// Using a simple self-signed won't do because of 3), +// as Chrome offers no way to set a self-signed as +// trusted when importing it. (same on android). +// We could have created a self-signed CA (that we +// would import in the browsers) and create another +// cert (signed by that CA) which would be the one +// used in camlistore. +// We're doing even simpler: create a self-signed +// CA and directly use it as a self-signed cert +// (and install it as a CA in the browsers). +// 2) is satisfied by doing our own checks, +// See pkg/client +func setupTLS(ws *webserver.Server, config *serverinit.Config, hostname string) { + cert, key := config.OptionalString("httpsCert", ""), config.OptionalString("httpsKey", "") + if !config.OptionalBool("https", true) { + return + } + if (cert != "") != (key != "") { + exitf("httpsCert and httpsKey must both be either present or absent") + } + + defCert := osutil.DefaultTLSCert() + defKey := osutil.DefaultTLSKey() + const hint = "You must add this certificate's fingerprint to your client's trusted certs list to use it. Like so:\n\"trustedCerts\": [\"%s\"]," + if cert == defCert && key == defKey { + _, err1 := wkfs.Stat(cert) + _, err2 := wkfs.Stat(key) + if err1 != nil || err2 != nil { + if os.IsNotExist(err1) || os.IsNotExist(err2) { + sig, err := httputil.GenSelfTLSFiles(hostname, defCert, defKey) + if err != nil { + exitf("Could not generate self-signed TLS cert: %q", err) + } + log.Printf(hint, sig) + } else { + exitf("Could not stat cert or key: %q, %q", err1, err2) + } + } + } + // Always generate new certificates if the config's httpsCert and httpsKey are empty. + if cert == "" && key == "" { + sig, err := httputil.GenSelfTLSFiles(hostname, defCert, defKey) + if err != nil { + exitf("Could not generate self signed creds: %q", err) + } + log.Printf(hint, sig) + cert = defCert + key = defKey + } + data, err := wkfs.ReadFile(cert) + if err != nil { + exitf("Failed to read pem certificate: %s", err) + } + sig, err := httputil.CertFingerprint(data) + if err != nil { + exitf("certificate error: %v", err) + } + log.Printf("TLS enabled, with SHA-256 certificate fingerprint: %v", sig) + ws.SetTLS(cert, key) +} + +var osExit = os.Exit // testing hook + +func handleSignals(shutdownc <-chan io.Closer) { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGHUP) + signal.Notify(c, syscall.SIGINT) + for { + sig := <-c + sysSig, ok := sig.(syscall.Signal) + if !ok { + log.Fatal("Not a unix signal") + } + switch sysSig { + case syscall.SIGHUP: + log.Print("SIGHUP: restarting camli") + err := osutil.RestartProcess() + if err != nil { + log.Fatal("Failed to restart: " + err.Error()) + } + case syscall.SIGINT: + log.Print("Got SIGINT: shutting down") + donec := make(chan bool) + go func() { + cl := <-shutdownc + if err := cl.Close(); err != nil { + exitf("Error shutting down: %v", err) + } + donec <- true + }() + select { + case <-donec: + log.Printf("Shut down.") + osExit(0) + case <-time.After(2 * time.Second): + exitf("Timeout shutting down. Exiting uncleanly.") + } + default: + log.Fatal("Received another signal, should not happen.") + } + } +} + +// listenAndBaseURL finds the configured, default, or inferred listen address +// and base URL from the command-line flags and provided config. +func listenAndBaseURL(config *serverinit.Config) (listen, baseURL string) { + baseURL = config.OptionalString("baseURL", "") + listen = *flagListen + listenConfig := config.OptionalString("listen", "") + // command-line takes priority over config + if listen == "" { + listen = listenConfig + if listen == "" { + exitf("\"listen\" needs to be specified either in the config or on the command line") + } + } + return +} + +func redirectFromHTTP(base string) { + http.ListenAndServe(":80", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, base, http.StatusFound) + })) +} + +// certHostname figures out the name to use for the TLS certificates, using baseURL +// and falling back to the listen address if baseURL is empty or invalid. +func certHostname(listen, baseURL string) (string, error) { + hostPort, err := netutil.HostPort(baseURL) + if err != nil { + hostPort = listen + } + hostname, _, err := net.SplitHostPort(hostPort) + if err != nil { + return "", fmt.Errorf("failed to find hostname for cert from address %q: %v", hostPort, err) + } + return hostname, nil +} + +// main wraps Main so tests (which generate their own func main) can still run Main. +func main() { + Main(nil, nil) +} + +// Main sends on up when it's running, and shuts down when it receives from down. +func Main(up chan<- struct{}, down <-chan struct{}) { + flag.Parse() + + if *flagVersion { + fmt.Fprintf(os.Stderr, "camlistored version: %s\nGo version: %s (%s/%s)\n", + buildinfo.Version(), runtime.Version(), runtime.GOOS, runtime.GOARCH) + return + } + if legalprint.MaybePrint(os.Stderr) { + return + } + if env.OnGCE() { + log.SetOutput(gce.LogWriter()) + } + + if *flagReindex { + index.SetImpendingReindex() + } + + log.Printf("Starting camlistored version %s; Go %s (%s/%s)", buildinfo.Version(), runtime.Version(), + runtime.GOOS, runtime.GOARCH) + + shutdownc := make(chan io.Closer, 1) // receives io.Closer to cleanly shut down + go handleSignals(shutdownc) + + // In case we're running in a Docker container with no + // filesytem from which to load the root CAs, this + // conditionally installs a static set if necessary. We do + // this before we load the config file, which might come from + // an https URL. + httputil.InstallCerts() + + config, isNewConfig, err := loadConfig(*flagConfigFile) + if err != nil { + exitf("Error loading config file: %v", err) + } + + ws := webserver.New() + listen, baseURL := listenAndBaseURL(config) + + hostname, err := certHostname(listen, baseURL) + if err != nil { + exitf("Bad baseURL or listen address: %v", err) + } + setupTLS(ws, config, hostname) + + err = ws.Listen(listen) + if err != nil { + exitf("Listen: %v", err) + } + + if baseURL == "" { + baseURL = ws.ListenURL() + } + + shutdownCloser, err := config.InstallHandlers(ws, baseURL, *flagReindex, nil) + if err != nil { + exitf("Error parsing config: %v", err) + } + shutdownc <- shutdownCloser + + urlToOpen := baseURL + if !isNewConfig { + // user may like to configure the server at the initial startup, + // open UI if this is not the first run with a new config file. + urlToOpen += config.UIPath + } + + if *flagOpenBrowser { + go osutil.OpenURL(urlToOpen) + } + + go ws.Serve() + if flagPollParent { + osutil.DieOnParentDeath() + } + + if err := config.StartApps(); err != nil { + exitf("StartApps: %v", err) + } + + for appName, appURL := range config.AppURL() { + addr, err := netutil.HostPort(appURL) + if err != nil { + log.Printf("Could not get app %v address: %v", appName, err) + continue + } + if err := netutil.AwaitReachable(addr, 5*time.Second); err != nil { + log.Printf("Could not reach app %v: %v", appName, err) + } + } + log.Printf("Available on %s", urlToOpen) + + if env.OnGCE() && strings.HasPrefix(baseURL, "https://") { + go redirectFromHTTP(baseURL) + } + + // Block forever, except during tests. + up <- struct{}{} + <-down + osExit(0) +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/run_test.go b/vendor/github.com/camlistore/camlistore/server/camlistored/run_test.go new file mode 100644 index 00000000..25c62a7f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/run_test.go @@ -0,0 +1,101 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "camlistore.org/pkg/osutil" +) + +func TestStarts(t *testing.T) { + td, err := ioutil.TempDir("", "camlistored-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(td) + + fakeHome := filepath.Join(td, "fakeHome") + confDir := filepath.Join(fakeHome, "conf") + varDir := filepath.Join(fakeHome, "var") + + defer pushEnv("CAMLI_CONFIG_DIR", confDir)() + defer pushEnv("CAMLI_VAR_DIR", varDir)() + + if _, err := os.Stat(osutil.CamliConfigDir()); !os.IsNotExist(err) { + t.Fatalf("expected conf dir %q to not exist", osutil.CamliConfigDir()) + } + if !strings.Contains(osutil.CamliBlobRoot(), td) { + t.Fatalf("blob root %q should contain the temp dir %q", osutil.CamliBlobRoot(), td) + } + if _, err := os.Stat(osutil.CamliBlobRoot()); !os.IsNotExist(err) { + t.Fatalf("expected blobroot dir %q to not exist", osutil.CamliBlobRoot()) + } + if fi, err := os.Stat(osutil.UserServerConfigPath()); !os.IsNotExist(err) { + t.Errorf("expected no server config file; got %v, %v", fi, err) + } + + mkdir(t, confDir) + *flagOpenBrowser = false + *flagListen = ":0" + + up := make(chan struct{}) + down := make(chan struct{}) + dead := make(chan int, 1) + osExit = func(status int) { + dead <- status + close(dead) + runtime.Goexit() + } + go Main(up, down) + select { + case status := <-dead: + t.Errorf("os.Exit(%d) before server came up", status) + return + case <-up: + t.Logf("server is up") + case <-time.After(10 * time.Second): + t.Fatal("timeout starting server") + } + + if _, err := os.Stat(osutil.UserServerConfigPath()); err != nil { + t.Errorf("expected a server config file; got %v", err) + } + + down <- struct{}{} + <-dead +} + +func pushEnv(k, v string) func() { + old := os.Getenv(k) + os.Setenv(k, v) + return func() { + os.Setenv(k, old) + } +} + +func mkdir(t *testing.T, dir string) { + if err := os.MkdirAll(dir, 0700); err != nil { + t.Fatal(err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/setup.go b/vendor/github.com/camlistore/camlistore/server/camlistored/setup.go new file mode 100644 index 00000000..b257da6e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/setup.go @@ -0,0 +1,51 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "net" + "net/http" + "syscall" + + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/netutil" +) + +func setupHome(rw http.ResponseWriter, req *http.Request) { + port := httputil.RequestTargetPort(req) + localhostAddr, err := netutil.Localhost() + if err != nil { + httputil.ServeError(rw, req, err) + } + ourAddr := &net.TCPAddr{IP: localhostAddr, Port: port} + rAddr, err := net.ResolveTCPAddr("tcp", req.RemoteAddr) + if err != nil { + fmt.Printf("camlistored: unable to resolve RemoteAddr %q: %v", req.RemoteAddr, err) + return + } + uid, err := netutil.AddrPairUserid(rAddr, ourAddr) + if err != nil { + httputil.ServeError(rw, req, err) + } + + fmt.Fprintf(rw, "Hello %q\n", req.RemoteAddr) + fmt.Fprintf(rw, "

    uid = %d\n", syscall.Getuid()) + fmt.Fprintf(rw, "

    euid = %d\n", syscall.Geteuid()) + + fmt.Fprintf(rw, "

    http_local_uid(%q => %q) = %d (%v)\n", req.RemoteAddr, ourAddr, uid, err) +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/TODO b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/TODO new file mode 100644 index 00000000..0dc293b7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/TODO @@ -0,0 +1,14 @@ +- Show a placeholder item while an upload is in progress. +- Hook through upload progress percentage from the camli javascript + API to the upload placeholder item. +- Add dragdrop of selected blobs to put them in sets? +- Fix resample quality - looks pretty crappy right now +- Can we put the type o a file within the icon, or other kind of + preview-type information? +- Permanode functionality is just weird... do we need this in the main + UI? should be a command-line only thing I think +- Add support for uploading entire folders +- Make the toolbar a Medium/Quip style floating thing - get temporary + icons from the noun project +- Infinite scroll + - bonus: some cool effect as items load! diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/animation_loop.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/animation_loop.js new file mode 100644 index 00000000..dd1481da --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/animation_loop.js @@ -0,0 +1,89 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.AnimationLoop'); + +goog.require('goog.events.EventTarget'); + +// Provides an easier-to-use interface around window.requestAnimationFrame(), and abstracts away browser differences. +// @param {Window} win +cam.AnimationLoop = function(win) { + goog.base(this); + + this.win_ = win; + + this.requestAnimationFrame_ = win.requestAnimationFrame || win.mozRequestAnimationFrame || win.webkitRequestAnimationFrame || win.msRequestAnimationFrame; + + this.handleFrame_ = this.handleFrame_.bind(this); + + this.lastTimestamp_ = 0; + + if (this.requestAnimationFrame_) { + this.requestAnimationFrame_ = this.requestAnimationFrame_.bind(win); + } else { + this.requestAnimationFrame_ = this.simulateAnimationFrame_.bind(this); + } +}; + +goog.inherits(cam.AnimationLoop, goog.events.EventTarget); + +cam.AnimationLoop.FRAME_EVENT_TYPE = 'frame'; + +cam.AnimationLoop.prototype.isRunning = function() { + return Boolean(this.lastTimestamp_); +}; + +cam.AnimationLoop.prototype.start = function() { + if (this.isRunning()) { + return; + } + + this.lastTimestamp_ = -1; + this.schedule_(); +}; + +cam.AnimationLoop.prototype.stop = function() { + this.lastTimestamp_ = 0; +}; + +cam.AnimationLoop.prototype.schedule_ = function() { + this.requestAnimationFrame_(this.handleFrame_); +}; + +cam.AnimationLoop.prototype.handleFrame_ = function(opt_timestamp) { + if (this.lastTimestamp_ == 0) { + return; + } + + var timestamp = opt_timestamp || new Date().getTime(); + if (this.lastTimestamp_ == -1) { + this.lastTimestamp_ = timestamp; + } else { + this.dispatchEvent({ + type: this.constructor.FRAME_EVENT_TYPE, + delay: timestamp - this.lastTimestamp_ + }); + this.lastTimestamp_ = timestamp; + } + + this.schedule_(); +}; + +cam.AnimationLoop.prototype.simulateAnimationFrame_ = function(fn) { + this.win_.setTimeout(function() { + fn(new Date().getTime()); + }, 0); +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob.js new file mode 100644 index 00000000..07e54cd5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob.js @@ -0,0 +1,67 @@ +/* +Copyright 2014 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.blob'); + +goog.require('goog.crypt'); +goog.require('goog.crypt.Sha1'); + +// Returns the Camlistore blobref for hash object. The only supported hash function is currently sha1, but more might be added later. +// @param {!goog.crypt.Hash} hash +// @returns {!string} +cam.blob.refFromHash = function(hash) { + if (hash instanceof goog.crypt.Sha1) { + return 'sha1-' + goog.crypt.byteArrayToHex(hash.digest()); + } + throw new Error('Unsupported hash function type'); +}; + +// Returns the Camlistore blobref for a string using the currently recommended hash function. +// @param {!string} str +// @returns {!string} +cam.blob.refFromString = function(str) { + var hash = cam.blob.createHash(); + // update only supports 8 bit chars: http://docs.closure-library.googlecode.com/git/class_goog_crypt_Sha1.html + hash.update(goog.crypt.stringToUtf8ByteArray(str)); + return cam.blob.refFromHash(hash); +}; + +// Returns the Camlistore blobref for a DOM blob (different from Camlistore blob) using the currently recommended hash function. This function currently only works within workers. +// @param {Blob} blob +// @returns {!string} +cam.blob.refFromDOMBlob = function(blob) { + if (!goog.global.FileReaderSync) { + // TODO(aa): If necessary, we can also implement this using FileReader for use on the main thread. But beware that should not be done for very large objects without checking the effect on framerate carefully. + throw new Error('FileReaderSync not available. Perhaps we are on the main thread?'); + } + + var fr = new FileReaderSync(); + var hash = cam.blob.createHash(); + var chunkSize = 1024 * 1024; + for (var start = 0; start < blob.size; start += chunkSize) { + var end = Math.min(start + chunkSize, blob.size); + var slice = blob.slice(start, end); + hash.update(new Uint8Array(fr.readAsArrayBuffer(slice))); + } + + return cam.blob.refFromHash(hash); +}; + +// Creates an instance of the currently recommened hash function. +// @return {!goog.crypt.Hash'} +cam.blob.createHash = function() { + return new goog.crypt.Sha1(); +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_detail.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_detail.js new file mode 100644 index 00000000..8a2445c7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_detail.js @@ -0,0 +1,204 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.BlobDetail'); + +goog.require('cam.blobref'); +goog.require('cam.ServerConnection'); + +goog.require('goog.labs.Promise'); + +cam.BlobDetail = React.createClass({ + displayName: 'BlobDetail', + + BLOBREF_PATTERN_: new RegExp(cam.blobref.PATTERN, 'g'), + propTypes: { + getDetailURL: React.PropTypes.func.isRequired, + meta: React.PropTypes.object.isRequired, + serverConnection: React.PropTypes.instanceOf(cam.ServerConnection).isRequired, + }, + + getInitialState: function() { + return { + content: null, + metadata: null, + claims: null, + refs: null, + }; + }, + + componentWillMount: function() { + var sc = this.props.serverConnection; + + sc.getBlobContents(this.props.meta.blobRef, this.handleBlobContents_); + sc.permanodeClaims(this.props.meta.blobRef, this.handleClaims_); + + goog.labs.Promise.all([ + new goog.labs.Promise(sc.pathsOfSignerTarget.bind(sc, this.props.meta.blobRef)), + new goog.labs.Promise(sc.search.bind(sc, { + permanode: { + attr: 'camliMember', + value: this.props.meta.blobRef, + }, + }, null, null, null)) + ]).then(this.handleRefs_); + }, + + render: function() { + return React.DOM.div( + { + style: { + fontFamily: 'Open Sans', + margin: '1.5em 2em', + } + }, + this.getSection_("Blob content", this.state.content), + this.getSection_("Indexer metadata", this.props.meta), + this.getSection_("Mutation claims", this.state.claims), + this.getReferencesSection_(this.state.refs) + ); + }, + + getReferencesSection_: function(refs) { + if (!refs) { + return this.getReferencesBlock_("Loading..."); + } + + if (refs.length <= 0) { + return this.getReferencesBlock_("No references"); + } + + return this.getReferencesBlock_( + React.DOM.ul( + null, + refs.map(function(blobref) { + return React.DOM.li( + {}, + React.DOM.a( + { + href: this.props.getDetailURL(blobref), + }, + blobref + ) + ); + }, this) + ) + ); + }, + + getReferencesBlock_: function(content) { + return React.DOM.div( + { + key: 'References', + }, + this.getHeader_("Referenced by"), + content + ); + }, + + getSection_: function(title, content) { + return React.DOM.div( + { + key: title + }, + this.getHeader_(title), + this.getCodeBlock_(content) + ); + }, + + getHeader_: function(title) { + return React.DOM.h1( + { + key: 'header', + style: { + fontSize: '1.5em', + } + }, + title + ); + }, + + getCodeBlock_: function(stuff) { + return React.DOM.pre( + { + key: 'code-block', + style: { + overflowX: 'auto', + }, + }, + stuff ? this.linkify_(JSON.stringify(stuff, null, 2)) : "No data" + ); + }, + + linkify_: function(code) { + var result = []; + var match; + var index = 0; + while ((match = this.BLOBREF_PATTERN_.exec(code)) !== null) { + result.push(code.substring(index, match.index)); + result.push(React.DOM.a({key: match.index, href: this.props.getDetailURL(match[0]).toString()}, match[0])); + index = match.index + match[0].length; + } + result.push(code.substring(index)); + return result; + }, + + handleBlobContents_: function(data) { + this.setState({content: JSON.parse(data)}); + }, + + handleClaims_: function(data) { + this.setState({claims: data}); + }, + + handleRefs_: function(results) { + var refs = []; + if (results[0].paths) { + refs = refs.concat(results[0].paths.map(function(path) { + return path.baseRef; + })); + } + if (results[1].blobs) { + refs = refs.concat(results[1].blobs.map(function(blob) { + return blob.blob; + })); + } + this.setState({refs: refs}); + }, +}); + +cam.BlobDetail.getAspect = function(getDetailURL, serverConnection, blobref, targetSearchSession) { + if(!targetSearchSession) { + return; + } + + var m = targetSearchSession.getMeta(blobref); + if (!m) { + return null; + } + + return { + fragment: 'blob', + title: 'Blob', + createContent: function(size) { + return cam.BlobDetail({ + getDetailURL: getDetailURL, + meta: m, + serverConnection: serverConnection, + }); + }, + }; +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item.css new file mode 100644 index 00000000..4bb6b4bb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item.css @@ -0,0 +1,120 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +@import (less) "prefix-free.css"; + + +/* Tile view of BlobItem */ +.cam-blobitem { + display: inline-block; + font-size: 0.8em; +} + +.cam-blobitem>a { + text-decoration: none; +} + +.cam-blobitem-thumbclip { + position: relative; + overflow: hidden; +} + +.cam-blobitem-loading { + background-color: rgb(240, 240, 240); +} + +.cam-blobitem-thumb { + display: block; + position: relative; +} + +.cam-blobitemcontainer-50 { + font-size: 10px; +} + +.cam-blobitemcontainer-75 { + font-size: 12px; +} + +.cam-blobitemcontainer-100 { + font-size: 12px; +} + +.cam-blobitemcontainer-150 { + font-size: 13px; +} + +.cam-blobitemcontainer-200 { + font-size: 14px; +} + +.cam-blobitemcontainer-250 { + font-size: 14px; +} + +.cam-blobitem-thumbtitle { + overflow: hidden; + padding: 0 1ex; + text-align: center; + text-overflow: ellipsis; + display: block; + color: #222; +} + +.cam-blobitem.cam-dropactive { + border: 1px solid #acf!important; + outline: 1px solid #acf!important; + background: #e5efff; +} + +.cam-blobitem .checkmark { + background-image: url('checkmark2.svg'); + background-position: 5px 5px; + background-repeat: no-repeat; + background-size: 42px 42px; + cursor: pointer; + height: 52px; + left: 0; + opacity: 0; + position: absolute; + top: 0; + .transition(opacity 0.2s ease); + width: 64px; + z-index: 2; + + /* To force us into a graphics layer, otherwise we get weird effects as we transition in and out of one during animation. See: https://camlistore.org/issue/284. */ + .transform(scale3d(1, 1, 1)); +} + +.cam-blobitem.goog-control-disabled .checkmark { + display: none; +} + +.cam-blobitem.goog-control-hover .checkmark { + opacity: 0.6; +} + +.cam-blobitem.goog-control-hover .checkmark:hover { + opacity: 1!important; +} + +.cam-blobitem.goog-control-checked .checkmark { + opacity: 1!important; +} + +.cam-blobitem.goog-control-checked .checkmark { + background-image: url('checkmark2_blue.svg'); +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_container.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_container.css new file mode 100644 index 00000000..c64760b0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_container.css @@ -0,0 +1,53 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +@import (less) "prefix-free.css"; + + +.cam-blobitemcontainer { + outline: 0; /* Do not show an outline when container has focus. */ + border: 1px solid rgba(0,0,0,0); + position: relative; + white-space: nowrap; +} + +.cam-blobitemcontainer-transform { + position: absolute; + left: 0; + top: 0; + .transition-transform(100ms ease-out); +} + +.cam-blobitemcontainer.cam-dropactive { + border-color: #acf; + background: #e5efff; +} +.cam-blobitemcontainer-hidden { + display: none; +} + +.cam-blobitemcontainer>.cam-blobitemcontainer-transform>.cam-blobitem { + position: absolute; +} + +.cam-blobitemcontainer-no-results { + position: relative; + + color: #444; + font-family: 'Open Sans', sans-serif; + font-size: 24px; + text-align: center; +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_container_react.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_container_react.js new file mode 100644 index 00000000..7c8dd139 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_container_react.js @@ -0,0 +1,398 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.BlobItemContainerReact'); + +goog.require('goog.array'); +goog.require('goog.async.Throttle'); +goog.require('goog.dom'); +goog.require('goog.events.EventHandler'); +goog.require('goog.object'); +goog.require('goog.math.Coordinate'); +goog.require('goog.math.Size'); +goog.require('goog.style'); + +goog.require('cam.BlobItemReact'); +goog.require('cam.SearchSession'); +goog.require('cam.SpritedImage'); + +cam.BlobItemContainerReact = React.createClass({ + displayName: 'BlobItemContainerReact', + + // Margin between items in the layout. + BLOB_ITEM_MARGIN_: 7, + + // If the last row uses at least this much of the available width before adjustments, we'll call it "close enough" and adjust things so that it fills the entire row. Less than this, and we'll leave the last row unaligned. + LAST_ROW_CLOSE_ENOUGH_TO_FULL_: 0.85, + + // Distance from the bottom of the page at which we will trigger loading more data. + INFINITE_SCROLL_THRESHOLD_PX_: 100, + + propTypes: { + availHeight: React.PropTypes.number.isRequired, + availWidth: React.PropTypes.number.isRequired, + detailURL: React.PropTypes.func.isRequired, // string->string (blobref->complete detail URL) + handlers: React.PropTypes.array.isRequired, + history: React.PropTypes.shape({replaceState:React.PropTypes.func.isRequired}).isRequired, + onSelectionChange: React.PropTypes.func, + scale: React.PropTypes.number.isRequired, + scaleEnabled: React.PropTypes.bool.isRequired, + scrolling: React.PropTypes.shape({ + target:React.PropTypes.shape({addEventListener:React.PropTypes.func.isRequired, removeEventListener:React.PropTypes.func.isRequired}), + get: React.PropTypes.func.isRequired, + set: React.PropTypes.func.isRequired, + }).isRequired, + searchSession: React.PropTypes.shape({getCurrentResults:React.PropTypes.func.isRequired, addEventListener:React.PropTypes.func.isRequired, loadMoreResults:React.PropTypes.func.isRequired}), + selection: React.PropTypes.object.isRequired, + style: React.PropTypes.object, + thumbnailSize: React.PropTypes.number.isRequired, + }, + + getDefaultProps: function() { + return { + style: {}, + }; + }, + + componentWillMount: function() { + this.eh_ = new goog.events.EventHandler(this); + this.lastCheckedIndex_ = -1; + this.layoutHeight_ = 0; + + // Minimal information we keep about every single child. We construct the actual child lazily when the user scrolls it into view using handler. + // @type {Array.<{{position:goog.math.Position, size:goog.math.Size, blobref:string, handler>} + this.childItems_ = null; + + // TODO(aa): This can be removed when https://code.google.com/p/chromium/issues/detail?id=50298 is fixed and deployed. + this.updateHistoryThrottle_ = new goog.async.Throttle(this.updateHistory_, 2000); + + // TODO(aa): This can be removed when https://code.google.com/p/chromium/issues/detail?id=312427 is fixed and deployed. + this.lastWheelItem_ = ''; + }, + + componentDidMount: function() { + this.eh_.listen(this.props.searchSession, cam.SearchSession.SEARCH_SESSION_CHANGED, this.handleSearchSessionChanged_); + this.eh_.listen(this.props.scrolling.target, 'scroll', this.handleScroll_); + if (this.props.history.state && this.props.history.state.scroll) { + this.props.scrolling.set(this.props.history.state.scroll); + } + this.fillVisibleAreaWithResults_(); + }, + + componentWillReceiveProps: function(nextProps) { + if (nextProps.searchSession != this.props.searchSession) { + this.eh_.unlisten(this.props.searchSession, cam.SearchSession.SEARCH_SESSION_CHANGED, this.handleSearchSessionChanged_); + this.eh_.listen(nextProps.searchSession, cam.SearchSession.SEARCH_SESSION_CHANGED, this.handleSearchSessionChanged_); + nextProps.searchSession.loadMoreResults(); + } + + this.childItems_ = null; + }, + + componentWillUnmount: function() { + this.eh_.dispose(); + this.updateHistoryThrottle_.dispose(); + }, + + getInitialState: function() { + return { + scroll:0, + }; + }, + + render: function() { + this.updateChildItems_(); + + var childControls = this.childItems_.filter(function(item) { + var visible = this.isVisible_(item.position.y) || this.isVisible_(item.position.y + item.size.height); + var isLastWheelItem = item.blobref == this.lastWheelItem_; + return visible || isLastWheelItem; + }, this).map(function(item) { + return cam.BlobItemReact({ + key: item.blobref, + blobref: item.blobref, + checked: Boolean(this.props.selection[item.blobref]), + onCheckClick: this.props.onSelectionChange ? this.handleCheckClick_ : null, + onWheel: this.handleChildWheel_, + position: item.position, + }, + item.handler.createContent(item.size) + ); + }, this); + + // If we haven't filled the window with results, add some more. + this.fillVisibleAreaWithResults_(); + + if (childControls.length == 0 && this.props.searchSession.isComplete()) { + childControls.push(this.getNoResultsMessage_()); + } + + var transformStyle = {}; + var scale = this.props.scaleEnabled ? this.props.scale : 1; + transformStyle[cam.reactUtil.getVendorProp('transform')] = goog.string.subs('scale3d(%s, %s, 1)', scale, scale); + transformStyle[cam.reactUtil.getVendorProp('transformOrigin')] = goog.string.subs('left %spx 0', this.state.scroll); + + return React.DOM.div( + { + className: 'cam-blobitemcontainer', + style: cam.object.extend(this.props.style, { + height: this.layoutHeight_, + width: this.props.availWidth, + }), + onMouseDown: this.handleMouseDown_, + }, + React.DOM.div( + { + className: 'cam-blobitemcontainer-transform', + style: transformStyle, + }, + childControls + ) + ); + }, + + updateChildItems_: function() { + if (this.childItems_ !== null) { + return; + } + + this.childItems_ = []; + + var results = this.props.searchSession.getCurrentResults(); + var items = results.blobs.map(function(blob) { + var blobref = blob.blob; + var self = this; + var href = self.props.detailURL(blobref).toString(); + var handler = null; + this.props.handlers.some(function(h) { return handler = h(blobref, self.props.searchSession, href); }); + return { + blobref: blobref, + handler: handler, + position: null, + size: null, + }; + }.bind(this)); + + var currentTop = this.BLOB_ITEM_MARGIN_; + var currentWidth = this.BLOB_ITEM_MARGIN_; + var rowStart = 0; + var lastItem = results.blobs.length - 1; + + for (var i = rowStart; i <= lastItem; i++) { + var item = items[i]; + var availWidth = this.props.availWidth; + var nextWidth = currentWidth + this.props.thumbnailSize * item.handler.getAspectRatio() + this.BLOB_ITEM_MARGIN_; + if (i != lastItem && nextWidth < availWidth) { + currentWidth = nextWidth; + continue; + } + + // Decide how many items are going to be in this row. We choose the number that will result in the smallest adjustment to the image sizes having to be done. + var rowEnd, rowWidth; + + // For the last item we always use all the rest of the items in this row. + if (i == lastItem) { + rowEnd = lastItem; + rowWidth = nextWidth; + if (nextWidth / availWidth < this.LAST_ROW_CLOSE_ENOUGH_TO_FULL_) { + availWidth = nextWidth; + } + + // If we have at least one item in this row, and the adjustment to the row width is less without the next item than with it, then we leave the next item for the next row. + } else if (i > rowStart && (availWidth - currentWidth <= nextWidth - availWidth)) { + rowEnd = i - 1; + rowWidth = currentWidth; + + // Otherwise we include the next item in this row. + } else { + rowEnd = i; + rowWidth = nextWidth; + } + + currentTop += this.updateChildItemsRow_(items, rowStart, rowEnd, availWidth, rowWidth, currentTop) + this.BLOB_ITEM_MARGIN_; + + currentWidth = this.BLOB_ITEM_MARGIN_; + rowStart = rowEnd + 1; + i = rowEnd; + } + + this.layoutHeight_ = currentTop; + }, + + updateChildItemsRow_: function(items, startIndex, endIndex, availWidth, usedWidth, top) { + var currentLeft = 0; + var rowHeight = Number.POSITIVE_INFINITY; + + var numItems = endIndex - startIndex + 1; + + // Doesn't seem like this should be necessary. Subpixel bug? Aaron can't math? + var fudge = 1; + + var availThumbWidth = availWidth - (this.BLOB_ITEM_MARGIN_ * (numItems + 1)) - fudge; + var usedThumbWidth = usedWidth - (this.BLOB_ITEM_MARGIN_ * (numItems + 1)); + + for (var i = startIndex; i <= endIndex; i++) { + // We figure out the amount to adjust each item in this slightly non-intuitive way so that the adjustment is split up as fairly as possible. Figuring out a ratio up front and applying it to all items uniformly can end up with a large amount left over because of rounding. + var item = items[i]; + var numItemsLeft = (endIndex + 1) - i; + var delta = Math.round((availThumbWidth - usedThumbWidth) / numItemsLeft); + var originalWidth = this.props.thumbnailSize * item.handler.getAspectRatio(); + var width = originalWidth + delta; + var ratio = width / originalWidth; + var height = Math.round(this.props.thumbnailSize * ratio); + + item.position = new goog.math.Coordinate(currentLeft + this.BLOB_ITEM_MARGIN_, top); + item.size = new goog.math.Size(width, height); + this.childItems_.push(item); + + currentLeft += width + this.BLOB_ITEM_MARGIN_; + usedThumbWidth += delta; + rowHeight = Math.min(rowHeight, height); + } + + for (var i = startIndex; i <= endIndex; i++) { + this.childItems_[i].size.height = rowHeight; + } + + return rowHeight; + }, + + getNoResultsMessage_: function() { + var piggyWidth = 88; + var piggyHeight = 62; + var w = 350; + var h = 100; + + return React.DOM.div( + { + key: 'no-results', + className: 'cam-blobitemcontainer-no-results', + style: { + width: w, + height: h, + left: (this.props.availWidth - w) / 2, + top: (this.props.availHeight - h) / 3 + }, + }, + React.DOM.div(null, 'No results found'), + cam.SpritedImage( + { + index: 6, + sheetWidth: 10, + spriteWidth: piggyWidth, + spriteHeight: piggyHeight, + src: 'glitch/npc_piggy__x1_rooked1_png_1354829442.png', + style: { + 'margin-left': (w - piggyWidth) / 2 + } + } + ) + ); + }, + + getScrollFraction_: function() { + var max = this.layoutHeight_; + if (max == 0) + return 0; + return this.state.scroll / max; + }, + + getTranslation_: function() { + var maxOffset = (1 - this.props.scale) * this.layoutHeight_; + var currentOffset = maxOffset * this.getScrollFraction_(); + return currentOffset; + }, + + transformY_: function(y) { + return y * this.props.scale + this.getTranslation_(); + }, + + getScrollBottom_: function() { + return this.state.scroll + this.props.availHeight; + }, + + isVisible_: function(y) { + y = this.transformY_(y); + return y >= this.state.scroll && y < this.getScrollBottom_(); + }, + + handleSearchSessionChanged_: function() { + this.childItems_ = null; + this.forceUpdate(); + }, + + handleCheckClick_: function(blobref, e) { + var blobs = this.props.searchSession.getCurrentResults().blobs; + var index = goog.array.findIndex(blobs, function(b) { return b.blob == blobref }); + var newSelection = cam.object.extend(this.props.selection, {}); + + if (e.shiftKey && this.lastCheckedIndex_ > -1) { + var low = Math.min(this.lastCheckedIndex_, index); + var high = Math.max(this.lastCheckedIndex_, index); + for (var i = low; i <= high; i++) { + newSelection[blobs[i].blob] = true; + } + } else { + if (newSelection[blobref]) { + delete newSelection[blobref]; + } else { + newSelection[blobref] = true; + } + } + + this.lastCheckedIndex_ = index; + this.forceUpdate(); + + this.props.onSelectionChange(newSelection); + }, + + handleMouseDown_: function(e) { + // Prevent the default selection behavior. + if (e.shiftKey) { + e.preventDefault(); + } + }, + + handleScroll_: function() { + this.setState({scroll:this.props.scrolling.get()}, function() { + this.updateHistoryThrottle_.fire(); + this.fillVisibleAreaWithResults_(); + }.bind(this)); + }, + + handleChildWheel_: function(child) { + this.lastWheelItem_ = child.props.blobref; + }, + + // NOTE: This method causes the URL bar to throb for a split second (at least on Chrome), so it should not be called constantly. + updateHistory_: function() { + // second argument (title) is ignored on Firefox, but not optional. + this.props.history.replaceState(cam.object.extend(this.props.history.state, {scroll:this.state.scroll}), ''); + }, + + fillVisibleAreaWithResults_: function() { + if (!this.isMounted()) { + return; + } + + var layoutEnd = this.transformY_(this.layoutHeight_); + if ((layoutEnd - this.getScrollBottom_()) > this.INFINITE_SCROLL_THRESHOLD_PX_) { + return; + } + + this.props.searchSession.loadMoreResults(); + }, +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_container_test.html b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_container_test.html new file mode 100644 index 00000000..7117dbfc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_container_test.html @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_demo_content.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_demo_content.js new file mode 100644 index 00000000..0f8252a3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_demo_content.js @@ -0,0 +1,99 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.BlobItemDemoContent'); + +goog.require('goog.math.Size'); + +// BlobItemDemoContent is a demo node type, useful for giving talks and showing +// how a custom renderer can be invoked just by making a permanode, setting +// its "camliNodeType" attribute to "camlistore.org:demo", and then changing its +// background color with the "color" property or text with the "title" property. +cam.BlobItemDemoContent = React.createClass({ + displayName: 'BlobItemDemoContent', + + propTypes: { + blobref: React.PropTypes.string.isRequired, + href: React.PropTypes.string.isRequired, + title: React.PropTypes.string.isRequired, + size: React.PropTypes.instanceOf(goog.math.Size).isRequired, + color: React.PropTypes.string.isRequired + }, + + getInitialState: function () { + return { + mouseover: false, + }; + }, + + render: function () { + return React.DOM.a({ + href: this.props.href, + style: { + backgroundColor: this.props.color, + width: this.props.size.width + "px", + height: this.props.size.height + "px", + display: 'block' + }, + onMouseEnter: this.handleMouseOver_, + onMouseLeave: this.handleMouseOut_ + }, + this.props.title + (this.state.mouseover ? ', mouseover' : '') + ); + }, + + handleMouseOver_: function () { + this.setState({ + mouseover: true + }); + }, + + handleMouseOut_: function () { + this.setState({ + mouseover: false + }); + }, +}); + +cam.BlobItemDemoContent.getHandler = function (blobref, searchSession, href) { + var m = searchSession.getMeta(blobref); + if (m.camliType == 'permanode') { + var typ = cam.permanodeUtils.getCamliNodeType(m.permanode); + if (typ == 'camlistore.org:demo') { + return new cam.BlobItemDemoContent.Handler(m, href) + } + } + return null; +}; + +cam.BlobItemDemoContent.Handler = function (meta, href) { + this.meta_ = meta; + this.href_ = href; +}; + +cam.BlobItemDemoContent.Handler.prototype.getAspectRatio = function () { + return 1; +}; + +cam.BlobItemDemoContent.Handler.prototype.createContent = function (size) { + return cam.BlobItemDemoContent({ + blobref: this.meta_.blobRef, + color: cam.permanodeUtils.getSingleAttr(this.meta_.permanode, 'color') || '#777', + title: cam.permanodeUtils.getSingleAttr(this.meta_.permanode, 'title') || '', + href: this.href_, + size: size, + }); +}; \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_foursquare.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_foursquare.css new file mode 100644 index 00000000..59b532be --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_foursquare.css @@ -0,0 +1,74 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +@import (less) "prefix-free.css"; + +.cam-blobitem-fs-checkin { + display: block; + border-radius: 7%; + position: relative; + overflow: hidden; + height: 100%; + color: white; + text-shadow:1px 1px 2px black; + white-space: normal; +} + +.cam-blobitem-fs-checkin-content { + position: relative; + background: rgba(80,80,80,0.4); + border-radius: 7%; + position: absolute; + z-index: 1; + width: 100%; + height: 100%; + padding: 6%; + text-align: center; +} + +.cam-blobitem-fs-checkin-content table { + border-collapse: collapse; + height: 100%; + width: 100%; +} + +.cam-blobitem-fs-checkin-content td { + height: 100%; + width: 100%; + vertical-align: middle; +} + +.cam-blobitem-fs-checkin-content img { + position: absolute; + top: 4%; + left: 50%; + margin-left: -59px; + width: 118px; + height: 32px; +} + +.cam-blobitem-fs-checkin-venue { + font-weight: bold; + font-size: 150%; + line-height: 1em; +} + +.cam-blobitem-fs-checkin-when { + position: absolute; + bottom: 4%; + left: 0; + width: 100%; +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_foursquare_content.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_foursquare_content.js new file mode 100644 index 00000000..30157cf8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_foursquare_content.js @@ -0,0 +1,139 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.BlobItemFoursquareContent'); + +goog.require('goog.array'); +goog.require('goog.math.Size'); +goog.require('goog.object'); +goog.require('goog.string'); + +goog.require('cam.dateUtils'); +goog.require('cam.math'); +goog.require('cam.permanodeUtils'); +goog.require('cam.Thumber'); + +cam.BlobItemFoursquareContent = React.createClass({ + propTypes: { + href: React.PropTypes.string.isRequired, + size: React.PropTypes.instanceOf(goog.math.Size).isRequired, + venueId: React.PropTypes.string.isRequired, + venueName: React.PropTypes.string.isRequired, + photo: React.PropTypes.string.isRequired, + date: React.PropTypes.number.isRequired, + }, + + render: function() { + return React.DOM.a({ + href: this.props.href, + className: 'cam-blobitem-fs-checkin', + style: { + backgroundImage: 'url(' + this.props.photo + ')', + width: this.props.size.width, + height: this.props.size.height, + }, + }, + React.DOM.div({className:'cam-blobitem-fs-checkin-content'}, + React.DOM.img({src: 'foursquare-logo.png'}), + React.DOM.table(null, + React.DOM.tr(null, + React.DOM.td(null, + React.DOM.div({className:'cam-blobitem-fs-checkin-intro'}, 'Check-in at'), + React.DOM.div({className:'cam-blobitem-fs-checkin-venue'}, this.props.venueName) + ) + ) + ), + React.DOM.div({className:'cam-blobitem-fs-checkin-when'}, cam.dateUtils.formatDateShort(this.props.date)) + ) + ); + }, +}); + +// Blech, we need this to prevent images from flashing when data changes server-side. +cam.BlobItemFoursquareContent.photoMeta_ = {}; + +cam.BlobItemFoursquareContent.getPhotoMeta_ = function(blobref, venueMeta, searchSession) { + var photoMeta = this.photoMeta_[blobref]; + if (photoMeta) { + return photoMeta; + } + + var photosBlobref = cam.permanodeUtils.getSingleAttr(venueMeta.permanode, 'camliPath:photos') + var photosMeta = searchSession.getMeta(photosBlobref); + var photoIds = (photosMeta && photosMeta.permanode && goog.object.getKeys(photosMeta.permanode.attr).filter(function(k) { return goog.string.startsWith(k, 'camliPath:') })) || []; + + photoMeta = (photoIds.length && cam.permanodeUtils.getSingleAttr(photosMeta.permanode, photoIds[goog.string.hashCode(blobref) % photoIds.length])) || null; + if (photoMeta) { + photoMeta = this.photoMeta_[blobref] = searchSession.getMeta(photoMeta); + } + + return photoMeta; +}; + +cam.BlobItemFoursquareContent.getHandler = function(blobref, searchSession, href) { + var m = searchSession.getMeta(blobref); + if (m.camliType != 'permanode') { + return null; + } + + if (cam.permanodeUtils.getCamliNodeType(m.permanode) != 'foursquare.com:checkin') { + return null; + } + + var startDate = cam.permanodeUtils.getSingleAttr(m.permanode, 'startDate'); + var venueBlobref = cam.permanodeUtils.getSingleAttr(m.permanode, 'foursquareVenuePermanode'); + if (!startDate || !venueBlobref) { + return null; + } + + + var venueMeta = searchSession.getResolvedMeta(venueBlobref); + if (!venueMeta) { + return null; + } + + var venueId = cam.permanodeUtils.getSingleAttr(venueMeta.permanode, 'foursquareId'); + var venueName = cam.permanodeUtils.getSingleAttr(venueMeta.permanode, 'title'); + if (!venueId || !venueName) { + return null; + } + + return new cam.BlobItemFoursquareContent.Handler(href, venueId, venueName, + cam.BlobItemFoursquareContent.getPhotoMeta_(blobref, venueMeta, searchSession), Date.parse(startDate)); +}; + +cam.BlobItemFoursquareContent.Handler = function(href, venueId, venueName, venuePhotoMeta, startDate) { + this.href_ = href; + this.venueId_ = venueId; + this.venueName_ = venueName; + this.startDate_ = startDate; + this.thumber_ = venuePhotoMeta ? new cam.Thumber.fromImageMeta(venuePhotoMeta) : null; +}; + +cam.BlobItemFoursquareContent.Handler.prototype.getAspectRatio = function() { + return 1.0; +}; + +cam.BlobItemFoursquareContent.Handler.prototype.createContent = function(size) { + return cam.BlobItemFoursquareContent({ + href: this.href_, + size: size, + venueId: this.venueId_, + venueName: this.venueName_, + photo: this.thumber_ ? this.thumber_.getSrc(size) : '', + date: this.startDate_, + }); +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_generic_content.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_generic_content.js new file mode 100644 index 00000000..dc09ddac --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_generic_content.js @@ -0,0 +1,137 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.BlobItemGenericContent'); + +goog.require('goog.math.Size'); + +goog.require('cam.math'); +goog.require('cam.object'); +goog.require('cam.permanodeUtils'); + +// Renders the content of blob items that are not known to be some more specific type. A generic file or folder icon is shown, along with a title if one can be determined. +cam.BlobItemGenericContent = React.createClass({ + displayName: 'BlobItemGenericContent', + + TITLE_HEIGHT: 22, + + propTypes: { + href: React.PropTypes.string.isRequired, + size: React.PropTypes.instanceOf(goog.math.Size).isRequired, + thumbSrc: React.PropTypes.string.isRequired, + thumbAspect: React.PropTypes.number.isRequired, + title: React.PropTypes.string.isRequired, + }, + + render: function() { + var thumbClipSize = this.getThumbClipSize_(); + // TODO(aa): I think we don't need/want the thumb clip div anymore. We can just make the anchor position:relative position the thumb inside it. + return React.DOM.a({href:this.props.href}, + React.DOM.div({className:this.getThumbClipClassName_(), style:thumbClipSize}, + this.getThumb_(thumbClipSize) + ), + this.getLabel_() + ); + }, + + getThumbClipClassName_: function() { + return React.addons.classSet({ + 'cam-blobitem-thumbclip': true, + 'cam-blobitem-loading': false, + }); + }, + + getThumb_: function(thumbClipSize) { + var thumbSize = this.getThumbSize_(thumbClipSize); + var pos = cam.math.center(thumbSize, thumbClipSize); + return React.DOM.img({ + className: 'cam-blobitem-thumb', + ref: 'thumb', + src: this.props.thumbSrc, + style: {left:pos.x, top:pos.y}, + width: thumbSize.width, + height: thumbSize.height, + }) + }, + + getLabel_: function() { + return React.DOM.span({className:'cam-blobitem-thumbtitle', style:{width:this.props.size.width}}, this.props.title); + }, + + getThumbSize_: function(available) { + var bleed = false; + return cam.math.scaleToFit(new goog.math.Size(this.props.thumbAspect, 1), available, bleed); + }, + + getThumbClipSize_: function() { + return new goog.math.Size(this.props.size.width, this.props.size.height - this.TITLE_HEIGHT); + }, +}); + +cam.BlobItemGenericContent.getHandler = function(blobref, searchSession, href) { + return new cam.BlobItemGenericContent.Handler(blobref, searchSession, href); +}; + + +cam.BlobItemGenericContent.Handler = function(blobref, searchSession, href) { + this.blobref_ = blobref; + this.searchSession_ = searchSession; + this.href_ = href; + this.thumbType_ = this.getThumbType_(); +}; + +cam.BlobItemGenericContent.Handler.ICON_ASPECT = { + FILE: 260 / 300, + FOLDER: 300 / 300, +}; + +cam.BlobItemGenericContent.Handler.prototype.getAspectRatio = function() { + return this.thumbType_ == 'folder' ? this.constructor.ICON_ASPECT.FOLDER : this.constructor.ICON_ASPECT.FILE; +}; + +cam.BlobItemGenericContent.Handler.prototype.createContent = function(size) { + // TODO(aa): In the case of a permanode that is a container (cam.permanodeUtils.isContainer()) and has a camliContentImage, it would be nice to show that image somehow along with the folder icon. + return cam.BlobItemGenericContent({ + href: this.href_, + size: size, + thumbSrc: this.thumbType_ + '.png', + thumbAspect: this.getAspectRatio(), + title: this.searchSession_.getTitle(this.blobref_), + }); +}; + +cam.BlobItemGenericContent.Handler.prototype.getThumbType_ = function() { + var m = this.searchSession_.getMeta(this.blobref_); + var rm = this.searchSession_.getResolvedMeta(this.blobref_); + + if (rm) { + if (rm.camliType == 'file') { + return 'file'; + } + + if (rm.camliType == 'directory' || rm.camliType == 'static-set') { + return 'folder'; + } + } + + // Using the directory icon for any random permanode is a bit weird. Ideally we'd use file for that. The problem is that we can't tell the difference between a permanode that is representing an empty dynamic set and a permanode that is representing something else entirely. + // And unfortunately, the UI has a big prominent button that says 'new set', and it looks funny if the new set is shown as a file icon :( + if (m.camliType == 'permanode') { + return 'folder'; + } + + return 'file'; +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_image_content.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_image_content.js new file mode 100644 index 00000000..d9f42074 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_image_content.js @@ -0,0 +1,153 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.BlobItemImageContent'); + +goog.require('goog.math.Size'); + +goog.require('cam.math'); +goog.require('cam.permanodeUtils'); +goog.require('cam.PyramidThrobber'); +goog.require('cam.Thumber'); + +// Renders image blob items. Handles the following cases: +// a) camliType == 'file', and also has an 'image' property. +// b) permanode with camliContent pointing to (a) +// c) permanode with 'camliImageContent' attribute pointing to (a) +cam.BlobItemImageContent = React.createClass({ + displayName: 'BlobItemImageContent', + + propTypes: { + aspect: React.PropTypes.number.isRequired, + href: React.PropTypes.string.isRequired, + size: React.PropTypes.instanceOf(goog.math.Size).isRequired, + src: React.PropTypes.string.isRequired, + title: React.PropTypes.string, + }, + + getInitialState: function() { + return { + loaded: false, + }; + }, + + componentWillMount: function() { + this.currentIntrinsicThumbHeight_ = 0; + }, + + componentDidUpdate: function(prevProps, prevState) { + // TODO(aa): It seems like we would not need this if we always use this component with the 'key' prop. + if (prevProps.blobref != this.props.blobref) { + this.currentIntrinsicThumbHeight_ = 0; + this.setState({loaded: false}); + } + }, + + render: function() { + var thumbClipSize = new goog.math.Size(this.props.size.width, this.props.size.height); + return React.DOM.a({href:this.props.href}, + React.DOM.div({className:this.getThumbClipClassName_(), style:thumbClipSize}, + this.getThrobber_(thumbClipSize), + this.getThumb_(thumbClipSize) + ) + ); + }, + + onThumbLoad_: function() { + this.setState({loaded:true}); + }, + + getThumbClipClassName_: function() { + return React.addons.classSet({ + 'cam-blobitem-thumbclip': true, + 'cam-blobitem-loading': !this.state.loaded, + }); + }, + + getThrobber_: function(thumbClipSize) { + if (this.state.loaded) { + return null; + } + return cam.PyramidThrobber({pos:cam.math.center(cam.PyramidThrobber.SIZE, thumbClipSize)}); + }, + + getThumb_: function(thumbClipSize) { + var thumbSize = this.getThumbSize_(thumbClipSize); + var pos = cam.math.center(thumbSize, thumbClipSize); + return React.DOM.img({ + className: 'cam-blobitem-thumb', + onLoad: this.onThumbLoad_, + src: this.props.src, + style: {left:pos.x, top:pos.y, visibility:(this.state.loaded ? 'visible' : 'hidden')}, + title: this.props.title, + width: thumbSize.width, + height: thumbSize.height, + }) + }, + + getThumbSize_: function(thumbClipSize) { + var bleed = true; + return cam.math.scaleToFit(new goog.math.Size(this.props.aspect, 1), thumbClipSize, bleed); + }, +}); + +cam.BlobItemImageContent.getHandler = function(blobref, searchSession, href) { + var rm = searchSession.getResolvedMeta(blobref); + if (rm && rm.image) { + return new cam.BlobItemImageContent.Handler(rm, href, searchSession.getTitle(blobref)); + } + + var m = searchSession.getMeta(blobref); + if (m.camliType != 'permanode') { + return null; + } + + // Sets can have the camliContentImage attr to indicate a user-chosen "cover image" for the entire set. Until we have some rendering for those, the folder in the generic handler is a better fit than the single image. + if (cam.permanodeUtils.isContainer(m.permanode)) { + return null; + } + + var cci = cam.permanodeUtils.getSingleAttr(m.permanode, 'camliContentImage'); + if (cci) { + var ccim = searchSession.getResolvedMeta(cci); + if (ccim) { + return new cam.BlobItemImageContent.Handler(ccim, href, searchSession.getTitle(blobref)); + } + } + + return null; +}; + +cam.BlobItemImageContent.Handler = function(imageMeta, href, title) { + this.imageMeta_ = imageMeta; + this.href_ = href; + this.title_ = title; + this.thumber_ = cam.Thumber.fromImageMeta(imageMeta); +}; + +cam.BlobItemImageContent.Handler.prototype.getAspectRatio = function() { + return this.imageMeta_.image.width / this.imageMeta_.image.height; +}; + +cam.BlobItemImageContent.Handler.prototype.createContent = function(size) { + return cam.BlobItemImageContent({ + aspect: this.getAspectRatio(), + href: this.href_, + size: size, + src: this.thumber_.getSrc(size.height), + title: this.title_, + }); +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_progress_test.html b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_progress_test.html new file mode 100644 index 00000000..684027cd --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_progress_test.html @@ -0,0 +1,17 @@ + + + + Camlistored progress + + + + +

    hello

    +
    +
    +
    +
    +
    +
    + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_react.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_react.js new file mode 100644 index 00000000..b587f146 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_react.js @@ -0,0 +1,92 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.BlobItemReact'); + +goog.require('goog.string'); +goog.require('goog.math.Coordinate'); + +cam.BlobItemReact = React.createClass({ + displayName: 'BlobItemReact', + + propTypes: { + blobref: React.PropTypes.string.isRequired, + checked: React.PropTypes.bool.isRequired, + onCheckClick: React.PropTypes.func, // (string,event)->void + onWheel: React.PropTypes.func.isRequired, + position: React.PropTypes.instanceOf(goog.math.Coordinate).isRequired, + }, + + getInitialState: function() { + return { + hovered: false, + }; + }, + + render: function() { + return React.DOM.div({ + className: this.getRootClassName_(), + style: this.getRootStyle_(), + onMouseEnter: this.handleMouseEnter_, + onMouseLeave: this.handleMouseLeave_, + onWheel: this.handleWheel_, + }, + this.getCheckmark_(), + this.props.children + ); + }, + + getRootClassName_: function() { + return React.addons.classSet({ + 'cam-blobitem': true, + 'goog-control-hover': this.state.hovered, + 'goog-control-checked': this.props.checked, + }); + }, + + getCheckmark_: function() { + if (this.props.onCheckClick) { + return React.DOM.div({className:'checkmark', onClick:this.handleCheckClick_}); + } else { + return null; + } + }, + + getRootStyle_: function() { + return { + left: this.props.position.x, + top: this.props.position.y, + }; + }, + + handleMouseEnter_: function() { + this.setState({hovered:true}); + }, + + handleMouseLeave_: function() { + this.setState({hovered:false}); + }, + + handleCheckClick_: function(e) { + this.props.onCheckClick(this.props.blobref, e); + }, + + handleWheel_: function() { + if (this.props.onWheel) { + this.props.onWheel(this); + } + }, +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_twitter.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_twitter.css new file mode 100644 index 00000000..897ff8f4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_twitter.css @@ -0,0 +1,66 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +@import (less) "prefix-free.css"; + + +.cam-blobitem-twitter-tweet { + color: black; + display: block; + font-size: 90%; + position: relative; + overflow: hidden; + white-space: normal; + border-radius: 7%; +} + +.cam-blobitem-twitter-tweet table { + border-spacing: 0; + background-color: #e1e8ed; + width: 100%; + height: 100%; +} + +.cam-blobitem-twitter-tweet-icon { + position: absolute; + width: 100%; + bottom: 0; +} + +.cam-blobitem-twitter-tweet-icon img { + width: 4em; + height: 4em; + position: absolute; + bottom: 1em; + right: 1em; + opacity: 1; +} + +.cam-blobitem-twitter-tweet-meta { + text-align: left; + vertical-align: top; + padding: 0.8em; +} + +.cam-blobitem-twitter-tweet-date { + color: #aaa; +} + +.cam-blobitem-twitter-tweet-image { + background-position: top; + background-size: cover; + height: 100%; +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_twitter_content.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_twitter_content.js new file mode 100644 index 00000000..0948b4d4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_twitter_content.js @@ -0,0 +1,129 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.BlobItemTwitterContent'); + +goog.require('goog.math.Size'); + +goog.require('cam.dateUtils'); +goog.require('cam.math'); +goog.require('cam.permanodeUtils'); +goog.require('cam.Thumber'); + +cam.BlobItemTwitterContent = React.createClass({ + propTypes: { + date: React.PropTypes.number.isRequired, + href: React.PropTypes.string.isRequired, + image: React.PropTypes.string, + size: React.PropTypes.instanceOf(goog.math.Size).isRequired, + username: React.PropTypes.string.isRequired, + }, + + getImageRow_: function() { + if (!this.props.image) { + return null; + } + + return React.DOM.tr(null, + React.DOM.td({ + className: 'cam-blobitem-twitter-tweet-image', + colSpan: 2, + src: 'twitter-icon.png', + style: { + backgroundImage: 'url(' + this.props.image + ')', + }, + }) + ); + }, + + render: function() { + return React.DOM.a({ + href: this.props.href, + className: 'cam-blobitem-twitter-tweet', + style: { + width: this.props.size.width, + height: this.props.size.height, + }, + }, + React.DOM.table({height: this.props.image ? '100%' : ''}, + React.DOM.tr(null, + React.DOM.td({className: 'cam-blobitem-twitter-tweet-meta'}, + React.DOM.span({className: 'cam-blobitem-twitter-tweet-date'}, cam.dateUtils.formatDateShort(this.props.date)), + React.DOM.br(), + React.DOM.span({className: ' cam-blobitem-twitter-tweet-content'}, this.props.content) + ) + ), + this.getImageRow_(), + React.DOM.tr(null, + React.DOM.td({className: 'cam-blobitem-twitter-tweet-icon'}, + React.DOM.img({src: 'twitter-logo.png'}) + ) + ) + ) + ); + }, +}); + +cam.BlobItemTwitterContent.getHandler = function(blobref, searchSession, href) { + var m = searchSession.getMeta(blobref); + if (m.camliType != 'permanode') { + return null; + } + + if (cam.permanodeUtils.getCamliNodeType(m.permanode) != 'twitter.com:tweet') { + return null; + } + + var date = cam.permanodeUtils.getSingleAttr(m.permanode, 'startDate'); + var username = cam.permanodeUtils.getSingleAttr(m.permanode, 'url'); + if (!date || !username) { + return null; + } + + username = username.match(/^https:\/\/twitter.com\/(.+?)\//)[1]; + + // It's OK to not have any content. Tweets can be just images or whatever. + var content = cam.permanodeUtils.getSingleAttr(m.permanode, 'content'); + var imageMeta = cam.permanodeUtils.getSingleAttr(m.permanode, 'camliContentImage'); + if (imageMeta) { + imageMeta = searchSession.getResolvedMeta(imageMeta); + } + + return new cam.BlobItemTwitterContent.Handler(content, Date.parse(date), href, imageMeta, username); +}; + +cam.BlobItemTwitterContent.Handler = function(content, date, href, imageMeta, username) { + this.content_ = content; + this.date_ = date; + this.href_ = href; + this.username_ = username; + this.thumber_ = imageMeta ? new cam.Thumber.fromImageMeta(imageMeta) : null; +}; + +cam.BlobItemTwitterContent.Handler.prototype.getAspectRatio = function() { + return 1.0; +}; + +cam.BlobItemTwitterContent.Handler.prototype.createContent = function(size) { + return cam.BlobItemTwitterContent({ + content: this.content_, + date: this.date_, + href: this.href_, + image: this.thumber_ ? this.thumber_.getSrc(size) : null, + size: size, + username: this.username_, + }); +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_video.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_video.css new file mode 100644 index 00000000..d2315ece --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_video.css @@ -0,0 +1,53 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +@import (less) "prefix-free.css"; + + +.cam-blobitem-video { + display: block; +} + +.cam-blobitem-video a { + text-decoration: none; +} + +.cam-blobitem-video .fa-video-camera { + color: #ccc; + display: block; + text-align: center; +} + +.cam-blobitem-video .fa-play, +.cam-blobitem-video .fa-pause { + cursor: default; + position: absolute; + left: 35%; + top: 40%; + color: rgba(125, 125, 125, 0.85); + line-height: 100%; +} + +.cam-blobitem-video .fa-pause { + left: 32%; +} + +.cam-blobitem-video-loaded .fa-play { + left: 40%; +} +.cam-blobitem-video-loaded .fa-pause { + left: 37%; +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_video_content.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_video_content.js new file mode 100644 index 00000000..e338f170 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_item_video_content.js @@ -0,0 +1,197 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.BlobItemVideoContent'); + +goog.require('goog.math.Size'); + +// Renders video blob items. Currently recognizes movies by looking for a filename with a common movie extension. +cam.BlobItemVideoContent = React.createClass({ + displayName: 'BlobItemVideoContent', + + MIN_PREVIEW_SIZE: 128, + + propTypes: { + blobref: React.PropTypes.string.isRequired, + filename: React.PropTypes.string.isRequired, + href: React.PropTypes.string.isRequired, + size: React.PropTypes.instanceOf(goog.math.Size).isRequired, + }, + + getInitialState: function() { + return { + loaded: false, + mouseover: false, + playing: false, + }; + }, + + render: function() { + return React.DOM.div({ + className: React.addons.classSet({ + 'cam-blobitem-video': true, + 'cam-blobitem-video-loaded': this.state.loaded, + }), + onMouseEnter: this.handleMouseOver_, + onMouseLeave: this.handleMouseOut_, + }, + React.DOM.a({href: this.props.href}, + this.getVideo_(), + this.getPoster_() + ), + this.getPlayPauseButton_() + ); + }, + + getPoster_: function() { + if (this.state.loaded) { + return null; + } + // TODO(aa): When server indexes videos and provides a poster image, render it here. + return React.DOM.i({ + className: 'fa fa-video-camera', + style: { + fontSize: this.props.size.height / 1.5 + 'px', + lineHeight: this.props.size.height + 'px', + width: this.props.size.width, + } + }) + }, + + getVideo_: function() { + if (!this.state.loaded) { + return null; + } + return React.DOM.video({ + autoPlay: true, + src: goog.string.subs('%s%s/%s', goog.global.CAMLISTORE_CONFIG.downloadHelper, this.props.blobref, this.props.filename), + width: this.props.size.width, + height: this.props.size.height, + }) + }, + + getPlayPauseButton_: function() { + if (!this.state.mouseover || this.props.size.width < this.MIN_PREVIEW_SIZE || this.props.size.height < this.MIN_PREVIEW_SIZE) { + return null; + } + return React.DOM.i({ + className: React.addons.classSet({ + 'fa': true, + 'fa-play': !this.state.playing, + 'fa-pause': this.state.playing, + }), + onClick: this.handlePlayPauseClick_, + style: { + fontSize: this.props.size.height / 5 + 'px', + } + }) + }, + + handlePlayPauseClick_: function(e) { + this.setState({ + loaded: true, + playing: !this.state.playing, + }); + + if (this.state.loaded) { + var video = this.getDOMNode().querySelector('video'); + if (this.state.playing) { + video.pause(); + } else { + video.play(); + } + } + }, + + handleMouseOver_: function() { + this.setState({mouseover:true}); + }, + + handleMouseOut_: function() { + this.setState({mouseover:false}); + }, +}); + +cam.BlobItemVideoContent.isVideo = function(rm) { + // From http://en.wikipedia.org/wiki/List_of_file_formats + // TODO(aa): Fix this quick hack once the server indexes movies and gives us more information. + var extensions = [ + '3gp', + 'aav', + 'asf', + 'avi', + 'dat', + 'm1v', + 'm2v', + 'm4v', + 'mov', + 'mp4', + 'mpe', + 'mpeg', + 'mpg', + 'ogg', + 'wmv', + ]; + return rm && rm.file && goog.array.some(extensions, goog.string.endsWith.bind(null, rm.file.fileName.toLowerCase())); +}; + +cam.BlobItemVideoContent.getHandler = function(blobref, searchSession, href) { + var rm = searchSession.getResolvedMeta(blobref); + + // From http://en.wikipedia.org/wiki/List_of_file_formats + // TODO(aa): Fix this quick hack once the server indexes movies and gives us more information. + var extensions = [ + '3gp', + 'aav', + 'asf', + 'avi', + 'dat', + 'm1v', + 'm2v', + 'm4v', + 'mov', + 'mp4', + 'mpe', + 'mpeg', + 'mpg', + 'ogg', + 'wmv', + ]; + if (cam.BlobItemVideoContent.isVideo(rm)) { + return new cam.BlobItemVideoContent.Handler(rm, href) + } + + return null; +}; + +cam.BlobItemVideoContent.Handler = function(rm, href) { + this.rm_ = rm; + this.href_ = href; +}; + +cam.BlobItemVideoContent.Handler.prototype.getAspectRatio = function() { + // TODO(aa): Provide the right value here once server indexes movies. + return 1; +}; + +cam.BlobItemVideoContent.Handler.prototype.createContent = function(size) { + return cam.BlobItemVideoContent({ + blobref: this.rm_.blobRef, + filename: this.rm_.file.fileName, + href: this.href_, + size: size, + }); +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_test.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_test.js new file mode 100644 index 00000000..d5eab3f4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blob_test.js @@ -0,0 +1,105 @@ +/* +Copyright 2014 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.require('goog.crypt.Hash'); +goog.require('goog.crypt.Sha1'); +goog.require('goog.string'); + +var assert = require('assert'); + +goog.require('cam.blob'); + + +var MockDOMBlob = function(buffer, start, end) { + this.buffer_ = buffer; + this.start_ = start; + this.size = end - start; +}; + +MockDOMBlob.fromSize = function(size, chr) { + var arr = new Uint8Array(size); + for (var i = 0; i < arr.length; i++) { + arr[i] = chr.charCodeAt(0); + } + return new MockDOMBlob(arr.buffer, 0, arr.length); +}; + +MockDOMBlob.prototype.slice = function(start, end) { + if (start < 0 || start >= this.size) { + throw new Error(goog.strings.subs("start '%s' out of range [0,%s)", start, this.size)); + } + if (end < this.start_ || end > this.size) { + throw new Error(goog.string.subs("end '%s' out of range [0,%s)", end, this.size)); + } + if (end < start) { + throw new Error(goog.string.subs("end '%s' is less than start '%s'", start, end)); + } + + return new MockDOMBlob(this.buffer_, this.start_ + start, this.start_ + end); +}; + +MockDOMBlob.prototype.getArrayBuffer = function() { + return new Uint8Array(this.buffer_, this.start_, this.size); +}; + + +var MockFileReaderSync = function() { +}; + +MockFileReaderSync.prototype.readAsArrayBuffer = function(blob) { + return blob.getArrayBuffer(); +}; + + +describe('cam.blob', function() { + describe('#refFromHash', function() { + it('should calculate the right hash', function() { + var hash = new goog.crypt.Sha1(); + assert.equal(cam.blob.refFromHash(hash), 'sha1-da39a3ee5e6b4b0d3255bfef95601890afd80709'); + + hash.reset(); + hash.update('The quick brown fox jumps over the lazy dog'); + assert.equal(cam.blob.refFromHash(hash), 'sha1-2fd4e1c67a2d28fced849ee1bb76e7391b93eb12'); + }); + + it('should complain about wrong hash function', function() { + function FooHash() {}; + goog.inherits(FooHash, goog.crypt.Hash); + assert.throws(cam.blob.refFromHash.bind(null, new FooHash()), /Unsupported hash function type/); + }); + }); + + describe('#refFromString', function() { + it('should calculate the right hash', function() { + assert.equal(cam.blob.refFromString(''), 'sha1-da39a3ee5e6b4b0d3255bfef95601890afd80709'); + assert.equal(cam.blob.refFromString('The quick brown fox jumps over the lazy dog'), 'sha1-2fd4e1c67a2d28fced849ee1bb76e7391b93eb12'); + assert.equal(cam.blob.refFromString('Les caractères accentués, quelle plaie.'), 'sha1-2ad8f499b8721a7fe35504bce86df451db37dd66'); + }); + }); + + describe('#refFromDOMBlob', function() { + it('should calculate the right hash', function() { + blob = MockDOMBlob.fromSize(1000001, 'a'); + goog.global.FileReaderSync = MockFileReaderSync; + try { + // Verified with openssl. + assert.equal(cam.blob.refFromDOMBlob(blob), 'sha1-432e7e01de7086c5246b6ac57f5f435b58f13752'); + } finally { + delete goog.global.FileReaderSync; + } + }); + }); +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blobref.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blobref.js new file mode 100644 index 00000000..13134972 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blobref.js @@ -0,0 +1,20 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.blobref'); + +// TODO(aa): Need to eventually implement something like ref.go, which understands all the different types of hashes. +cam.blobref.PATTERN = 'sha1-[0-9a-f]{40}'; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blog.html b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blog.html new file mode 100644 index 00000000..f0029781 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/blog.html @@ -0,0 +1,6 @@ + + + + TODO + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/cache_buster_iframe.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/cache_buster_iframe.js new file mode 100644 index 00000000..3360eb29 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/cache_buster_iframe.js @@ -0,0 +1,107 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.CacheBusterIframe'); + +goog.require('goog.Uri'); + +goog.require('cam.Navigator'); + +// Reload/shift-reload doesn't actually reload iframes from server in Chrome. +// We should implement content stamping, but for now, this is a workaround. +cam.CacheBusterIframe = React.createClass({ + propTypes: { + baseURL: React.PropTypes.instanceOf(goog.Uri).isRequired, + height: React.PropTypes.number.isRequired, + onChildFrameClick: React.PropTypes.func, + src: React.PropTypes.instanceOf(goog.Uri).isRequired, + width: React.PropTypes.number.isRequired, + }, + + componentDidMount: function() { + this.getDOMNode().contentWindow.addEventListener('DOMContentLoaded', this.handleDOMContentLoaded_); + }, + + componentDidUpdate: function() { + this.componentDidMount(); + }, + + getInitialState: function() { + return { + height: this.props.height, + r: Date.now(), + } + }, + + render: function() { + var uri = this.props.src.clone(); + uri.setParameterValue('r', this.state.r); + return React.DOM.iframe({ + height: this.state.height, + src: uri.toString(), + style: { + border: 'none', + }, + width: this.props.width, + }); + }, + + handleDOMContentLoaded_: function() { + this.updateSize_(); + if (this.props.onChildFrameClick) { + this.getDOMNode().contentWindow.addEventListener('click', this.handleChildFrameClick_); + } + }, + + handleChildFrameClick_: function(e) { + var elm = cam.Navigator.shouldHandleClick(e); + if (!elm) { + return; + } + + var oldURL = new goog.Uri(e.target.href); + var newURL = this.props.baseURL.clone(); + var query = oldURL.getParameterValue('q'); + + if (query) { + newURL.setParameterValue('q', query); + } else { + newURL.setPath(newURL.getPath() + (oldURL.getParameterValue('p') || oldURL.getParameterValue('d') || oldURL.getParameterValue('b'))); + } + + try { + if (this.props.onChildFrameClick(newURL)) { + e.preventDefault(); + } + } catch (ex) { + e.preventDefault(); + throw ex; + } + }, + + updateSize_: function() { + if (!this.isMounted()) { + return; + } + + var node = this.getDOMNode(); + if (node && node.contentDocument && node.contentDocument.body) { + node.contentDocument.body.style.overflowY = 'hidden'; + this.setState({height: node.contentDocument.documentElement.offsetHeight }); + } + window.setTimeout(this.updateSize_, 200); + }, +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/checkmark2.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/checkmark2.svg new file mode 100644 index 00000000..539745c0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/checkmark2.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/checkmark2_blue.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/checkmark2_blue.svg new file mode 100644 index 00000000..531ad35a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/checkmark2_blue.svg @@ -0,0 +1,71 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/circled_plus.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/circled_plus.svg new file mode 100644 index 00000000..0a8f4c8b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/circled_plus.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/clear.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/clear.svg new file mode 100644 index 00000000..9dd6d136 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/clear.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/close.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/close.svg new file mode 100644 index 00000000..70afcef5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/close.svg @@ -0,0 +1,68 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/closure-toolbar-bg.png b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/closure-toolbar-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..469c353b2d64bbce81b030e4914f7af0e017c42c GIT binary patch literal 166 zcmV;X09pTuP)+9?4>gws~>Fw?9=jZ3<=H}() z 1) { + return interval + ' years'; + } + interval = Math.floor(seconds / 2592000); + if (interval > 1) { + return interval + ' months'; + } + interval = Math.floor(seconds / 86400); + if (interval > 1) { + return interval + ' days'; + } + interval = Math.floor(seconds / 3600); + if (interval > 1) { + return interval + ' hours'; + } + interval = Math.floor(seconds / 60); + if (interval > 1) { + return interval + ' minutes'; + } + return Math.floor(seconds) + ' seconds'; + })() + ' ago'; +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/debug.html b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/debug.html new file mode 100644 index 00000000..b5eee3d2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/debug.html @@ -0,0 +1,47 @@ + + + + Camlistored UI + + + + + + + +
    +

    Root Discovery

    +

    +
    (discovery results)
    + +

    Signing Discovery

    +

    +
    (jsonsign discovery results)
    + +

    Signing Debug

    + + + + + + + + + + + + + + + +
    JSON blob to sign: Signed blob:Verification details:
    +
    + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/debug_console.html b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/debug_console.html new file mode 100644 index 00000000..390d6174 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/debug_console.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + Brad's (somewhat less) ghetto console thing + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/debug_console.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/debug_console.js new file mode 100644 index 00000000..f37f9af7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/debug_console.js @@ -0,0 +1,259 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.DebugConsole'); + +goog.require('cam.reactUtil'); + +goog.require('cam.ServerConnection'); + +goog.require('goog.labs.Promise'); + +goog.require('goog.object'); + +cam.DebugConsole = React.createClass({ + HELP_TEXT: "-help", + HANDLERS: { + selected: { + execute: function(client, input, callback) { + var blobrefs = goog.object.getKeys(client.getSelectedItems()); + + if (!blobrefs.length) { + callback('Please select at least one item'); + } else { + callback(goog.object.getKeys(client.getSelectedItems()).join(', ')); + } + }, + help: function(callback) { + callback('Usage: selected | Blobrefs of the selected items will be written to console output'); + }, + }, + tag: { + execute: function(client, input, callback) { + var blobrefs = goog.object.getKeys(client.getSelectedItems()); + var parts = cam.DebugConsole.parseCommandAndArgs(input); + var mode = parts['command']; + var tags = parts['args'].split(',').map(function(s) { return s.trim(); }); + var prettyTags = tags.join(', '); + + if (!blobrefs.length) { + callback('Please select at least one item'); + return; + } else if (!mode) { + callback('Please provide a mode of operation for tag'); + return; + } else if (mode != 'clear' && tags.some(function(t) { return !t })) { + callback('At least one invalid tag value was supplied: ' + prettyTags); + return; + } + + var sc = client.serverConnection; + var promises = []; + + // TODO(mr): do we need to restrict add/removal of tags based upon existing values? ex: Don't delete tag 'taco' if item is not tagged with 'taco' + switch (mode) { + case "add": { + if (tags.length == 1 && tags[0] == '') { + callback('Please provide at least one tag value to add'); + return; + } + + blobrefs.forEach(function(permanode) { + tags.forEach(function(tag) { + console.log('add-tag-promise for: ' + permanode + ", tag: " + tag); + promises.push(new goog.labs.Promise(sc.newAddAttributeClaim.bind(sc, permanode, 'tag', tag))); + }); + }); + break; + } + case "del": { + if (tags.length == 1 && tags[0] == '') { + callback('Please provide at least one tag value to delete'); + return; + } + + blobrefs.forEach(function(permanode) { + tags.forEach(function(tag) { + console.log('del-tag-promise for: ' + permanode + ", tag: " + tag); + promises.push(new goog.labs.Promise(sc.newDelAttributeClaim.bind(sc, permanode, 'tag', tag))); + }); + }); + break; + } + case "set": { + if (tags.length == 1 && tags[0] == '') { + callback('Please provide at least one tag value to set'); + return; + } + + // 'set' tags using first value supplied then 'add' any additional + var numTags = tags.length; + blobrefs.forEach(function(permanode) { + console.log('set-tag-promise for: ' + permanode + ", tag: " + tags[0]); + promises.push(new goog.labs.Promise(sc.newSetAttributeClaim.bind(sc, permanode, 'tag', tags[0]))); + + for (var i = 1; i < numTags; i++) { + console.log('add-tag-promise for: ' + permanode + ", tag: " + tags[i]); + promises.push(new goog.labs.Promise(sc.newAddAttributeClaim.bind(sc, permanode, 'tag', tags[i]))); + } + }); + break; + } + case "clear": { + blobrefs.forEach(function(permanode) { + console.log('clear-tag-promise for: ' + permanode); + promises.push(new goog.labs.Promise(sc.newDelAttributeClaim.bind(sc, permanode, 'tag', ''))); + }); + break; + } + default: { + callback('tag command does not support : ' + mode); + return; + } + } + + goog.labs.Promise.all(promises).thenCatch(function(e) { + console.error('promise rejected: %s', e); + callback('The system encountered an error executing tag ' + mode + ': ' + e); + }).then(function(results) { + if (results) { + console.log('successfully completed %d of %d promises', results.length, promises.length); + + if (mode == 'add') { + callback('Successfully added the tag(s) {' + prettyTags + '} to ' + blobrefs.length + ' items'); + } else if (mode == 'del') { + callback('Successfully deleted the tag(s) {' + prettyTags + '} from ' + blobrefs.length + ' items'); + } else if (mode == 'set') { + callback('Successfully reset ' + blobrefs.length + ' items to have the tag(s) {' + prettyTags + '}'); + } else if (mode == 'clear') { + callback('Successfully deleted all tags from ' + blobrefs.length + ' items'); + } + } else { + // else: intentionally left blank. empty error object returned upon promise rejection + } + }).then(function() { + console.log('tag operation complete'); + }); + callback('executing tag operation'); + }, + help: function(callback) { + callback('Usage: tag [val1,val2,...] | Add, delete, set, or clear tag attributes on the selected permanodes | Examples: tag add val1,val2,val3 | tag del val1,val2 | tag set val1 | tag clear'); + }, + }, + }, + + getPlaceholderText_: function() { + return this.getAvailableCommands_() + " (" + this.HELP_TEXT + ")"; + }, + + getStaticHelpText_: function() { + return 'Further usage information is available by ' + this.HELP_TEXT; + }, + + getAvailableCommands_: function() { + return goog.object.getKeys(this.HANDLERS).join(', '); + }, + + handleInputChange_: function(e) { + this.setState({commandInput:e.target.value}); + }, + + handleSubmit_: function(e) { + e.preventDefault(); + var parts = cam.DebugConsole.parseCommandAndArgs(this.state.commandInput); + var h = this.HANDLERS[parts['command']]; + if (h) { + if (parts['args'] == this.HELP_TEXT) { + h.help(this.handleOutput_); + } else { + h.execute(this.props.client, parts['args'], this.handleOutput_); + } + } else { + this.handleOutput_('Command not found. Available commands are: ' + this.getAvailableCommands_() + '. ' + this.getStaticHelpText_()); + } + }, + + handleOutput_: function(out) { + this.setState({commandResult:out}); + this.setState({commandInput:''}); + this.refs.consoleInput.getDOMNode().focus(); + }, + + /* + * ReactJS #ComponentSpec + */ + getInitialState: function() { + return { + commandInput: '', + commandResult: 'Enter a command and hit Go or press the Enter key to execute. ' + this.getStaticHelpText_() + }; + }, + + propTypes: { + client: React.PropTypes.shape({ + getSelectedItems: React.PropTypes.func.isRequired, + // TODO(mr): JS warning in Chrome console, I assume here, though no exact line # provided. "invalid prop 'serverConnection' supplied to '<>', expected instance of '<>'" + serverConnection: React.PropTypes.instanceOf(cam.ServerConnection).isRequired, + }), + }, + + render: function() { + // TODO(aa): Figure out flexbox to lay this out correctly. + return React.DOM.div(null, + React.DOM.div(null, "Input"), + React.DOM.div(null, + React.DOM.form({onSubmit:this.handleSubmit_}, + React.DOM.input({ + type: 'text', + ref: 'consoleInput', + placeholder: this.getPlaceholderText_(), + style: {width:275}, + onChange: this.handleInputChange_, + value: this.state.commandInput + }), + React.DOM.button(null, 'Go') + ) + ), + React.DOM.div(null, "Output"), + React.DOM.textarea({ + readOnly: true, + style: {overflow:'auto', width:310, height:150}, + value: this.state.commandResult + }) + ); + }, + + statics: { + /** + * @return {'command' : 'x', 'args': 'y'} The first word (command) and remaining arguments of the input string + */ + parseCommandAndArgs : function(s) { + var parts = s.split(' '); + var firstCommand = parts.shift(); + var arguments = parts.join(' ').trim(); + + return {'command':firstCommand, 'args':arguments}; + } + }, + + /* + * ReactJS #Lifecycle Methods + */ + componentDidMount: function() { + // allow immediate entry of commands + this.refs.consoleInput.getDOMNode().focus(); + }, +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/detail.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/detail.css new file mode 100644 index 00000000..163561cc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/detail.css @@ -0,0 +1,113 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +@import (less) "prefix-free.css"; + + +/* TODO(aa): All this needs to get renamed to image-detail */ +.detail-view { + background: black; + left: 0; + position: absolute; + overflow: hidden; + top: 0; +} + +.detail-view-img { + opacity: 0; + position: absolute; + .transition(opacity 200ms linear); +} + +.detail-view-img-loaded { + opacity: 1; +} + +.detail-img-leave { + .transition(opacity 200ms linear); + opacity: 1; +} +.detail-img-leave.detail-img-leave-active { + opacity: 0; +} + +.detail-view-sidebar { + background: #f9f9f9; + bottom: 0; + overflow: auto; + position: absolute; + right: 0; + top: 0; +} + +.detail-view-piggy { + position: absolute; +} + +.detail-view-piggy.detail-view-piggy-backward { + .transform(scaleX(-1)); +} + +.detail-piggy-leave { + .transition(opacity 200ms linear); + opacity: 1; +} + +.detail-piggy-leave.detail-piggy-leave-active { + opacity: 0; +} + +.detail-title { + font-size: inherit; + margin: 0; +} + +.detail-description { + margin-bottom: 0; +} + +.cam-detail { + position: absolute; + background: #222; + left: 0; + top: 0; + width: 100%; + overflow: hidden; +} + +.cam-detail iframe { + background: white; +} + +.cam-detail-aspect-nav { + position: absolute; + bottom: 0; + left: 0; + width: 100%; +} + +.cam-detail-aspect-nav a { + color: #bbb; + display: inline-block; + font-family: 'Open Sans', sans-serif; + font-size: 14px; + margin-left: 1em; + padding: 0.5ex; +} + +.cam-detail-aspect-nav a:hover { + color: #ddd; +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/detail.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/detail.js new file mode 100644 index 00000000..f8e035fc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/detail.js @@ -0,0 +1,213 @@ +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.DetailView'); + +goog.require('goog.array'); +goog.require('goog.events.EventHandler'); +goog.require('goog.math.Size'); +goog.require('goog.object'); +goog.require('goog.string'); + +goog.require('cam.AnimationLoop'); +goog.require('cam.ImageDetail'); +goog.require('cam.Navigator'); +goog.require('cam.reactUtil'); +goog.require('cam.SearchSession'); +goog.require('cam.SpritedAnimation'); + +// Top-level control for the detail view. Handles loading data specified in URL and left/right navigation. +// The details of the actual rendering are left up to child controls which are chosen based on the type of data loaded. However, currently there is only one type of child control: cam.ImageDetail. +cam.DetailView = React.createClass({ + displayName: 'DetailView', + + propTypes: { + aspects: cam.reactUtil.mapOf(React.PropTypes.shape({ + getTitle: React.PropTypes.func.isRequired, + createContent: React.PropTypes.func.isRequired, + })).isRequired, + blobref: React.PropTypes.string.isRequired, + getDetailURL: React.PropTypes.func.isRequired, + history: React.PropTypes.shape({go:React.PropTypes.func.isRequired}).isRequired, + height: React.PropTypes.number.isRequired, + keyEventTarget: React.PropTypes.object.isRequired, // An event target we will addEventListener() on to receive key events. + navigator: React.PropTypes.instanceOf(cam.Navigator).isRequired, + searchSession: React.PropTypes.instanceOf(cam.SearchSession).isRequired, + searchURL: React.PropTypes.instanceOf(goog.Uri).isRequired, + width: React.PropTypes.number.isRequired, + }, + + getInitialState: function() { + return { + lastNavigateWasBackward: false, + selectedAspect: '', + }; + }, + + componentWillMount: function() { + this.pendingNavigation_ = 0; + this.navCount_ = 1; + this.eh_ = new goog.events.EventHandler(this); + }, + + componentDidMount: function(root) { + this.eh_.listen(this.props.searchSession, cam.SearchSession.SEARCH_SESSION_CHANGED, this.searchUpdated_); + this.eh_.listen(this.props.keyEventTarget, 'keyup', this.handleKeyUp_); + this.searchUpdated_(); + }, + + render: function() { + var activeAspects = null; + var selectedAspect = null; + + if (this.dataIsLoaded_()) { + activeAspects = goog.object.filter( + goog.object.map(this.props.aspects, function(f) { + return f(this.props.blobref, this.props.searchSession); + }, this), + function(a) { + return a != null; + } + ); + + selectedAspect = activeAspects[this.state.selectedAspect] || goog.object.getAnyValue(activeAspects); + } + + return React.DOM.div({className: 'cam-detail', style: {height: this.props.height}}, + this.getAspectNav_(activeAspects), + + // TODO(aa): Actually pick this based on the current URL + this.getAspectView_(selectedAspect) + ); + }, + + getAspectNav_: function(aspects) { + if (!aspects) { + return null; + } + var items = goog.object.getValues(goog.object.map(aspects, function(aspect, name) { + // TODO(aa): URLs involving k I guess? + return React.DOM.a({href: '#', onClick: this.handleAspectClick_.bind(this, name)}, aspect.getTitle()); + }, this)); + items.push(React.DOM.a({href: this.props.searchURL.toString()}, 'Back to search')); + return React.DOM.div({className: 'cam-detail-aspect-nav'}, items); + }, + + getAspectView_: function(aspect) { + if (aspect) { + // TODO(aa): Why doesn't parent pass us |Size| instead of width/height? + return aspect.createContent(new goog.math.Size(this.props.width, this.props.height - 25), this.state.lastNavigateWasBackward); + } else { + return null; + } + }, + + componentWillUnmount: function() { + this.eh_.dispose(); + }, + + handleAspectClick_: function(name, e) { + // Mathieu requests that middle and right-click do nothing until we can make real URLs work. + if (e.button == 0) { + this.setState({ + selectedAspect: name, + }); + } + return false; + }, + + handleKeyUp_: function(e) { + if (e.keyCode == goog.events.KeyCodes.LEFT) { + this.navigate_(-1); + } else if (e.keyCode == goog.events.KeyCodes.RIGHT) { + this.navigate_(1); + } else if (e.keyCode == goog.events.KeyCodes.ESC) { + this.handleEscape_(e); + } + }, + + navigate_: function(offset) { + this.pendingNavigation_ = offset; + ++this.navCount_; + this.setState({lastNavigateWasBackward: offset < 0}); + this.handlePendingNavigation_(); + }, + + handleEscape_: function(e) { + e.preventDefault(); + e.stopPropagation(); + history.go(-this.navCount_); + }, + + handlePendingNavigation_: function() { + if (!this.pendingNavigation_) { + return; + } + + var results = this.props.searchSession.getCurrentResults(); + var index = goog.array.findIndex(results.blobs, function(elm) { + return elm.blob == this.props.blobref; + }.bind(this)); + + if (index == -1) { + this.props.searchSession.loadMoreResults(); + return; + } + + index += this.pendingNavigation_; + if (index < 0) { + this.pendingNavigation_ = 0; + console.log('Cannot navigate past beginning of search result.'); + return; + } + + if (index >= results.blobs.length) { + if (this.props.searchSession.isComplete()) { + this.pendingNavigation_ = 0; + console.log('Cannot navigate past end of search result.'); + } else { + this.props.searchSession.loadMoreResults(); + } + return; + } + + this.props.navigator.navigate(this.props.getDetailURL(results.blobs[index].blob)); + }, + + searchUpdated_: function() { + this.handlePendingNavigation_(); + + if (this.dataIsLoaded_()) { + this.forceUpdate(); + return; + } + + if (this.props.searchSession.isComplete()) { + // TODO(aa): 404 UI. + var error = goog.string.subs('Could not find blobref %s in search session.', this.props.blobref); + alert(error); + throw new Error(error); + } + + // TODO(aa): This can be inefficient in the case of a fresh page load if we have to load lots of pages to find the blobref. + // Our search protocol needs to be updated to handle the case of paging ahead to a particular item. + this.props.searchSession.loadMoreResults(); + }, + + dataIsLoaded_: function() { + return Boolean(this.props.searchSession.getMeta(this.props.blobref)); + }, +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/dialog.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/dialog.css new file mode 100644 index 00000000..c2d49742 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/dialog.css @@ -0,0 +1,49 @@ +/* +Copyright 2014 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +@import (less) "prefix-free.css"; + + +.cam-dialog-mask { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2; + background: rgba(200,200,200,0.85); +} + +.cam-dialog { + position: fixed; + padding: 0 2em; + border: solid #E56A5E; + background: #eee; + color: #444; + z-index: 3; + box-shadow: 0 0 1em 0.2em rgba(0, 0, 0, 0.4); + font-family: 'Open Sans', sans-serif; + font-size: 24px; + text-align: center; +} + +.cam-dialog .cam-dialog-close { + position: absolute; + right: 14px; + top: 14px; + color: #555; + cursor: pointer; +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/dialog.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/dialog.js new file mode 100644 index 00000000..8ead3ca4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/dialog.js @@ -0,0 +1,61 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.Dialog'); + +cam.Dialog = React.createClass({ + propTypes: { + availWidth: React.PropTypes.number.isRequired, + availHeight: React.PropTypes.number.isRequired, + width: React.PropTypes.number.isRequired, + height: React.PropTypes.number.isRequired, + borderWidth: React.PropTypes.number.isRequired, + onClose: React.PropTypes.func, + }, + + render: function() { + return React.DOM.div( + { + className: 'cam-dialog-mask', + }, + React.DOM.div( + { + className: 'cam-dialog', + style: { + 'width': this.props.width, + 'height': this.props.height, + 'left': (this.props.availWidth - this.props.width) / 2, + 'top': (this.props.availHeight - this.props.height) / 2, + 'border-width': this.props.borderWidth, + }, + }, + this.getClose_(), + this.props.children + ) + ); + }, + + getClose_: function() { + if (!this.props.onClose) { + return null; + } + + return React.DOM.i({ + className: 'fa fa-times fa-lg fa-border cam-dialog-close', + onClick: this.props.onClose, + }); + }, +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/directory_detail.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/directory_detail.js new file mode 100644 index 00000000..a674d62a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/directory_detail.js @@ -0,0 +1,47 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.DirectoryDetail'); + +goog.require('cam.CacheBusterIframe'); + +// TODO(aa): Rename file. +cam.DirectoryDetail.getAspect = function(baseURL, onChildFrameClick, blobref, targetSearchSession) { + if (!targetSearchSession) { + return; + } + + var rm = targetSearchSession.getResolvedMeta(blobref); + if (!rm || rm.camliType != 'directory') { + return null; + } + + return { + fragment: 'directory', + title: 'Directory', + createContent: function(size) { + var url = baseURL.clone(); + url.setParameterValue('d', rm.blobRef); + return cam.CacheBusterIframe({ + baseURL: baseURL, + height: size.height, + onChildFrameClick: onChildFrameClick, + src: url, + width: size.width, + }); + }, + }; +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/down.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/down.svg new file mode 100644 index 00000000..f5653853 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/down.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/file.png b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/file.png new file mode 100644 index 0000000000000000000000000000000000000000..e38332e20c37627a5e16dd1e46e238e2bca657d8 GIT binary patch literal 8713 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 Fe*rK~1atrZ literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/fileembed.go b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/fileembed.go new file mode 100644 index 00000000..3a14023f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/fileembed.go @@ -0,0 +1,35 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package ui contains the resources for the Camlistore web UI. + +The below is read by genfileembed.go to determine the files to embed in the +server binary. Crazy, but true. +#fileembed pattern .+\.(js|css|html|png|svg)$ +*/ +package ui + +import ( + "camlistore.org/pkg/fileembed" +) + +const GaeSourceRoot = "source_root" + +var ( + Files *fileembed.Files + IsAppEngine, IsProdAppEngine bool +) diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/fileembed_appengine.go b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/fileembed_appengine.go new file mode 100644 index 00000000..de57dbbb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/fileembed_appengine.go @@ -0,0 +1,28 @@ +// +build appengine + +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ui + +import ( + "appengine" +) + +func init() { + IsAppEngine = true + IsProdAppEngine = !appengine.IsDevAppServer() +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/fileembed_normal.go b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/fileembed_normal.go new file mode 100644 index 00000000..523e80aa --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/fileembed_normal.go @@ -0,0 +1,29 @@ +// +build !appengine + +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ui + +import ( + "camlistore.org/pkg/fileembed" +) + +func init() { + Files = &fileembed.Files{ + Listable: true, + } +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/filetree.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/filetree.css new file mode 100644 index 00000000..9e77b373 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/filetree.css @@ -0,0 +1,32 @@ +/* +Copyright 2011 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.cam-filetree-page { + font: 16px/1.4 normal Arial, sans-serif; +} +.cam-filetree-nav:before { + content: "["; +} +.cam-filetree-nav:after { + content: "]"; +} +.cam-filetree-newp { + text-decoration: underline; + cursor: pointer; + color: darkgreen; + margin-left: .4em; + font-size: 80%; +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/filetree.html b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/filetree.html new file mode 100644 index 00000000..480094e5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/filetree.html @@ -0,0 +1,23 @@ + + + + Filetree + + + + + + + +
    +

    FileTree for

    + +
    + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/filetree.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/filetree.js new file mode 100644 index 00000000..938a7d1e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/filetree.js @@ -0,0 +1,223 @@ +/* +Copyright 2011 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.FiletreePage'); + +goog.require('goog.dom'); +goog.require('goog.events.EventType'); +goog.require('goog.ui.Component'); + +goog.require('cam.ServerConnection'); + +// @param {cam.ServerType.DiscoveryDocument} config Global config of the current server this page is being rendered for. +// @param {goog.dom.DomHelper=} opt_domHelper DOM helper to use. +// @extends {goog.ui.Component} +// @constructor +cam.FiletreePage = function(config, opt_domHelper) { + goog.base(this, opt_domHelper); + + this.config_ = config; + this.connection_ = new cam.ServerConnection(config); + +}; +goog.inherits(cam.FiletreePage, goog.ui.Component); + +cam.FiletreePage.prototype.indentStep_ = 20; + +function getDirBlobrefParam() { + var blobRef = getQueryParam('d'); + return (blobRef && isPlausibleBlobRef(blobRef)) ? blobRef : null; +} + +// Returns the first value from the query string corresponding to |key|. Returns null if the key isn't present. +getQueryParam = function(key) { + var params = document.location.search.substring(1).split('&'); + for (var i = 0; i < params.length; ++i) { + var parts = params[i].split('='); + if (parts.length == 2 && decodeURIComponent(parts[0]) == key) + return decodeURIComponent(parts[1]); + } + return null; +}; + +// Returns true if the passed-in string might be a blobref. +isPlausibleBlobRef = function(blobRef) { + return /^\w+-[a-f0-9]+$/.test(blobRef); +}; + +cam.FiletreePage.prototype.enterDocument = function() { + cam.FiletreePage.superClass_.enterDocument.call(this); + var blobref = getDirBlobrefParam(); + + if (blobref) { + this.connection_.search({blobRefPrefix: blobref}, cam.ServerConnection.DESCRIBE_REQUEST, null, null, + goog.bind(this.handleDescribeBlob_, this, blobref) + ); + } +} + +// @param {string} blobref blob to describe. +// @param {cam.ServerType.DescribeResponse} response +cam.FiletreePage.prototype.handleDescribeBlob_ = +function(blobref, response) { + if (!response || !response.description || !response.description.meta) { + alert("did not get fully described response"); + return; + } + var meta = response.description.meta; + var binfo = meta[blobref]; + if (!binfo) { + alert("Error describing blob " + blobref); + return; + } + if (binfo.camliType != "directory") { + alert("Does not contain a directory"); + return; + } + this.connection_.getBlobContents( + blobref, + goog.bind(function(data) { + var finfo = JSON.parse(data); + var fileName = finfo.fileName; + var curDir = document.getElementById('curDir'); + curDir.innerHTML = "" + fileName + ""; + this.buildTree_(); + }, this), + function(msg) { + alert("failed to get blobcontents: " + msg); + } + ); +} + +cam.FiletreePage.prototype.buildTree_ = function() { + var blobref = getDirBlobrefParam(); + var children = goog.dom.getElement("children"); + this.connection_.getFileTree(blobref, + goog.bind(function(jres) { + this.onChildrenFound_(children, 0, jres); + }, this) + ); +} + +// @param {string} div node used as root for the tree +// @param {number} depth how deep we are in the tree, for indenting +// @param {cam.ServerType.DescribeResponse} jres describe result +cam.FiletreePage.prototype.onChildrenFound_ = function(div, depth, jres) { + var indent = depth// cam.FiletreePage.prototype.indentStep_; + div.innerHTML = ""; + for (var i = 0; i < jres.children.length; i++) { + var children = jres.children; + var pdiv = goog.dom.createElement("div"); + var alink = goog.dom.createElement("a"); + alink.style.paddingLeft=indent + "px" + alink.id = children[i].blobRef; + switch (children[i].type) { + case 'directory': + goog.dom.setTextContent(alink, "+ " + children[i].name); + goog.events.listen(alink, + goog.events.EventType.CLICK, + goog.bind(function (b, d) { + this.unFold_(b, d); + }, this, alink.id, depth), + false, this + ); + break; + case 'file': + goog.dom.setTextContent(alink, " " + children[i].name); + alink.href = "./?b=" + alink.id; + break; + default: + alert("not a file or dir"); + break; + } + var newPerm = goog.dom.createElement("span"); + newPerm.className = "cam-filetree-newp"; + goog.dom.setTextContent(newPerm, "P"); + goog.events.listen(newPerm, + goog.events.EventType.CLICK, + this.newPermWithContent_(alink.id), + false, this + ); + goog.dom.appendChild(pdiv, alink); + goog.dom.appendChild(pdiv, newPerm); + goog.dom.appendChild(div, pdiv); + } +} + +cam.FiletreePage.prototype.newPermWithContent_ = function(content) { + var fun = function(e) { + this.connection_.createPermanode( + goog.bind(function(permanode) { + this.connection_.newAddAttributeClaim( + permanode, "camliContent", content, + function() { + alert("permanode created"); + }, + function(msg) { + // TODO(mpl): "cancel" new permanode + alert("set permanode content failed: " + msg); + } + ); + }, this), + function(msg) { + alert("create permanode failed: " + msg); + } + ); + } + return goog.bind(fun, this); +} + +// @param {string} blobref dir to unfold. +// @param {number} depth so we know how much to indent. +cam.FiletreePage.prototype.unFold_ = function(blobref, depth) { + var node = goog.dom.getElement(blobref); + var div = goog.dom.createElement("div"); + this.connection_.getFileTree(blobref, + goog.bind(function(jres) { + this.onChildrenFound_(div, depth+1, jres); + insertAfter(node, div); + goog.events.removeAll(node); + goog.events.listen(node, + goog.events.EventType.CLICK, + goog.bind(function(b, d) { + this.fold_(b, d); + }, this, blobref, depth), + false, this + ); + }, this) + ); +} + +function insertAfter( referenceNode, newNode ) { + // nextSibling X2 because of the "P" span + referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling.nextSibling ); +} + +// @param {string} nodeid id of the node to fold. +// @param {depth} depth so we know how much to indent. +cam.FiletreePage.prototype.fold_ = function(nodeid, depth) { + var node = goog.dom.getElement(nodeid); + // nextSibling X2 because of the "P" span + node.parentNode.removeChild(node.nextSibling.nextSibling); + goog.events.removeAll(node); + goog.events.listen(node, + goog.events.EventType.CLICK, + goog.bind(function(b, d) { + this.unFold_(b, d); + }, this, nodeid, depth), + false, this + ); +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/folder.png b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..286c03823149ab856a0daaaa62e376e6f7fb0c60 GIT binary patch literal 98565 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∨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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/foursquare-logo.png b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/foursquare-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd2786c48f5a5effadc215847cdaaaab422ed35 GIT binary patch literal 6303 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 V@DM`{d#V5c002ovPDHLkV1j(c^u+)G literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/hash_worker.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/hash_worker.js new file mode 100644 index 00000000..7defe42a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/hash_worker.js @@ -0,0 +1,30 @@ +/* +Copyright 2014 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// These two lines are required setup to make goog.require() work throughout the codebase. +var CLOSURE_BASE_PATH = 'closure/goog/'; +importScripts('closure/goog/bootstrap/webworkers.js', 'closure/goog/base.js', 'deps.js'); + +goog.require('cam.blob'); +goog.require('cam.WorkerMessageRouter'); + +// This is a simple webworker that expects to receive a single message containing a file, and sends back that file's sha1 hash. +// We do this in a worker because we observed that doing it on the main thread decreased the framerate significantly, even when chunking, and even when the chunk sizes were as small as 32k. + +var router = new cam.WorkerMessageRouter(goog.global); +router.registerHandler('ref', function(msg, sendReply) { + sendReply(cam.blob.refFromDOMBlob(msg)); +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/header.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/header.css new file mode 100644 index 00000000..26e2c834 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/header.css @@ -0,0 +1,167 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +@import (less) "prefix-free.css"; + + +@cam-header-bg: #3a3a3a; + + +.cam-header { + cursor: default; + font-family: 'Open Sans', sans-serif; + font-weight: 700; + font-size: 13px; + left: 0; + position: fixed; + top: 0; + z-index: 1; + .transform(translateZ(0)); +} + +.cam-header-main { + background: @cam-header-bg; + border-collapse: true; + border-spacing: 0; + box-shadow: 0.1em 0 0.5em 0.1em rgba(0, 0, 0, 0.4); + color: #e4e4e4; + height: 38px; + position: relative; + width: 100%; + z-index: 3; +} + +.cam-header.cam-header-sub-active .cam-header-main { + box-shadow: none; +} + +.cam-header.cam-header-sub-active .cam-header-sub { + box-shadow: 0.1em 0 0.5em 0.1em rgba(0, 0, 0, 0.4); + .transform(translate3d(0, 0, 0)); +} + +.cam-header-item { + padding: 0; + position: relative; + vertical-align: middle; + white-space: nowrap; +} + +.cam-header-title { + margin-top: 10px; + padding-right: 2em; +} + +.cam-header-title span { + font-size: 14px; +} + +.cam-header-title span:first-child { + margin-right: 5px; +} + +.cam-header-menu-dropdown { + box-shadow: 0.1em 0 1em 0.3em rgba(0, 0, 0, 0.25); + cursor: default; + left: 0; + font-weight: 500; + position: absolute; + top: 38px; + white-space: nowrap; + min-width: 140px; + max-width: 280px; + .transition-transform(100ms ease-out); + z-index: 2; +} + +.cam-header-menu-item { + background: @cam-header-bg; + border-top: 1px solid #666; + color: #eee; + cursor: pointer; + display: block; + padding: 10px 24px; + position: relative; + text-decoration: none; + .transition(background-color 100ms ease-out); + overflow: hidden; + text-overflow: ellipsis; +} + +.cam-header-menu-item-icon { + position: absolute; + left: 8px; + top: 13px; +} + +.cam-header-menu-item-error { + color: rgb(255, 157, 148); + .transition(background-color 100ms ease-out); +} + +.cam-header-menu-item:hover { + background-color: #444; +} + +.cam-header-main input { + color: white; + height: 30px; + width: 100%; + background: #444; + border: 1px solid #555; + outline: none; + padding: 1ex; + font-size: 110%; + font-family: default; + font-weight: normal; + .transition(border-color 100ms ease-out); +} + +.cam-header-main input:focus { + background: #4c4c4c; + border-color: #888; +} + +.cam-header-main-controls { + padding-left: 1em; +} + +.cam-header-main-controls.cam-header-main-controls-empty { + padding-left: 6px; +} + +.cam-header-main-controls a { + color: #eee; + display: inline-block; + height: 38px; + font-family: 'Open Sans', sans-serif; + font-weight: 500; + font-size: 13px; + line-height: 38px; + padding-left: 1em; + padding-right: 1em; + text-decoration: none; + white-space: nowrap; +} + +.cam-header-main-controls a:hover { + background: #555; +} + +.cam-header-main-controls a.cam-header-main-control-active { + height: 38px; + border-bottom: 3px solid rgb(232,139,131); +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/header.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/header.js new file mode 100644 index 00000000..23a75adb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/header.js @@ -0,0 +1,320 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.Header'); + +goog.require('goog.Uri'); + +goog.require('cam.reactUtil'); +goog.require('cam.SpritedImage'); + +cam.Header = React.createClass({ + displayName: 'Header', + + PIGGY_NATIVE_WIDTH: 88, + PIGGY_NATIVE_HEIGHT: 62, + PIGGY_MARGIN: { + LEFT: 1, + RIGHT: 4, + TOP: -1, + BOTTOM: 1, + }, + + SEARCH_MARGIN: { + LEFT: 180, + RIGHT: 145, + }, + + propTypes: { + currentSearch: React.PropTypes.string, + errors: React.PropTypes.arrayOf( + React.PropTypes.shape({ + error: React.PropTypes.string.isRequired, + onClick: React.PropTypes.func, + url: React.PropTypes.string, + }).isRequired + ).isRequired, + height: React.PropTypes.number.isRequired, + helpURL: React.PropTypes.instanceOf(goog.Uri).isRequired, + homeURL: React.PropTypes.instanceOf(goog.Uri).isRequired, + importersURL: React.PropTypes.instanceOf(goog.Uri).isRequired, + mainControls: React.PropTypes.arrayOf(React.PropTypes.renderable), + onNewPermanode: React.PropTypes.func, + onSearch: React.PropTypes.func, + searchRootsURL: React.PropTypes.instanceOf(goog.Uri).isRequired, + statusURL: React.PropTypes.instanceOf(goog.Uri).isRequired, + timer: React.PropTypes.shape({setTimeout:React.PropTypes.func.isRequired, clearTimeout:React.PropTypes.func.isRequired}).isRequired, + width: React.PropTypes.number.isRequired, + }, + + focusSearch: function() { + this.getSearchNode_().focus(); + this.getSearchNode_().select(); + }, + + getInitialState: function() { + return { + currentSearch: this.props.currentSearch, + menuVisible: false, + }; + }, + + componentWillReceiveProps: function(nextProps) { + if (nextProps.currentSearch != this.props.currentSearch) { + this.setState({currentSearch: nextProps.currentSearch}); + } + }, + + render: function() { + return React.DOM.div( + { + className: 'cam-header', + style: { + width: this.props.width, + }, + }, + React.DOM.table( + { + className: 'cam-header-main', + }, + React.DOM.tr(null, + this.getPiggy_(), + this.getTitle_(), + this.getSearchbox_(), + this.getMainControls_() + ) + ), + this.getMenuDropdown_() + ) + }, + + getPiggy_: function() { + var props = { + sheetWidth: 11, + spriteWidth: this.PIGGY_NATIVE_WIDTH, + spriteHeight: this.PIGGY_NATIVE_HEIGHT, + style: cam.reactUtil.getVendorProps({ + position: 'absolute', + left: this.PIGGY_MARGIN.LEFT, + top: this.PIGGY_MARGIN.TOP, + transform: 'scale(' + this.getPiggyScale_() + ')', + transformOrigin: '0 0', + }), + }; + + var image = function() { + if (this.props.errors.length) { + return cam.SpritedAnimation(cam.object.extend(props, { + key: 'error', + loopDelay: 10 * 1000, + numFrames: 65, + src: 'glitch/npc_piggy__x1_too_much_nibble_png_1354829441.png', + })); + } else { + return cam.SpritedImage(cam.object.extend(props, { + key: 'ok', + index: 5, + src: 'glitch/npc_piggy__x1_chew_png_1354829433.png', + })); + } + }; + + return React.DOM.td( + { + className: 'cam-header-item', + style: { + minWidth: this.getPiggyWidth_() + this.PIGGY_MARGIN.LEFT + this.PIGGY_MARGIN.RIGHT, + }, + onClick: this.handleClick_, + onMouseEnter: this.handleMouseEnter_, + onMouseLeave: this.handleMouseLeave_, + }, + image.call(this) + ) + }, + + getTitle_: function() { + return React.DOM.td( + { + className: 'cam-header-item cam-header-title', + onClick: this.handleClick_, + onMouseEnter: this.handleMouseEnter_, + onMouseLeave: this.handleMouseLeave_, + }, + React.DOM.span(null, 'Pudgy'), + React.DOM.span(null, '\u25BE') + ); + }, + + getSearchbox_: function() { + return React.DOM.td( + { + className: 'cam-header-item', + style: { + width: '100%', + } + }, + React.DOM.form( + { + onSubmit: this.handleSearchSubmit_, + }, + React.DOM.input( + { + onChange: this.handleSearchChange_, + placeholder: 'Search...', + ref: 'searchbox', + value: this.state.currentSearch, + } + ) + ) + ) + }, + + getMainControls_: function() { + return React.DOM.td( + { + className: React.addons.classSet({ + 'cam-header-item': true, + 'cam-header-main-controls': true, + 'cam-header-main-controls-empty': !this.props.mainControls.length, + }), + }, + this.props.mainControls + ); + }, + + getMenuDropdown_: function() { + var errorItems = this.props.errors.map(function(err) { + var children = [ + React.DOM.i({className: 'fa fa-exclamation-circle cam-header-menu-item-icon'}), + err.error + ]; + return this.getMenuItem_(children, err.url, err.onClick, 'cam-header-menu-item-error'); + }, this); + + return React.DOM.div( + { + className: 'cam-header-menu-dropdown', + onClick: this.handleDropdownClick_, + onMouseEnter: this.handleMouseEnter_, + onMouseLeave: this.handleMouseLeave_, + style: cam.reactUtil.getVendorProps({ + transform: 'translate3d(0, ' + this.getMenuTranslate_() + '%, 0)', + }), + }, + this.getMenuItem_('Home', this.props.homeURL), + this.getMenuItem_('Upload...', null, this.props.onUpload), + + // TODO(aa): Create a new permanode UI that delays creating the permanode until the user confirms, then change this to a link to that UI. + // TODO(aa): Also I keep going back and forth about whether we should call this 'permanode' or 'set' in the UI. Hrm. + this.getMenuItem_('New set', null, this.props.onNewPermanode), + + this.getMenuItem_('Importers', this.props.importersURL), + this.getMenuItem_('Server status', this.props.statusURL), + this.getMenuItem_('Search roots', this.props.searchRootsURL), + this.getMenuItem_('Help', this.props.helpURL), + errorItems + ); + }, + + getMenuItem_: function(text, opt_link, opt_onClick, opt_class) { + if (!text || (!opt_onClick && !opt_link)) { + return null; + } + + var className = 'cam-header-menu-item'; + if (opt_class) { + className += ' ' + opt_class; + } + + var ctor = opt_link ? React.DOM.a : React.DOM.div; + return ctor( + { + className: className, + href: opt_link, + onClick: opt_onClick, + }, + text + ); + }, + + getMenuTranslate_: function() { + if (this.state.menuVisible) { + return 0; + } else { + // 110% because it has a shadow that we don't want to double-up with the shadow from the header. + return -110; + } + }, + + getPiggyHeight_: function() { + return this.props.height - this.PIGGY_MARGIN.TOP - this.PIGGY_MARGIN.BOTTOM; + }, + + getPiggyWidth_: function() { + return this.getPiggyScale_() * this.PIGGY_NATIVE_WIDTH; + }, + + getPiggyScale_: function() { + return this.getPiggyHeight_() / this.PIGGY_NATIVE_HEIGHT; + }, + + handleClick_: function() { + this.setState({menuVisible: !this.state.menuVisible}); + }, + + handleMouseEnter_: function() { + this.clearTimer_(); + this.setTimer_(true); + }, + + handleMouseLeave_: function() { + this.clearTimer_(); + this.setTimer_(false); + }, + + handleDropdownClick_: function(e) { + this.clearTimer_(); + this.setState({menuVisible:false}); + }, + + setTimer_: function(show) { + this.timerId_ = this.props.timer.setTimeout(this.handleTimer_.bind(null, show), 250); + }, + + clearTimer_: function() { + if (this.timerId_) { + this.props.timer.clearTimeout(this.timerId_); + } + }, + + handleTimer_: function(show) { + this.setState({menuVisible:show}); + }, + + handleSearchChange_: function(e) { + this.setState({currentSearch: e.target.value}); + }, + + handleSearchSubmit_: function(e) { + this.props.onSearch(this.getSearchNode_().value); + e.preventDefault(); + }, + + getSearchNode_: function() { + return this.refs['searchbox'].getDOMNode(); + }, +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/icon_16716.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/icon_16716.svg new file mode 100644 index 00000000..1e5a8147 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/icon_16716.svg @@ -0,0 +1,58 @@ + +image/svg+xml + + + + + + \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/icon_27307.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/icon_27307.svg new file mode 100644 index 00000000..81278d3c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/icon_27307.svg @@ -0,0 +1,68 @@ + +image/svg+xml + + + + + + + + + + + + \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/image_detail.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/image_detail.js new file mode 100644 index 00000000..c9de256f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/image_detail.js @@ -0,0 +1,183 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.ImageDetail'); + +goog.require('cam.BlobItemVideoContent'); +goog.require('cam.Thumber'); + +// Renders the guts of the detail view for images. +cam.ImageDetail = React.createClass({ + displayName: 'ImageDetail', + + IMG_MARGIN: 20, + PIGGY_WIDTH: 88, + PIGGY_HEIGHT: 62, + + propTypes: { + backwardPiggy: React.PropTypes.bool.isRequired, + height: React.PropTypes.number.isRequired, + permanodeMeta: React.PropTypes.object, + resolvedMeta: React.PropTypes.object.isRequired, + width: React.PropTypes.number.isRequired, + }, + + isVideo_: function() { + return !this.isImage_(); + }, + + isImage_: function() { + return Boolean(this.props.resolvedMeta.image); + }, + + componentWillReceiveProps: function(nextProps) { + if (this.props == nextProps || this.props.resolvedMeta.blobRef != nextProps.resolvedMeta.blobRef) { + this.thumber_ = nextProps.resolvedMeta.image && cam.Thumber.fromImageMeta(nextProps.resolvedMeta); + this.setState({imgHasLoaded: false}); + } + }, + + componentWillMount: function() { + this.componentWillReceiveProps(this.props, true); + }, + + render: function() { + this.imgSize_ = this.getImgSize_(); + return React.DOM.div({className:'detail-view', style: this.getStyle_()}, + this.getImg_(), + this.getPiggy_() + ); + }, + + getSinglePermanodeAttr_: function(name) { + return cam.permanodeUtils.getSingleAttr(this.props.permanodeMeta.permanode, name); + }, + + onImgLoad_: function() { + this.setState({imgHasLoaded:true}); + }, + + getImg_: function() { + var transition = React.addons.TransitionGroup({transitionName: 'detail-img'}, []); + if (this.imgSize_) { + var ctor = this.props.resolvedMeta.image ? React.DOM.img : React.DOM.video; + transition.props.children.push( + ctor({ + className: React.addons.classSet({ + 'detail-view-img': true, + 'detail-view-img-loaded': this.isImage_() ? this.state.imgHasLoaded : true, + }), + controls: true, + // We want each image to have its own node in the DOM so that during the crossfade, we don't see the image jump to the next image's size. + key: 'img' + this.props.resolvedMeta.blobRef, + onLoad: this.isImage_() ? this.onImgLoad_ : null, + src: this.isImage_() ? this.thumber_.getSrc(this.imgSize_.height) : './download/' + this.props.resolvedMeta.blobRef + '/' + this.props.resolvedMeta.file.fileName, + style: this.getCenteredProps_(this.imgSize_.width, this.imgSize_.height) + }) + ); + } + return transition; + }, + + getPiggy_: function() { + var transition = React.addons.TransitionGroup({transitionName: 'detail-piggy'}, []); + if (this.isImage_() && !this.state.imgHasLoaded) { + transition.props.children.push( + cam.SpritedAnimation({ + key: 'piggy-sprite', + src: 'glitch/npc_piggy__x1_walk_png_1354829432.png', + className: React.addons.classSet({ + 'detail-view-piggy': true, + 'detail-view-piggy-backward': this.props.backwardPiggy + }), + numFrames: 24, + spriteWidth: this.PIGGY_WIDTH, + spriteHeight: this.PIGGY_HEIGHT, + sheetWidth: 8, + style: this.getCenteredProps_(this.PIGGY_WIDTH, this.PIGGY_HEIGHT) + })); + } + return transition; + }, + + getCenteredProps_: function(w, h) { + var avail = new goog.math.Size(this.props.width, this.props.height); + return { + top: (avail.height - h) / 2, + left: (avail.width - w) / 2, + width: w, + height: h + } + }, + + getImgSize_: function() { + if (this.isVideo_()) { + return new goog.math.Size(this.props.width, this.props.height); + } + var rawSize = new goog.math.Size(this.props.resolvedMeta.image.width, this.props.resolvedMeta.image.height); + var available = new goog.math.Size( + this.props.width - this.IMG_MARGIN * 2, + this.props.height - this.IMG_MARGIN * 2); + if (rawSize.height <= available.height && rawSize.width <= available.width) { + return rawSize; + } + return rawSize.scaleToFit(available); + }, + + getStyle_: function() { + return { + width: this.props.width, + height: this.props.height + } + }, +}); + +cam.ImageDetail.getAspect = function(blobref, searchSession) { + if (!blobref) { + return null; + } + + var rm = searchSession.getResolvedMeta(blobref); + var pm = searchSession.getMeta(blobref); + + if (!pm) { + return null; + } + + if (pm.camliType != 'permanode') { + pm = null; + } + + // We don't handle camliContentImage like BlobItemImage.getHandler does because that only tells us what image to display in the search results. It doesn't actually make the permanode an image or anything. + if (rm && (rm.image || cam.BlobItemVideoContent.isVideo(rm))) { + return { + fragment: 'image', + title: 'Image', + createContent: function(size, backwardPiggy) { + return cam.ImageDetail({ + backwardPiggy: backwardPiggy, + key: 'image', + height: size.height, + permanodeMeta: pm, + resolvedMeta: rm, + width: size.width, + }); + }, + }; + } else { + return null; + } +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/index.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/index.css new file mode 100644 index 00000000..b5ec048f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/index.css @@ -0,0 +1,49 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +@import (less) "prefix-free.css"; + + +/* http://www.paulirish.com/2012/box-sizing-border-box-ftw/ */ +*, *:before, *:after { + .box-sizing(border-box); +} + +body { + margin: 0; + overflow-x: hidden; + overflow-y: scroll; +} + +.cam-index-page { + font: 16px/1.4 normal Arial, sans-serif; +} + +.cam-index-title { + display: inline-block; +} + +.cam-content-wrap { + position: relative; +} + +.cam-unselectable { + .user-select(none); +} + +.cam-index-upload-dialog>* { + vertical-align: middle; +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/index.html b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/index.html new file mode 100644 index 00000000..6c4c47e3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/index.html @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/index.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/index.js new file mode 100644 index 00000000..50a24476 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/index.js @@ -0,0 +1,1084 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// IndexPage is the top level React component class that owns all the other +// components of the web UI. +// See the React documentation and in particular +// https://facebook.github.io/react/docs/component-specs.html to learn about +// components. +goog.provide('cam.IndexPage'); + +goog.require('goog.array'); +goog.require('goog.dom'); +goog.require('goog.dom.classlist'); +goog.require('goog.events.EventHandler'); +goog.require('goog.format'); +goog.require('goog.functions'); +goog.require('goog.labs.Promise'); +goog.require('goog.object'); +goog.require('goog.string'); +goog.require('goog.Uri'); + +goog.require('cam.BlobDetail'); +goog.require('cam.BlobItemContainerReact'); +goog.require('cam.BlobItemDemoContent'); +goog.require('cam.BlobItemFoursquareContent'); +goog.require('cam.BlobItemGenericContent'); +goog.require('cam.BlobItemImageContent'); +goog.require('cam.BlobItemTwitterContent'); +goog.require('cam.BlobItemVideoContent'); +goog.require('cam.blobref'); +goog.require('cam.DetailView'); +goog.require('cam.Dialog'); +goog.require('cam.DirectoryDetail'); +goog.require('cam.Header'); +goog.require('cam.Navigator'); +goog.require('cam.PermanodeDetail'); +goog.require('cam.permanodeUtils'); +goog.require('cam.reactUtil'); +goog.require('cam.SearchSession'); +goog.require('cam.ServerConnection'); +goog.require('cam.Sidebar'); +goog.require('cam.TagsControl'); + +cam.IndexPage = React.createClass({ + displayName: 'IndexPage', + + SIDEBAR_OPEN_WIDTH_: 250, + + HEADER_HEIGHT_: 38, + SEARCH_PREFIX_: { + RAW: 'raw' + }, + THUMBNAIL_SIZE_: 200, + + SEARCH_SESSION_CACHE_SIZE_: 3, + + // Note that these are ordered by priority. + BLOB_ITEM_HANDLERS_: [ + cam.BlobItemDemoContent.getHandler, + cam.BlobItemFoursquareContent.getHandler, + cam.BlobItemTwitterContent.getHandler, + cam.BlobItemImageContent.getHandler, + cam.BlobItemVideoContent.getHandler, + cam.BlobItemGenericContent.getHandler + ], + + BLOBREF_PATTERN_: new RegExp('^' + cam.blobref.PATTERN + '$'), + + propTypes: { + availWidth: React.PropTypes.number.isRequired, + availHeight: React.PropTypes.number.isRequired, + config: React.PropTypes.object.isRequired, + eventTarget: React.PropTypes.shape({addEventListener:React.PropTypes.func.isRequired}).isRequired, + history: React.PropTypes.shape({pushState:React.PropTypes.func.isRequired, replaceState:React.PropTypes.func.isRequired, go:React.PropTypes.func.isRequired, state:React.PropTypes.object}).isRequired, + openWindow: React.PropTypes.func.isRequired, + location: React.PropTypes.shape({href:React.PropTypes.string.isRequired, reload:React.PropTypes.func.isRequired}).isRequired, + scrolling: cam.BlobItemContainerReact.originalSpec.propTypes.scrolling, + serverConnection: React.PropTypes.instanceOf(cam.ServerConnection).isRequired, + timer: cam.Header.originalSpec.propTypes.timer, + }, + + // Invoked once right before initial rendering. This is essentially IndexPage's + // constructor. We populate non-React helpers that live for the entire lifetime + // of IndexPage here. + componentWillMount: function() { + this.baseURL_ = null; + this.dragEndTimer_ = 0; + this.navigator_ = null; + this.searchSessionCache_ = []; + this.targetSearchSession_ = null; + this.childSearchSession_ = null; + + this.eh_ = new goog.events.EventHandler(this); + + var newURL = new goog.Uri(this.props.location.href); + this.baseURL_ = newURL.resolve(new goog.Uri(this.props.config.uiRoot)); + + this.navigator_ = new cam.Navigator(this.props.eventTarget, this.props.location, this.props.history); + this.navigator_.onWillNavigate = this.handleWillNavigate_; + this.navigator_.onDidNavigate = this.handleDidNavigate_; + + this.handleWillNavigate_(newURL); + this.handleDidNavigate_(); + }, + + // Invoked right after initial rendering. + componentDidMount: function() { + // TODO(aa): This supports some of the old iframed pages. We can remove it once they are dead. + goog.global.getSearchSession = function() { + return this.childSearchSession_; + }.bind(this); + this.eh_.listen(this.props.eventTarget, 'keypress', this.handleKeyPress_); + this.eh_.listen(this.props.eventTarget, 'keyup', this.handleKeyUp_); + }, + + componentWillUnmount: function() { + this.eh_.dispose(); + this.clearDragTimer_(); + }, + + // Invoked once before everything else on initial rendering. Values are + // subsequently in this.state. We use this to set the initial state and + // also to document what state fields are possible + getInitialState: function() { + return { + backwardPiggy: false, + currentURL: null, + currentSet: '', + dropActive: false, + selection: {}, + serverStatus: null, + + // TODO: This should be calculated by whether selection is empty, and not need separate state. + sidebarVisible: false, + + uploadDialogVisible: false, + totalBytesToUpload: 0, + totalBytesComplete: 0, + }; + }, + + // render() is called by React every time a component is determined to need + // re-rendering. This is typically caused by a call to setState() or a parent + // component re-rendering. + render: function() { + var aspects = this.getAspects_(); + var selectedAspect = goog.array.findIndex(aspects, function(v) { + return v.fragment == this.state.currentURL.getFragment(); + }, this); + + if (selectedAspect == -1) { + selectedAspect = 0; + } + + var contentSize = new goog.math.Size(this.props.availWidth, this.props.availHeight - this.HEADER_HEIGHT_); + return React.DOM.div({onDragEnter:this.handleDragStart_, onDragOver:this.handleDragStart_, onDrop:this.handleDrop_}, + this.getHeader_(aspects, selectedAspect), + React.DOM.div( + { + className: 'cam-content-wrap', + style: { + top: this.HEADER_HEIGHT_, + }, + }, + aspects[selectedAspect] && aspects[selectedAspect].createContent(contentSize, this.state.backwardPiggy) + ), + this.getSidebar_(aspects[selectedAspect]), + this.getUploadDialog_() + ); + }, + + setSelection_: function(selection) { + this.props.history.replaceState(cam.object.extend(this.props.history.state, { + selection: selection, + }), '', this.props.location.href); + + this.setState({selection: selection}); + this.setState({sidebarVisible: !goog.object.isEmpty(selection)}); + }, + + getTargetBlobref_: function(opt_url) { + var url = opt_url || this.state.currentURL; + var suffix = url.getPath().substr(this.baseURL_.getPath().length); + + // TODO(aa): Need to implement something like ref.go that knows about the other hash types. + var match = suffix.match(this.BLOBREF_PATTERN_); + return match && match[0]; + }, + + getAspects_: function() { + var childFrameClickHandler = this.navigator_.navigate.bind(this.navigator_); + var target = this.getTargetBlobref_(); + var getAspect = function(f) { + return f(target, this.targetSearchSession_); + }.bind(this); + + var specificAspects = [ + cam.ImageDetail.getAspect, + cam.DirectoryDetail.getAspect.bind(null, this.baseURL_, childFrameClickHandler), + ].map(getAspect).filter(goog.functions.identity); + + var generalAspects = [ + this.getSearchAspect_.bind(null, specificAspects), + cam.PermanodeDetail.getAspect.bind(null, this.props.serverConnection, this.props.timer), + cam.BlobDetail.getAspect.bind(null, this.getDetailURL_, this.props.serverConnection), + ].map(getAspect).filter(goog.functions.identity); + + return specificAspects.concat(generalAspects); + }, + + getSearchAspect_: function(specificAspects, blobref, targetSearchSession) { + if (blobref) { + var m = targetSearchSession.getMeta(blobref); + if (!m || !m.permanode) { + // We have a target, but it's not a permanode. So don't show the contents view. + // TODO(aa): Maybe we do want to for directories though? + return null; + } + + // If the permanode already has children, we always show the container view. + // Otherwise, show the container view only if there is no more specific type. + var showSearchAspect = false; + if (cam.permanodeUtils.isContainer(m.permanode)) { + showSearchAspect = true; + } else if (!cam.permanodeUtils.getCamliNodeType(m.permanode) && specificAspects.length == 0) { + showSearchAspect = true; + } + + if (!showSearchAspect) { + return null; + } + } + + // This can happen when a user types a raw (JSON) query that is invalid. + if (!this.childSearchSession_) { + return null; + } + + return { + title: blobref ? 'Contents' : 'Search', + fragment: blobref ? 'contents': 'search', + createContent: this.getBlobItemContainer_.bind(null, this), + }; + }, + + handleDragStart_: function(e) { + this.clearDragTimer_(); + e.preventDefault(); + this.dragEndTimer_ = window.setTimeout(this.handleDragStop_, 2000); + this.setState({ + dropActive: true, + uploadDialogVisible: false, + }); + }, + + handleDragStop_: function() { + this.clearDragTimer_(); + this.setState({dropActive: false}); + }, + + clearDragTimer_: function() { + if (this.dragEndTimer_) { + window.clearTimeout(this.dragEndTimer_); + this.dragEndTimer_ = 0; + } + }, + + onUploadStart_: function(files) { + var numFiles = files.length; + var totalBytes = Array.prototype.reduce.call(files, function(sum, file) { return sum + file.size; }, 0); + + this.setState({ + dropActive: false, + totalBytesToUpload: totalBytes, + totalBytesComplete: 0, + }); + + console.log('Uploading %d files (%d bytes)...', numFiles, totalBytes); + }, + + onUploadProgressUpdate_: function(file) { + var completedBytes = this.state.totalBytesComplete + file.size; + + this.setState({ + totalBytesComplete: completedBytes + }); + + console.log('Uploaded %d of %d bytes', completedBytes, this.state.totalBytesToUpload); + }, + + onUploadComplete_: function() { + console.log('Upload complete!'); + + this.setState({ + totalBytesToUpload: 0, + totalBytesComplete: 0, + }); + }, + + handleDrop_: function(e) { + if (!e.nativeEvent.dataTransfer.files) { + return; + } + + e.preventDefault(); + + var files = e.nativeEvent.dataTransfer.files; + var sc = this.props.serverConnection; + + this.onUploadStart_(files); + + goog.labs.Promise.all( + Array.prototype.map.call(files, function(file) { + return uploadFile(file) + .then(fetchExistingPermanode) + .then(createPermanodeIfNotExists) + .then(nameResults) + .then(createPermanodeAssociations.bind(this)) + .thenCatch(function(e) { + console.error('File upload fall down go boom. file: %s, error: %s', file.name, e); + }) + .then(this.onUploadProgressUpdate_.bind(this, file)); + }.bind(this)) + ).thenCatch(function(e) { + console.error('File upload failed with error: %s', e); + }).then(this.onUploadComplete_); + + function uploadFile(file) { + var uploadFile = new goog.labs.Promise(sc.uploadFile.bind(sc, file)); + return goog.labs.Promise.all([uploadFile]); + } + + function fetchExistingPermanode(blobIds) { + var fileRef = blobIds[0]; + var fileUploaded = new goog.labs.Promise.resolve(fileRef); + var getPermanode = new goog.labs.Promise(sc.getPermanodeWithContent.bind(sc, fileRef)); + return goog.labs.Promise.all([fileUploaded, getPermanode]); + } + + function createPermanodeIfNotExists(results) { + var fileRef = results[0]; + var permanode = results[1]; + if (!permanode) { + var fileUploaded = new goog.labs.Promise.resolve(fileRef); + var createPermanode = new goog.labs.Promise(sc.createPermanode.bind(sc)); + return goog.labs.Promise.all([fileUploaded, createPermanode]); + } + // Empty values so the next in chain knows that we're in the "permanode already exists" case. + return goog.labs.Promise.resolve(["", ""]); + } + + // 'readable-ify' the blob references returned from upload/create + function nameResults(blobIds) { + return { + 'fileRef': blobIds[0], + 'permanodeRef': blobIds[1] + }; + } + + function createPermanodeAssociations(refs) { + if (refs.permanodeRef == "") { + // Any value would do, but boolean helps make it clear that we end + // here, by resolving the file upload promise chain. + return goog.labs.Promise.resolve(true); + } + + // associate uploaded file to new permanode + var camliContent = new goog.labs.Promise(sc.newSetAttributeClaim.bind(sc, refs.permanodeRef, 'camliContent', refs.fileRef)); + var promises = [camliContent]; + + // if currently viewing a set, make new permanode a member of the set + var parentPermanodeRef = this.getTargetBlobref_(); + if (parentPermanodeRef) { + var camliMember = new goog.labs.Promise(sc.newAddAttributeClaim.bind(sc, parentPermanodeRef, 'camliMember', refs.permanodeRef)); + promises.push(camliMember); + } + + return goog.labs.Promise.all(promises); + } + }, + + handleWillNavigate_: function(newURL) { + if (!goog.string.startsWith(newURL.toString(), this.baseURL_.toString())) { + return false; + } + + var targetBlobref = this.getTargetBlobref_(newURL); + this.updateTargetSearchSession_(targetBlobref, newURL); + this.updateChildSearchSession_(targetBlobref, newURL); + this.pruneSearchSessionCache_(); + this.setState({ + backwardPiggy: false, + currentURL: newURL, + }); + return true; + }, + + handleDidNavigate_: function() { + var s = this.props.history.state && this.props.history.state.selection; + this.setSelection_(s || {}); + }, + + updateTargetSearchSession_: function(targetBlobref, newURL) { + this.targetSearchSession_ = null; + if (targetBlobref) { + var query = this.queryAsBlob_(targetBlobref); + var parentPermanode = newURL.getParameterValue('p'); + if (parentPermanode) { + query = this.queryFromParentPermanode_(parentPermanode); + } else { + var queryString = newURL.getParameterValue('q'); + if (queryString) { + query = this.queryFromSearchParam_(queryString); + } + } + this.targetSearchSession_ = this.getSearchSession_(targetBlobref, query); + } + }, + + updateChildSearchSession_: function(targetBlobref, newURL) { + var query = ' '; + if (targetBlobref) { + query = this.queryFromParentPermanode_(targetBlobref); + } else { + var queryString = newURL.getParameterValue('q'); + if (queryString) { + query = this.queryFromSearchParam_(queryString); + } + } + this.childSearchSession_ = this.getSearchSession_(null, query); + }, + + queryFromSearchParam_: function(queryString) { + // TODO(aa): Remove this when the server can do something like the 'raw' operator. + if (goog.string.startsWith(queryString, this.SEARCH_PREFIX_.RAW + ':')) { + try { + return JSON.parse(queryString.substring(this.SEARCH_PREFIX_.RAW.length + 1)); + } catch (e) { + console.error('Raw search is invalid JSON', e); + return null; + } + } else { + return queryString; + } + }, + + queryFromParentPermanode_: function(blobRef) { + return { + permanode: { + relation: { + relation: 'parent', + any: { blobRefPrefix: blobRef }, + }, + }, + }; + }, + + queryAsBlob_: function(blobRef) { + return { + blobRefPrefix: blobRef, + } + }, + + // Finds an existing cached SearchSession that meets criteria, or creates a new one. + // + // If opt_query is present, the returned query must be exactly equivalent. + // If opt_targetBlobref is present, the returned query must have current results that contain opt_targetBlobref. Otherwise, the returned query must contain the first result. + // + // If only opt_targetBlobref is set, then any query that happens to currently contain that blobref is acceptable to the caller. + getSearchSession_: function(opt_targetBlobref, opt_query) { + // This whole business of reusing search session relies on the assumption that we use the same describe rules for both detail queries and search queries. + var queryString = JSON.stringify(opt_query); + + var cached = goog.array.findIndex(this.searchSessionCache_, function(ss) { + if (opt_targetBlobref) { + if (!ss.getMeta(opt_targetBlobref)) { + return false; + } + if (!opt_query) { + return true; + } + } + + if (JSON.stringify(ss.getQuery()) != queryString) { + return false; + } + + if (!opt_targetBlobref) { + return !ss.getAround(); + } + + // If there's a targetBlobref, we require that it is not at the very edge of the results so that we can implement lefr/right in detail views. + var targetIndex = goog.array.findIndex(ss.getCurrentResults().blobs, function(b) { + return b.blob == opt_targetBlobref; + }); + return (targetIndex > 0) && (targetIndex < (ss.getCurrentResults().blobs.length - 1)); + }); + + if (cached > -1) { + this.searchSessionCache_.splice(0, 0, this.searchSessionCache_.splice(cached, 1)[0]); + return this.searchSessionCache_[0]; + } + + console.log('Creating new search session for query %s', queryString); + var ss = new cam.SearchSession(this.props.serverConnection, this.baseURL_.clone(), opt_query, opt_targetBlobref); + this.eh_.listen(ss, cam.SearchSession.SEARCH_SESSION_CHANGED, function() { + this.forceUpdate(); + }); + this.eh_.listen(ss, cam.SearchSession.SEARCH_SESSION_STATUS, function(e) { + this.setState({ + serverStatus: e.status, + }); + }); + this.eh_.listen(ss, cam.SearchSession.SEARCH_SESSION_ERROR, function() { + this.forceUpdate(); + }); + ss.loadMoreResults(); + this.searchSessionCache_.splice(0, 0, ss); + return ss; + }, + + pruneSearchSessionCache_: function() { + for (var i = this.SEARCH_SESSION_CACHE_SIZE_; i < this.searchSessionCache_.length; i++) { + this.searchSessionCache_[i].close(); + } + + this.searchSessionCache_.length = Math.min(this.searchSessionCache_.length, this.SEARCH_SESSION_CACHE_SIZE_); + }, + + getHeader_: function(aspects, selectedAspectIndex) { + // We don't show the chooser if there's only one thing to choose from. + if (aspects.length == 1) { + aspects = []; + } + + // TODO(aa): It would be cool to normalize the query and single target case, by supporting searches like is:, that way we can always show something in the searchbox, even when we're not in a listview. + var target = this.getTargetBlobref_(); + var query = ''; + if (target) { + query = 'ref:' + target; + } else { + query = this.state.currentURL.getParameterValue('q') || ''; + } + + return cam.Header( + { + currentSearch: query, + errors: this.getErrors_(), + height: 38, + helpURL: this.baseURL_.resolve(new goog.Uri(this.props.config.helpRoot)), + homeURL: this.baseURL_, + importersURL: this.baseURL_.resolve(new goog.Uri(this.props.config.importerRoot)), + mainControls: aspects.map(function(val, idx) { + return React.DOM.a( + { + key: val.title, + className: React.addons.classSet({ + 'cam-header-main-control-active': idx == selectedAspectIndex, + }), + href: this.state.currentURL.clone().setFragment(val.fragment).toString(), + }, + val.title + ); + }, this), + onUpload: this.handleUpload_, + onNewPermanode: this.handleCreateSetWithSelection_, + onSearch: this.setSearch_, + searchRootsURL: this.getSearchRootsURL_(), + statusURL: this.baseURL_.resolve(new goog.Uri(this.props.config.statusRoot)), + ref: 'header', + timer: this.props.timer, + width: this.props.availWidth, + } + ) + }, + + handleNewPermanode_: function() { + this.props.serverConnection.createPermanode(this.getDetailURL_.bind(this)); + }, + + getSearchRootsURL_: function() { + return this.baseURL_.clone().setParameterValue( + 'q', + this.SEARCH_PREFIX_.RAW + ':' + JSON.stringify({ + permanode: { + attr: 'camliRoot', + numValue: { + min: 1 + } + } + }) + ); + }, + + handleSelectAsCurrentSet_: function() { + this.setState({ + currentSet: goog.object.getAnyKey(this.state.selection), + }); + this.setSelection_({}); + alert('Now, select the items to add to this set and click "Add to picked set" in the sidebar.\n\n' + + 'Sorry this is lame, we\'re working on it.'); + }, + + handleAddToSet_: function() { + this.addMembersToSet_(this.state.currentSet, goog.object.getKeys(this.state.selection)); + alert('Done!'); + }, + + handleUpload_: function() { + this.setState({ + uploadDialogVisible: true, + }); + }, + + handleCreateSetWithSelection_: function() { + var selection = goog.object.getKeys(this.state.selection); + this.props.serverConnection.createPermanode(function(permanode) { + this.props.serverConnection.newSetAttributeClaim(permanode, 'title', 'New set', function() { + this.addMembersToSet_(permanode, selection); + }.bind(this)); + }.bind(this)); + }, + + addMembersToSet_: function(permanode, blobrefs) { + var numComplete = -1; + var callback = function() { + if (++numComplete == blobrefs.length) { + this.setSelection_({}); + this.refreshIfNecessary_(); + this.navigator_.navigate(this.getDetailURL_(permanode)); + } + }.bind(this); + + callback(); + + blobrefs.forEach(function(br) { + this.props.serverConnection.newAddAttributeClaim(permanode, 'camliMember', br, callback); + }.bind(this)); + }, + + handleClearSelection_: function() { + this.setSelection_({}); + }, + + handleDeleteSelection_: function() { + var blobrefs = goog.object.getKeys(this.state.selection); + var msg = 'Delete'; + if (blobrefs.length > 1) { + msg += goog.string.subs(' %s items?', blobrefs.length); + } else { + msg += ' item?'; + } + if (!confirm(msg)) { + return null; + } + + var numDeleted = 0; + blobrefs.forEach(function(br) { + this.props.serverConnection.newDeleteClaim(br, function() { + if (++numDeleted == blobrefs.length) { + this.setSelection_({}); + this.refreshIfNecessary_(); + } + }.bind(this)); + }.bind(this)); + }, + + handleOpenWindow_: function(url) { + this.props.openWindow(url); + }, + + handleKeyPress_: function(e) { + if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') { + return; + } + + switch (String.fromCharCode(e.charCode)) { + case '/': { + this.refs['header'].focusSearch(); + e.preventDefault(); + break; + } + + case '|': { + window.__debugConsoleClient = { + getSelectedItems: function() { + return this.state.selection; + }.bind(this), + serverConnection: this.props.serverConnection, + }; + window.open('debug_console.html', 'debugconsole', 'width=400,height=300'); + break; + } + } + }, + + handleKeyUp_: function(e) { + var isEsc = (e.keyCode == 27); + var isRight = (e.keyCode == 39); + var isLeft = (e.keyCode == 37); + + if (isEsc) { + // TODO: This isn't right, it should go back to the context URL if there is one. + this.navigator_.navigate(this.baseURL_); + return; + } + + if (!isRight && !isLeft) { + return; + } + + if (!this.targetSearchSession_) { + return; + } + + var blobs = this.targetSearchSession_.getCurrentResults().blobs; + var target = this.getTargetBlobref_(); + var idx = goog.array.findIndex(blobs, function(item) { + return item.blob == target; + }); + + if (isRight) { + if (idx >= (blobs.length - 1)) { + return; + } + idx++; + } else { + if (idx <= 0) { + return; + } + idx--; + } + + var url = this.getDetailURL_(blobs[idx].blob, this.state.currentURL.getFragment()); + ['q', 'p'].forEach(function(p) { + var v = this.state.currentURL.getParameterValue(p); + if (v) { + url.setParameterValue(p, v); + } + }, this); + this.navigator_.navigate(url); + this.setState({ + backwardPiggy: isLeft, + }); + }, + + handleDetailURL_: function(blobref) { + return this.getChildDetailURL_(blobref); + }, + + getChildDetailURL_: function(blobref, opt_fragment) { + var query = this.state.currentURL.getParameterValue('q'); + var targetBlobref = this.getTargetBlobref_(); + var url = this.getDetailURL_(blobref, opt_fragment); + if (targetBlobref) { + url.setParameterValue('p', targetBlobref); + } else { + url.setParameterValue('q', query || ' '); + } + return url; + }, + + getDetailURL_: function(blobref, opt_fragment) { + var query = this.state.currentURL.getParameterValue('q'); + var targetBlobref = this.getTargetBlobref_(); + return url = this.baseURL_.clone().setPath(this.baseURL_.getPath() + blobref).setFragment(opt_fragment || ''); + }, + + setSearch_: function(query) { + var searchURL; + var match = query.match(/^ref:(.+)/); + if (match) { + searchURL = this.getDetailURL_(match[1]); + } else { + searchURL = this.baseURL_.clone().setParameterValue('q', query); + } + this.navigator_.navigate(searchURL); + }, + + getSelectAsCurrentSetItem_: function() { + if (goog.object.getCount(this.state.selection) != 1) { + return null; + } + + var blobref = goog.object.getAnyKey(this.state.selection); + var m = this.childSearchSession_.getMeta(blobref); + if (!m || m.camliType != 'permanode') { + return null; + } + + return React.DOM.button( + { + key:'selectascurrent', + onClick:this.handleSelectAsCurrentSet_ + }, + 'Add items to set' + ); + }, + + getAddToCurrentSetItem_: function() { + if (!this.state.currentSet) { + return null; + } + + return React.DOM.button( + { + key:'addtoset', + onClick:this.handleAddToSet_ + }, + 'Add to picked set' + ); + }, + + getCreateSetWithSelectionItem_: function() { + return React.DOM.button( + { + key:'createsetwithselection', + onClick:this.handleCreateSetWithSelection_ + }, + 'Create set with items' + ); + }, + + getClearSelectionItem_: function() { + return React.DOM.button( + { + key:'clearselection', + onClick:this.handleClearSelection_ + }, + 'Clear selection' + ); + }, + + getDeleteSelectionItem_: function() { + return React.DOM.button( + { + key:'deleteselection', + onClick:this.handleDeleteSelection_ + }, + 'Delete items' + ); + }, + + getViewOriginalSelectionItem_: function() { + if (goog.object.getCount(this.state.selection) != 1) { + return null; + } + + var blobref = goog.object.getAnyKey(this.state.selection); + var rm = this.childSearchSession_.getResolvedMeta(blobref); + if (!rm || !rm.file) { + return null; + } + + var fileName = ''; + if (rm.file.fileName) { + fileName = goog.string.subs('/%s', rm.file.fileName); + } + + var downloadUrl = goog.string.subs('%s%s%s', this.props.config.downloadHelper, rm.blobRef, fileName); + return React.DOM.button( + { + key:'viewSelection', + onClick: this.handleOpenWindow_.bind(null, downloadUrl), + }, + 'View original' + ); + }, + + getSidebar_: function(selectedAspect) { + if (selectedAspect) { + if (selectedAspect.fragment == 'search' || selectedAspect.fragment == 'contents') { + var count = goog.object.getCount(this.state.selection); + return cam.Sidebar( { + isExpanded: this.state.sidebarVisible, + header: React.DOM.span( + { + className: 'header', + }, + goog.string.subs('%s selected item%s', count, count > 1 ? 's' : '') + ), + mainControls: [ + { + "displayTitle": "Update tags", + "control": this.getTagsControl_() + } + ].filter(goog.functions.identity), + selectionControls: [ + this.getClearSelectionItem_(), + this.getCreateSetWithSelectionItem_(), + this.getSelectAsCurrentSetItem_(), + this.getAddToCurrentSetItem_(), + this.getDeleteSelectionItem_(), + this.getViewOriginalSelectionItem_(), + ].filter(goog.functions.identity), + selectedItems: this.state.selection + }); + } + } + + return null; + }, + + getTagsControl_: function() { + return cam.TagsControl( + { + selectedItems: this.state.selection, + searchSession: this.childSearchSession_, + serverConnection: this.props.serverConnection + } + ); + }, + + isUploading_: function() { + return this.state.totalBytesToUpload > 0; + }, + + getUploadDialog_: function() { + if (!this.state.uploadDialogVisible && !this.state.dropActive && !this.state.totalBytesToUpload) { + return null; + } + + var piggyWidth = 88; + var piggyHeight = 62; + var borderWidth = 18; + var w = this.props.availWidth * 0.8; + var h = this.props.availHeight * 0.8; + var iconProps = { + key: 'icon', + sheetWidth: 10, + spriteWidth: piggyWidth, + spriteHeight: piggyHeight, + style: { + 'margin-right': 3, + position: 'relative', + display: 'inline-block', + } + }; + + function getIcon() { + if (this.isUploading_()) { + return cam.SpritedAnimation(cam.object.extend(iconProps, { + numFrames: 48, + src: 'glitch/npc_piggy__x1_chew_png_1354829433.png', + })); + } else if (this.state.dropActive) { + return cam.SpritedAnimation(cam.object.extend(iconProps, { + loopDelay: 4000, + numFrames: 48, + src: 'glitch/npc_piggy__x1_look_screen_png_1354829434.png', + startFrame: 6, + })); + } else { + return cam.SpritedImage(cam.object.extend(iconProps, { + index: 0, + src: 'glitch/npc_piggy__x1_look_screen_png_1354829434.png', + })); + } + } + + function getText() { + if (this.isUploading_()) { + return goog.string.subs('Uploaded %s (%s%)', + goog.format.numBytesToString(this.state.totalBytesComplete, 2), + getUploadProgressPercent.call(this)); + } else { + return 'Drop files here to upload...'; + } + } + + function getUploadProgressPercent() { + if (!this.state.totalBytesToUpload) { + return 0; + } + + return Math.round(100 * (this.state.totalBytesComplete / this.state.totalBytesToUpload)); + } + + return cam.Dialog( + { + availWidth: this.props.availWidth, + availHeight: this.props.availHeight, + width: w, + height: h, + borderWidth: borderWidth, + onClose: this.state.uploadDialogVisible ? this.handleCloseUploadDialog_ : null, + }, + React.DOM.div( + { + className: 'cam-index-upload-dialog', + style: { + 'text-align': 'center', + position: 'relative', + left: -piggyWidth / 2, + top: (h - piggyHeight - borderWidth * 2) / 2, + }, + }, + getIcon.call(this), + getText.call(this) + ) + ); + }, + + handleCloseUploadDialog_: function() { + this.setState({ + uploadDialogVisible: false, + }); + }, + + handleSelectionChange_: function(newSelection) { + this.setSelection_(newSelection); + }, + + getBlobItemContainer_: function() { + var sidebarClosedWidth = this.props.availWidth; + var sidebarOpenWidth = sidebarClosedWidth - this.SIDEBAR_OPEN_WIDTH_; + var scale = sidebarOpenWidth / sidebarClosedWidth; + + return cam.BlobItemContainerReact({ + key: 'blobitemcontainer', + ref: 'blobItemContainer', + availHeight: this.props.availHeight, + availWidth: this.props.availWidth, + detailURL: this.handleDetailURL_, + handlers: this.BLOB_ITEM_HANDLERS_, + history: this.props.history, + onSelectionChange: this.handleSelectionChange_, + scale: scale, + scaleEnabled: this.state.sidebarVisible, + scrolling: this.props.scrolling, + searchSession: this.childSearchSession_, + selection: this.state.selection, + style: this.getBlobItemContainerStyle_(), + thumbnailSize: this.THUMBNAIL_SIZE_, + }); + }, + + getBlobItemContainerStyle_: function() { + return { + left: 0, + overflowY: this.state.dropActive ? 'hidden' : '', + position: 'absolute', + top: 0, + }; + }, + + getContentWidth_: function() { + return this.props.availWidth; + }, + + refreshIfNecessary_: function() { + if (this.targetSearchSession_) { + this.targetSearchSession_.refreshIfNecessary(); + } + if (this.childSearchSession_) { + this.childSearchSession_.refreshIfNecessary(); + } + }, + + getErrors_: function() { + var errors = (this.state.serverStatus && this.state.serverStatus.errors) || []; + if ((this.targetSearchSession_ && this.targetSearchSession_.hasSocketError()) || + (this.childSearchSession_ && this.childSearchSession_.hasSocketError())) { + errors.push({ + error: 'WebSocket error - click to reload', + onClick: this.props.location.reload.bind(null, this.props.location, true), + }); + } + return errors; + }, +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/js-notes.txt b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/js-notes.txt new file mode 100644 index 00000000..cbf76e2a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/js-notes.txt @@ -0,0 +1,16 @@ +FormData +http://hacks.mozilla.org/2010/05/formdata-interface-coming-to-firefox/ + +window.atob / window.btoa +https://developer.mozilla.org/en/DOM/window.atob +http://demos.hacks.mozilla.org/openweb/imageUploader/js/extends/xhr.js + +File Writer +http://www.w3.org/TR/file-writer-api/ + +File API +http://www.w3.org/TR/2009/WD-FileAPI-20091117/ + +Uint8Array +https://developer.mozilla.org/en/JavaScript_typed_arrays/Uint8Array + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/magnifying_glass.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/magnifying_glass.svg new file mode 100644 index 00000000..a66a72f7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/magnifying_glass.svg @@ -0,0 +1,73 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/math.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/math.js new file mode 100644 index 00000000..da680b59 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/math.js @@ -0,0 +1,20 @@ +goog.provide('cam.math'); + +goog.require('goog.math.Coordinate'); +goog.require('goog.math.Size'); + +// @param goog.math.Size subject +// @param goog.math.Size frame +// @param =boolean opt_bleed If true, subject will be scaled such that its area is greater or equal to frame. Otherwise, it will be scaled such that its area is less than or equal to frame. +// @return goog.math.Size +cam.math.scaleToFit = function(subject, frame, opt_bleed) { + var s = (!opt_bleed && subject.aspectRatio() > frame.aspectRatio()) || (opt_bleed && subject.aspectRatio() <= frame.aspectRatio()) ? frame.width / subject.width : frame.height / subject.height; + return subject.scale(s); +}; + +// @param goog.math.Size subject +// @param goog.math.Size frame +// @return goog.math.Coordinate the left and top coordinat subject should be positioned at relative to frame to be centered within it. This might be negative if subject is larger than frame. +cam.math.center = function(subject, frame) { + return new goog.math.Coordinate((frame.width - subject.width) / 2, (frame.height - subject.height) / 2); +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/mobile.html b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/mobile.html new file mode 100644 index 00000000..f310eae5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/mobile.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/mobile_setup.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/mobile_setup.css new file mode 100644 index 00000000..99d422ec --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/mobile_setup.css @@ -0,0 +1,70 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mobile-setup-page { + background-color: rgb(51, 51, 51); + color: rgba(255, 255, 255, 1); + font-family: 'Open Sans', sans-serif; + font-size: 19px; + margin: 0 auto; + padding-top: 9px; + width: 456px; +} + +.mobile-setup-page img { + height: 456px; + width: 456px; +} + +.mobile-setup-page label { + display: block; + padding: 8px 0; +} + +.mobile-setup-page input { + background: none; + border: 1px rgba(255, 255, 255, 0); + border-style: solid none; + color: rgba(255, 255, 255, 1); + cursor: default; + font-family: 'Open Sans', sans-serif; + font-size: 19px; + margin: 0; + outline: none; + padding: 0; +} + +.mobile-setup-page .mobile-setup-auto-upload input { + float: right; + margin: 9px; +} + +.mobile-setup-page .mobile-setup-helptext { + display: block; + font-size: 16px; + font-weight: lighter; +} + +.mobile-setup-page .mobile-setup-max-cache-size input[type="text"] { + border-bottom: 1px solid rgba(255, 255, 255, 0.25); + width: 5em; + text-align: right; +} + +.mobile-setup-page input[type="text"] { + border-bottom: 1px solid rgba(255, 255, 255, 0.25); + width: 100%; +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/mobile_setup.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/mobile_setup.js new file mode 100644 index 00000000..b741b651 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/mobile_setup.js @@ -0,0 +1,129 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +goog.provide('cam.MobileSetupView'); + +goog.require('goog.Uri'); + +cam.MobileSetupView = React.createClass({ + displayName: 'MobileSetupView', + + propTypes: { + baseURL: React.PropTypes.object.isRequired, + defaultUsername: React.PropTypes.string.isRequired, + }, + + getInitialState: function() { + var serverURL = this.props.baseURL.clone().setPath('').setQuery(''); + return { + autoUpload: false, + // TODO(wathiede): autopopulate this, not sure how. + certFingerprint: '', + maxCacheSize: 256, + server: serverURL.toString() + }; + }, + + getQRURL_: function() { + // TODO(wathiede): I'm not sure what the Android and iPhone requirements are for registering a URL handler are. If they can't be the same for both platforms, then we'll need this to be conditional based on a checkbox in the form. + var settingsURL = goog.Uri.parse('camli://settings/'); + if (this.state.username != '') { + settingsURL.setParameterValue('username', this.state.username); + } + if (this.state.server != '') { + settingsURL.setParameterValue('server', this.state.server); + } + if (this.state.autoUpload) { + settingsURL.setParameterValue('autoUpload', 1); + } + settingsURL.setParameterValue('maxCacheSize', this.state.maxCacheSize); + if (this.state.certFingerprint != '') { + settingsURL.setParameterValue('certFingerprint', this.state.certFingerprint); + } + + var qrURL = this.props.baseURL.clone(); + qrURL.setPath(qrURL.getPath() + '/qr/').setParameterValue('url', settingsURL.toString()); + return qrURL.toString(); + }, + + handleServerChange_: function(e) { + this.setState({server: e.target.value}); + }, + + handleUsernameChange_: function(e) { + this.setState({username: e.target.value}); + }, + + handleAutoUploadChange_: function(e) { + this.setState({autoUpload: e.target.checked}); + }, + + handleMaxCacheSizeChange_: function(e) { + this.setState({maxCacheSize: e.target.value}); + }, + + handleCertFingerprintChange_: function(e) { + this.setState({certFingerprint: e.target.value}); + }, + + render: function() { + return ( + React.DOM.div({}, + React.DOM.img({src:this.getQRURL_()}), + React.DOM.form({ref:'form', onSubmit:this.handleChange_}, + React.DOM.label({}, 'Camlistore Server:', + React.DOM.input({ + defaultValue: this.state.server, + onChange: this.handleServerChange_, + placeholder: 'e.g. https://foo.example.com or example.com:3179', + type: 'text' + })), + React.DOM.label({}, 'Username:', + React.DOM.input({ + defaultValue: this.props.defaultUsername, + onChange: this.handleUsernameChange_, + placeholder: '', + type: 'text' + })), + React.DOM.label({className: 'mobile-setup-auto-upload'}, + React.DOM.input({ + onChange: this.handleAutoUploadChange_, + type: 'checkbox' + }), + 'Auto-Upload', + React.DOM.span({className: 'mobile-setup-helptext'}, 'Upload SD card files as created')), + // TODO(wathiede): add suboptions to auto-upload? + React.DOM.label({className: 'mobile-setup-max-cache-size'}, + 'Maximum cache size', + React.DOM.input({ + defaultValue: this.state.maxCacheSize, + onChange: this.handleMaxCacheSizeChange_, + type: 'text' + }), + 'MB'), + React.DOM.label({}, 'Self-signed cert fingerprint:', + React.DOM.input({ + onChange: this.handleCertFingerprintChange_, + placeholder: '', + type: 'text' + })) + ))); + }, + + handleChange_: function() { + var u = this.getQRURL_(); + console.log(u); + }, +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/navigator.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/navigator.js new file mode 100644 index 00000000..84ea7823 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/navigator.js @@ -0,0 +1,127 @@ +/* +Copyright 2014 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.Navigator'); + +goog.require('cam.object'); +goog.require('goog.Uri'); + +// Navigator intercepts various types of browser navgiations and gives its client an opportunity to decide whether the navigation should be handled with JavaScript or not. +// Currently, 'click' events on hyperlinks and 'popstate' events are intercepted. Clients can also call navigate() to manually initiate navigation. +// +// @param Window win The window to listen for click and popstate events within to potentially interpret as navigations. +// @param Location location Network navigation will be executed using this location object. +// @param History history PushState navigation will be executed using this history object. +cam.Navigator = function(win, location, history) { + this.win_ = win; + this.location_ = location; + this.history_ = history; + this.handlers_ = []; + + // This is needed so that in handlePopState_, we can differentiate navigating back to this frame from the initial load. + // We can't just initialize to {} because there can already be interesting state (e.g., in the case of the user pressing the refresh button). + history.replaceState(cam.object.extend(history.state), '', location.href); + + this.win_.addEventListener('click', this.handleClick_.bind(this)); + this.win_.addEventListener('popstate', this.handlePopState_.bind(this)); +}; + +cam.Navigator.shouldHandleClick = function(e) { + // We are conservative and only try to handle left clicks that are unmodified. + // For any other kind of click, assume that something fancy (e.g., context menu, open in new tab, etc) is about to happen and let whatever it happen as normal. + if (e.button != 0 || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { + return null; + } + + for (var elm = e.target; ; elm = elm.parentElement) { + if (!elm) { + return null; + } + if (elm.nodeName == 'A' && elm.href) { + return elm; + } + } + + throw new Error('Should never get here'); + return null; +}; + +// Client should set this to handle navigation. +// +// This is called before the navigation has actually taken place: location.href will refer to the old URL, not the new one. Also, history.state will refer to previous state. +// +// If client returns true, then Navigator considers the navigation handled locally, and will add an entry to history using pushState(). If this method returns false, Navigator lets the navigation fall through to the browser. +// @param goog.Uri newURL The URL to navigate to. +// @return boolean Whether the navigation was handled locally. +cam.Navigator.prototype.onWillNavigate = function(newURL) {}; + +// Called after a local (pushState) navigation has been performed. At this point, location.href and history.state have been updated. +cam.Navigator.prototype.onDidNavigate = function() {}; + +// Programmatically initiate a navigation to a URL. Useful for triggering navigations from things other than hyperlinks. +// @param goog.Uri url The URL to navigate to. +// @return boolean Whether the navigation was handled locally. +cam.Navigator.prototype.navigate = function(url) { + if (this.dispatchImpl_(url, true)) { + return true; + } + this.location_.href = url.toString(); + return false; +}; + +// Handles navigations initiated via clicking a hyperlink. +cam.Navigator.prototype.handleClick_ = function(e) { + var elm = cam.Navigator.shouldHandleClick(e); + if (!elm) { + return; + } + + try { + if (this.dispatchImpl_(new goog.Uri(elm.href), true)) { + e.preventDefault(); + } + } catch (ex) { + // Prevent the navigation so that we can see the error. + e.preventDefault(); + throw ex; + } + // Otherwise, the event continues bubbling and navigation should happen as normal via the browser. +}; + +// Handles navigation via popstate. +cam.Navigator.prototype.handlePopState_ = function(e) { + // WebKit and older Chrome versions will fire a spurious initial popstate event after load. + // We can differentiate this event from ones corresponding to frames we generated ourselves with pushState() or replaceState() because our own frames always have a non-empty state. + // See: http://stackoverflow.com/questions/6421769/popstate-on-pages-load-in-chrome + if (!e.state) { + return; + } + if (!this.dispatchImpl_(new goog.Uri(this.location_.href), false)) { + this.location_.reload(); + } +}; + +cam.Navigator.prototype.dispatchImpl_ = function(url, addState) { + if (this.onWillNavigate(url)) { + if (addState) { + // Pass an empty object rather than null or undefined so that we can filter out spurious initial popstate events in handlePopState_. + this.history_.pushState({}, '', url.toString()); + } + this.onDidNavigate(); + return true; + } + return false; +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/navigator_test.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/navigator_test.js new file mode 100644 index 00000000..e8f35750 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/navigator_test.js @@ -0,0 +1,146 @@ +/* +Copyright 2014 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.require('goog.Uri'); +goog.require('goog.events.EventTarget'); +var assert = require('assert'); + +goog.require('cam.Navigator'); + +var MockLocation = function() { + goog.base(this); + this.href = ''; + this.reloadCount = 0; +}; +goog.inherits(MockLocation, goog.events.EventTarget); +MockLocation.prototype.reload = function() { + this.reloadCount++; +}; + +var MockHistory = function() { + this.states = [null]; +}; +MockHistory.prototype.pushState = function(a, b, url) { + this.states.push({state:a, url:url}); +}; +MockHistory.prototype.replaceState = function(a, b, url) { + this.states[this.states.length - 1] = {state:a, url:url}; +} + +var Handler = function() { + this.lastURL = null; + this.returnsTrue = false; + this.handle = this.handle.bind(this); +}; +Handler.prototype.handle = function(url) { + this.lastURL = url; + return this.returnsTrue; +}; + +describe('cam.Navigator', function() { + var mockWindow, mockLocation, mockHistory, handler, navigator; + var url = new goog.Uri('http://www.camlistore.org/foobar'); + + beforeEach(function() { + mockWindow = new goog.events.EventTarget(); + mockLocation = new MockLocation(); + mockHistory = new MockHistory(); + handler = new Handler(); + navigator = new cam.Navigator(mockWindow, mockLocation, mockHistory); + navigator.onWillNavigate = handler.handle; + }); + + it ('#constructor - seed initial state', function() { + assert.deepEqual(mockHistory.states, [{state:{}, url:''}]); + }); + + it('#navigate - no handler', function() { + // We should do network navigation. + navigator.onWillNavigate = function(){}; + var handledLocally = navigator.navigate(url); + assert.equal(mockLocation.href, url.toString()); + assert.equal(mockHistory.states.length, 1); + assert.equal(handledLocally, false); + }); + + it('#navigate - handler returns false', function() { + // Both handlers should get called, we should do network navigation. + var handledLocally = navigator.navigate(url); + assert.equal(handler.lastURL, url); + assert.equal(mockLocation.href, url.toString()); + assert.equal(mockHistory.states.length, 1); + assert.equal(handledLocally, false); + }); + + it('#navigate - handler returns true', function() { + // Both handlers should get called, we should do pushState() navigation. + handler.returnsTrue = true; + var handledLocally = navigator.navigate(url); + assert.equal(handler.lastURL, url); + assert.equal(mockLocation.href, ''); + assert.deepEqual(mockHistory.states, [{state:{}, url:''}, {state:{}, url:url.toString()}]); + assert.equal(handledLocally, true); + }); + + it('#handleClick_ - handled', function() { + handler.returnsTrue = true; + var ev = new goog.events.Event('click'); + ev.button = 0; + ev.target = { + nodeName: 'A', + href: url.toString() + }; + mockWindow.dispatchEvent(ev); + assert.equal(mockLocation.href, ''); + assert.deepEqual(mockHistory.states, [{state:{}, url:''}, {state:{}, url:url.toString()}]); + }); + + it('#handleClick_ - not handled', function() { + var ev = new goog.events.Event('click'); + ev.button = 0; + ev.target = { + nodeName: 'A', + href: url.toString() + }; + mockWindow.dispatchEvent(ev); + assert.equal(mockLocation.href, ''); + assert.deepEqual(mockHistory.states, [{state:{}, url:''}]); + assert.equal(ev.defaultPrevented, false); + }); + + it('#handlePopState_ - handled', function() { + handler.returnsTrue = true; + mockWindow.dispatchEvent({type:'popstate', state:{}}); + assert.equal(mockLocation.reloadCount, 0); + assert.deepEqual(mockHistory.states, [{state:{}, url:''}]); + }); + + it('#handlePopState_ - not handled', function() { + mockWindow.dispatchEvent({type:'popstate', state:{}}); + assert.equal(mockLocation.reloadCount, 1); + assert.deepEqual(mockHistory.states, [{state:{}, url:''}]); + }); + + it('#handlePopState_ - ignore initial popstate', function() { + // Fire a popstate with no state property. This simulates what happens in buggy browsers onload. This one should be ignored. + mockWindow.dispatchEvent({type:'popstate', state:null}); + assert.equal(mockLocation.reloadCount, 0); + + // Now fire a popstate with a state property it should be handled. + mockWindow.dispatchEvent({type:'popstate', state:{}}); + assert.equal(mockLocation.reloadCount, 1); + }); +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/new_permanode.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/new_permanode.svg new file mode 100644 index 00000000..c1463f73 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/new_permanode.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/node.png b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/node.png new file mode 100644 index 0000000000000000000000000000000000000000..8cb6df02293ae15b9a17f2f67a8dfc63132d83e5 GIT binary patch literal 3770 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 + + + + + Permanode + + + + + + + + + + +

    Permanode

    + +

    + Permalink: + + +

    + +
    +

    + Title: + + +

    +
    + +
    +

    + Tags: + + + +

    + +
    +

    + Access: + + + + ... with URL: + + + +

    +
    + +
    + +
    + +
    +
    + +
    + +
    +
    + + +
    +

    + or drag & drop files here +

    +
    
    +	
    + +

    Current object attributes

    +
    
    +
    +	
    +
    +
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_detail.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_detail.css
    new file mode 100644
    index 00000000..f41471af
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_detail.css
    @@ -0,0 +1,82 @@
    +/*
    +Copyright 2014 The Camlistore Authors
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +@import (less) "prefix-free.css";
    +
    +
    +.cam-permanode-detail {
    +	font-family: 'Open Sans', sans-serif;
    +	margin: 1.5em 2em;
    +}
    +
    +.cam-permanode-detail h1 {
    +	font-size: 1.5em;
    +}
    +
    +.cam-permanode-detail table {
    +	border-collapse: collapse;
    +	border-spacing: 0;
    +	width: 100%;
    +}
    +
    +.cam-permanode-detail th {
    +	border-bottom: 1px solid black;
    +	cursor: pointer;
    +	padding: 0.6em 1em 0.4em;
    +	text-align: left;
    +}
    +
    +.cam-permanode-detail th i {
    +	margin-left: 5px;
    +}
    +
    +.cam-permanode-detail td {
    +	border: 1px solid #aaa;
    +	padding: 0.6em 1em 0.4em;
    +	text-align: left;
    +}
    +
    +.cam-permanode-detail tr>*:nth-child(1) {
    +	width: 50%;
    +}
    +
    +.cam-permanode-detail tr>*:nth-child(2) {
    +	width: 50%;
    +}
    +
    +.cam-permanode-detail tr>*:nth-child(3) {
    +	color: #444;
    +	text-align: center;
    +	width: 0;
    +}
    +
    +.cam-permanode-detail-delete-attribute {
    +	cursor: pointer;
    +}
    +
    +.cam-permanode-detail td input[type=text] {
    +	border: none;
    +	font: inherit;
    +	width: 100%;
    +}
    +
    +.cam-permanode-detail-status {
    +	background: #eee;
    +	bottom: 1em;
    +	left: 1em;
    +	padding: 1em;
    +	position: fixed;
    +}
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_detail.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_detail.js
    new file mode 100644
    index 00000000..eb07dea7
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_detail.js
    @@ -0,0 +1,307 @@
    +/*
    +Copyright 2014 The Camlistore Authors
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +goog.provide('cam.PermanodeDetail');
    +
    +goog.require('goog.array');
    +goog.require('goog.labs.Promise');
    +goog.require('goog.object');
    +
    +goog.require('cam.ServerConnection');
    +
    +cam.PermanodeDetail = React.createClass({
    +	displayName: 'PermanodeDetail',
    +
    +	propTypes: {
    +		meta: React.PropTypes.object.isRequired,
    +		serverConnection: React.PropTypes.instanceOf(cam.ServerConnection).isRequired,
    +		timer: React.PropTypes.shape({
    +			setTimeout: React.PropTypes.func.isRequired,
    +		}).isRequired,
    +	},
    +
    +	getInitialState: function() {
    +		return {
    +			newRow: {},
    +			rows: this.getInitialRows_(),
    +			sortBy: 'name',
    +			sortAsc: true,
    +			status: '',
    +		};
    +	},
    +
    +	render: function() {
    +		return React.DOM.div({className: 'cam-permanode-detail'},
    +			React.DOM.h1(null, 'Current attributes'),
    +			this.getAttributesTable_(),
    +			this.getStatus_()
    +		);
    +	},
    +
    +	getStatus_: function() {
    +		if (this.state.status) {
    +			return React.DOM.div(
    +				{className: 'cam-permanode-detail-status'},
    +				this.state.status
    +			);
    +		} else {
    +			return null;
    +		}
    +	},
    +
    +	getInitialRows_: function() {
    +		var rows = [];
    +		for (var name in this.props.meta.permanode.attr) {
    +			var values = this.props.meta.permanode.attr[name];
    +			for (var i = 0; i < values.length; i++) {
    +				rows.push({
    +					'name': name,
    +					'value': values[i],
    +				});
    +			}
    +		}
    +		return rows;
    +	},
    +
    +	getAttributesTable_: function() {
    +		var headerText = function(name, column) {
    +			var children = [name];
    +			if (this.state.sortBy == column) {
    +				children.push(
    +					React.DOM.i({
    +						key: goog.string.subs('%s-sort-icon', name),
    +						className: React.addons.classSet({
    +							'fa': true,
    +							'fa-caret-up': this.state.sortAsc,
    +							'fa-caret-down': !this.state.sortAsc,
    +						}),
    +					})
    +				);
    +			}
    +			return React.DOM.span(null, children);
    +		}.bind(this);
    +
    +		var header = function(content, onclick) {
    +			return React.DOM.th(
    +				{
    +					className: 'cam-unselectable',
    +					onClick: onclick,
    +				},
    +				content
    +			);
    +		};
    +
    +		return React.DOM.table(null,
    +			React.DOM.tbody(null,
    +				React.DOM.tr(
    +					{key: 'header'},
    +					header(headerText('Name', 'name'), this.handleSort_.bind(null, 'name')),
    +					header(headerText('Value', 'value'), this.handleSort_.bind(null, 'value')),
    +					header('')
    +				),
    +				cam.PermanodeDetail.AttributeRow({
    +					className: 'cam-permanode-detail-new-row',
    +					key: 'new',
    +					onBlur: this.handleBlur_,
    +					onChange: this.handleChange_,
    +					row: this.state.newRow,
    +				}),
    +				this.state.rows.map(function(r, i) {
    +					return cam.PermanodeDetail.AttributeRow({
    +						key: i,
    +						onBlur: this.handleBlur_,
    +						onChange: this.handleChange_,
    +						onDelete: this.handleDelete_.bind(null, r),
    +						row: r,
    +					});
    +				}, this)
    +			)
    +		);
    +	},
    +
    +	handleChange_: function(row, column, e) {
    +		row[column] = e.target.value;
    +		this.forceUpdate();
    +	},
    +
    +	handleDelete_: function(row) {
    +		this.setState({
    +			rows: this.state.rows.filter(function(r) { return r != row; }),
    +		}, function() {
    +			this.commitChanges_();
    +		}.bind(this));
    +	},
    +
    +	handleBlur_: function(row) {
    +		if (row == this.state.newRow) {
    +			if (row.name && row.value) {
    +				this.state.rows.splice(0, 0, row);
    +				this.state.newRow = {};
    +				this.forceUpdate();
    +				this.commitChanges_();
    +			}
    +		} else {
    +			this.commitChanges_();
    +		}
    +	},
    +
    +	handleSort_: function(sortBy) {
    +		var sortAsc = true;
    +		if (this.state.sortBy == sortBy) {
    +			sortAsc = !this.state.sortAsc;
    +		}
    +		this.setState({
    +			rows: this.getSortedRows_(sortBy, sortAsc),
    +			sortAsc: sortAsc,
    +			sortBy: sortBy,
    +		});
    +	},
    +
    +	getSortedRows_: function(sortBy, sortAsc) {
    +		var numericSort = function(a, b) {
    +			return parseFloat(a) - parseFloat(b);
    +		}
    +		var stringSort = function(a, b) {
    +			return a.localeCompare(b);
    +		}
    +
    +		var rows = goog.array.clone(this.state.rows);
    +		var sort = rows.some(function(r) {
    +			return isNaN(parseFloat(r[sortBy]));
    +		}) ? stringSort : numericSort;
    +
    +		rows.sort(function(a, b) {
    +			if (!sortAsc) {
    +				var tmp = a;
    +				a = b;
    +				b = tmp;
    +			}
    +			return sort(a[sortBy], b[sortBy]);
    +		});
    +
    +		return rows;
    +	},
    +
    +	getChanges_: function() {
    +		var key = function(r) {
    +			return r.name + ':' + r.value;
    +		};
    +		var before = goog.array.toObject(this.getInitialRows_(), key);
    +		var after = goog.array.toObject(this.state.rows, key);
    +
    +		var adds = goog.object.filter(after, function(v, k) { return !(k in before); });
    +		var deletes = goog.object.filter(before, function(v, k) { return !(k in after); });
    +
    +		return {
    +			adds: goog.object.getValues(adds),
    +			deletes: goog.object.getValues(deletes),
    +		};
    +	},
    +
    +	commitChanges_: function() {
    +		var changes = this.getChanges_();
    +		if (changes.adds.length == 0 && changes.deletes.length == 0) {
    +			return;
    +		}
    +		this.setState({
    +			status: 'Saving...',
    +		});
    +		var promises = changes.adds.map(function(add) {
    +			return new goog.labs.Promise(this.props.serverConnection.newAddAttributeClaim.bind(this.props.serverConnection, this.props.meta.blobRef, add.name, add.value));
    +		}, this).concat(changes.deletes.map(function(del) {
    +			return new goog.labs.Promise(this.props.serverConnection.newDelAttributeClaim.bind(this.props.serverConnection, this.props.meta.blobRef, del.name, del.value));
    +		}, this));
    +		goog.labs.Promise.all(promises).then(function() {
    +			this.props.timer.setTimeout(function() {
    +				this.setState({
    +					status: '',
    +				});
    +			}.bind(this), 500);
    +		}.bind(this));
    +	}
    +});
    +
    +cam.PermanodeDetail.AttributeRow = React.createClass({
    +	displayName: 'AttributeRow',
    +
    +	propTypes: {
    +		className: React.PropTypes.string,
    +		onBlur: React.PropTypes.func,
    +		onDelete: React.PropTypes.func,
    +		onChange: React.PropTypes.func.isRequired,
    +		row: React.PropTypes.object,
    +	},
    +
    +	render: function() {
    +		var deleteButton = function(onDelete) {
    +			if (onDelete) {
    +				return React.DOM.i({
    +					className: 'fa fa-times-circle-o cam-permanode-detail-delete-attribute',
    +					onClick: onDelete,
    +				});
    +			} else {
    +				return null;
    +			}
    +		};
    +
    +		return React.DOM.tr(
    +			{
    +				className: this.props.className,
    +				onBlur: this.props.onBlur && this.props.onBlur.bind(null, this.props.row),
    +			},
    +			React.DOM.td(null,
    +				React.DOM.input({
    +					onChange: this.props.onChange.bind(null, this.props.row, 'name'),
    +					placeholder: this.props.row.name ? '': 'New attribute name',
    +					type: 'text',
    +					value: this.props.row.name || '',
    +				})
    +			),
    +			React.DOM.td(null,
    +				React.DOM.input({
    +					onChange: this.props.onChange.bind(null, this.props.row, 'value'),
    +					placeholder: this.props.row.value ? '' : 'New attribute value',
    +					type: 'text',
    +					value: this.props.row.value || '',
    +				})
    +			),
    +			React.DOM.td(null, deleteButton(this.props.onDelete))
    +		);
    +	},
    +});
    +
    +cam.PermanodeDetail.getAspect = function(serverConnection, timer, blobref, targetSearchSession) {
    +	if (!targetSearchSession) {
    +		return null;
    +	}
    +
    +	var pm = targetSearchSession.getMeta(blobref);
    +	if (!pm || pm.camliType != 'permanode') {
    +		return null;
    +	}
    +
    +	return {
    +		fragment: 'permanode',
    +		title: 'Permanode',
    +		createContent: function(size) {
    +			return cam.PermanodeDetail({
    +				meta: pm,
    +				serverConnection: serverConnection,
    +				timer: timer,
    +			});
    +		},
    +	};
    +};
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_utils.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_utils.js
    new file mode 100644
    index 00000000..1bff20b5
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_utils.js
    @@ -0,0 +1,35 @@
    +/*
    +Copyright 2014 The Camlistore Authors
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +goog.provide('cam.permanodeUtils');
    +
    +goog.require('goog.array');
    +
    +cam.permanodeUtils.getSingleAttr = function(permanode, name) {
    +	var val = permanode.attr[name];
    +	if (val) {
    +		return goog.isArray(val) ? val[0] : val;
    +	}
    +	return null;
    +};
    +
    +cam.permanodeUtils.isContainer = function(permanode) {
    +	return goog.object.some(permanode.attr, function(v, k) { return k == 'camliMember' || goog.string.startsWith(k, 'camliPath:'); });
    +};
    +
    +cam.permanodeUtils.getCamliNodeType = function(permanode) {
    +	return cam.permanodeUtils.getSingleAttr(permanode, 'camliNodeType');
    +};
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_utils_test.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_utils_test.js
    new file mode 100644
    index 00000000..3e5745b4
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/permanode_utils_test.js
    @@ -0,0 +1,52 @@
    +/*
    +Copyright 2014 Google Inc.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +     http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +var assert = require('assert');
    +
    +goog.require('cam.permanodeUtils');
    +
    +
    +describe('cam.permanodeUtils', function() {
    +  describe('#getSingleAttr', function() {
    +    it('should return null if attr unknown or empty string', function() {
    +        var pn = {
    +            attr: {
    +                foo: '',
    +            },
    +        };
    +        assert.strictEqual(null, cam.permanodeUtils.getSingleAttr(pn, 'foo'));
    +        assert.strictEqual(null, cam.permanodeUtils.getSingleAttr(pn, 'bar'));
    +    });
    +
    +    it('should return first array val', function() {
    +        var pn = {
    +            attr: {
    +                foo: ['bar', 'baz'],
    +            },
    +        };
    +        assert.equal('bar', cam.permanodeUtils.getSingleAttr(pn, 'foo'));
    +    });
    +
    +    it('should return string val', function() {
    +        var pn = {
    +            attr: {
    +                foo: 'bar',
    +            },
    +        };
    +        assert.equal('bar', cam.permanodeUtils.getSingleAttr(pn, 'foo'));
    +    });
    +  });
    +});
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/prefix-free.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/prefix-free.css
    new file mode 100644
    index 00000000..81024fc7
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/prefix-free.css
    @@ -0,0 +1,389 @@
    +//---------------------------------------------------
    +//  LESS Prefixer
    +//---------------------------------------------------
    +//
    +//  All of the CSS3 fun, none of the prefixes!
    +//
    +//  As a rule, you can use the CSS properties you
    +//  would expect just by adding a '.':
    +//
    +//  box-shadow => .box-shadow(@args)
    +//
    +//  Also, when shorthand is available, arguments are
    +//  not parameterized. Learn CSS, not LESS Prefixer.
    +//
    +//  -------------------------------------------------
    +//  TABLE OF CONTENTS
    +//  (*) denotes a syntax-sugar helper
    +//  -------------------------------------------------
    +//
    +//      .animation(@args)
    +//          .animation-delay(@delay)
    +//          .animation-direction(@direction)
    +//          .animation-duration(@duration)
    +//          .animation-fill-mode(@mode)
    +//          .animation-iteration-count(@count)
    +//          .animation-name(@name)
    +//          .animation-play-state(@state)
    +//          .animation-timing-function(@function)
    +//      .background-size(@args)
    +//      .border-radius(@args)
    +//      .box-shadow(@args)
    +//          .inner-shadow(@args) *
    +//      .box-sizing(@args)
    +//          .border-box() *
    +//          .content-box() *
    +//      .columns(@args)
    +//          .column-count(@count)
    +//          .column-gap(@gap)
    +//          .column-rule(@args)
    +//          .column-width(@width)
    +//      .gradient(@default,@start,@stop) *
    +//          .linear-gradient-top(@default,@color1,@stop1,@color2,@stop2,[@color3,@stop3,@color4,@stop4])*
    +//          .linear-gradient-left(@default,@color1,@stop1,@color2,@stop2,[@color3,@stop3,@color4,@stop4])*
    +//      .opacity(@factor)
    +//      .transform(@args)
    +//          .transform-origin(@args)
    +//          .transform-style(@style)
    +//          .rotate(@deg)
    +//          .scale(@factor)
    +//          .translate(@x,@y)
    +//          .translate3d(@x,@y,@z)
    +//          .translateHardware(@x,@y) *
    +//      .text-shadow(@args)
    +//      .transition(@args)
    +//          .transition-delay(@delay)
    +//          .transition-duration(@duration)
    +//          .transition-property(@property)
    +//          .transition-timing-function(@function)
    +//
    +//
    +//
    +//  Credit to LESS Elements for the motivation and
    +//  to CSS3Please.com for implementation.
    +//
    +//  Copyright (c) 2012 Joel Sutherland
    +//  MIT Licensed:
    +//  http://www.opensource.org/licenses/mit-license.php
    +//
    +//---------------------------------------------------
    +
    +
    +// Animation
    +
    +.animation(@args) {
    +    -webkit-animation: @args;
    +    -moz-animation: @args;
    +    -ms-animation: @args;
    +    -o-animation: @args;
    +    animation: @args;
    +}
    +.animation-delay(@delay) {
    +    -webkit-animation-delay: @delay;
    +    -moz-animation-delay: @delay;
    +    -ms-animation-delay: @delay;
    +    -o-animation-delay: @delay;
    +    animation-delay: @delay;
    +}
    +.animation-direction(@direction) {
    +    -webkit-animation-direction: @direction;
    +    -moz-animation-direction: @direction;
    +    -ms-animation-direction: @direction;
    +    -o-animation-direction: @direction;
    +}
    +.animation-duration(@duration) {
    +    -webkit-animation-duration: @duration;
    +    -moz-animation-duration: @duration;
    +    -ms-animation-duration: @duration;
    +    -o-animation-duration: @duration;
    +}
    +.animation-fill-mode(@mode) {
    +    -webkit-animation-fill-mode: @mode;
    +    -moz-animation-fill-mode: @mode;
    +    -ms-animation-fill-mode: @mode;
    +    -o-animation-fill-mode: @mode;
    +    animation-fill-mode: @mode;
    +}
    +.animation-iteration-count(@count) {
    +    -webkit-animation-iteration-count: @count;
    +    -moz-animation-iteration-count: @count;
    +    -ms-animation-iteration-count: @count;
    +    -o-animation-iteration-count: @count;
    +    animation-iteration-count: @count;
    +}
    +.animation-name(@name) {
    +    -webkit-animation-name: @name;
    +    -moz-animation-name: @name;
    +    -ms-animation-name: @name;
    +    -o-animation-name: @name;
    +    animation-name: @name;
    +}
    +.animation-play-state(@state) {
    +    -webkit-animation-play-state: @state;
    +    -moz-animation-play-state: @state;
    +    -ms-animation-play-state: @state;
    +    -o-animation-play-state: @state;
    +    animation-play-state: @state;
    +}
    +.animation-timing-function(@function) {
    +    -webkit-animation-timing-function: @function;
    +    -moz-animation-timing-function: @function;
    +    -ms-animation-timing-function: @function;
    +    -o-animation-timing-function: @function;
    +    animation-timing-function: @function;
    +}
    +
    +
    +// Background Size
    +
    +.background-size(@args) {
    +    -webkit-background-size: @args;
    +    background-size: @args;
    +}
    +
    +
    +// Border Radius
    +
    +.border-radius(@args) {
    +	-webkit-border-radius: @args;
    +    border-radius: @args;
    +
    +    background-clip: padding-box;
    +}
    +
    +
    +// Box Shadows
    +
    +.box-shadow(@args) {
    +    -webkit-box-shadow: @args;
    +    box-shadow: @args;
    +}
    +.inner-shadow(@args) {
    +    .box-shadow(inset @args);
    +}
    +
    +
    +// Box Sizing
    +
    +.box-sizing(@args) {
    +    -webkit-box-sizing: @args;
    +    -moz-box-sizing: @args;
    +    box-sizing: @args;
    +}
    +.border-box(){
    +    .box-sizing(border-box);
    +}
    +.content-box(){
    +    .box-sizing(content-box);
    +}
    +
    +
    +// Columns
    +
    +.columns(@args) {
    +    -webkit-columns: @args;
    +    -moz-columns: @args;
    +    columns: @args;
    +}
    +.column-count(@count) {
    +    -webkit-column-count: @count;
    +    -moz-column-count: @count;
    +    column-count: @count;
    +}
    +.column-gap(@gap) {
    +    -webkit-column-gap: @gap;
    +    -moz-column-gap: @gap;
    +    column-gap: @gap;
    +}
    +.column-width(@width) {
    +    -webkit-column-width: @width;
    +    -moz-column-width: @width;
    +    column-width: @width;
    +}
    +.column-rule(@args) {
    +    -webkit-column-rule: @args;
    +    -moz-column-rule: @args;
    +    column-rule: @args;
    +}
    +
    +
    +// Gradients
    +
    +.gradient(@default: #F5F5F5, @start: #EEE, @stop: #FFF) {
    +    .linear-gradient-top(@default,@start,0%,@stop,100%);
    +}
    +.linear-gradient-top(@default,@color1,@stop1,@color2,@stop2) {
    +    background-color: @default;
    +    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(@stop1, @color1), color-stop(@stop2 @color2));
    +    background-image: -webkit-linear-gradient(top, @color1 @stop1, @color2 @stop2);
    +    background-image: -moz-linear-gradient(top, @color1 @stop1, @color2 @stop2);
    +    background-image: -ms-linear-gradient(top, @color1 @stop1, @color2 @stop2);
    +    background-image: -o-linear-gradient(top, @color1 @stop1, @color2 @stop2);
    +    background-image: linear-gradient(top, @color1 @stop1, @color2 @stop2);
    +}
    +.linear-gradient-top(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3) {
    +    background-color: @default;
    +    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(@stop1, @color1), color-stop(@stop2 @color2), color-stop(@stop3 @color3));
    +    background-image: -webkit-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3);
    +    background-image: -moz-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3);
    +    background-image: -ms-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3);
    +    background-image: -o-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3);
    +    background-image: linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3);
    +}
    +.linear-gradient-top(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3,@color4,@stop4) {
    +    background-color: @default;
    +    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(@stop1, @color1), color-stop(@stop2 @color2), color-stop(@stop3 @color3), color-stop(@stop4 @color4));
    +    background-image: -webkit-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4);
    +    background-image: -moz-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4);
    +    background-image: -ms-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4);
    +    background-image: -o-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4);
    +    background-image: linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4);
    +}
    +.linear-gradient-left(@default,@color1,@stop1,@color2,@stop2) {
    +    background-color: @default;
    +    background-image: -webkit-gradient(linear, left top, left top, color-stop(@stop1, @color1), color-stop(@stop2 @color2));
    +    background-image: -webkit-linear-gradient(left, @color1 @stop1, @color2 @stop2);
    +    background-image: -moz-linear-gradient(left, @color1 @stop1, @color2 @stop2);
    +    background-image: -ms-linear-gradient(left, @color1 @stop1, @color2 @stop2);
    +    background-image: -o-linear-gradient(left, @color1 @stop1, @color2 @stop2);
    +    background-image: linear-gradient(left, @color1 @stop1, @color2 @stop2);
    +}
    +.linear-gradient-left(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3) {
    +    background-color: @default;
    +    background-image: -webkit-gradient(linear, left top, left top, color-stop(@stop1, @color1), color-stop(@stop2 @color2), color-stop(@stop3 @color3));
    +    background-image: -webkit-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3);
    +    background-image: -moz-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3);
    +    background-image: -ms-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3);
    +    background-image: -o-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3);
    +    background-image: linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3);
    +}
    +.linear-gradient-left(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3,@color4,@stop4) {
    +    background-color: @default;
    +    background-image: -webkit-gradient(linear, left top, left top, color-stop(@stop1, @color1), color-stop(@stop2 @color2), color-stop(@stop3 @color3), color-stop(@stop4 @color4));
    +    background-image: -webkit-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4);
    +    background-image: -moz-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4);
    +    background-image: -ms-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4);
    +    background-image: -o-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4);
    +    background-image: linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4);
    +}
    +
    +
    +// Opacity
    +
    +.opacity(@factor) {
    +    @iefactor: @factor*100;
    +    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=@{iefactor})";
    +	filter: ~"alpha(opacity=(@{iefactor}))";
    +    opacity: @factor;
    +}
    +
    +
    +// Text Shadow
    +
    +.text-shadow(@args) {
    +    text-shadow: @args;
    +}
    +
    +
    +// Transforms
    +
    +.transform(@args) {
    +    -webkit-transform: @args;
    +    -moz-transform: @args;
    +    -ms-transform: @args;
    +    -o-transform: @args;
    +    transform: @args;
    +}
    +.transform-origin(@args) {
    +    -webkit-transform-origin: @args;
    +    -moz-transform-origin: @args;
    +    -ms-transform-origin: @args;
    +    -o-transform-origin: @args;
    +    transform-origin: @args;
    +}
    +.transform-style(@style) {
    +    -webkit-transform-style: @style;
    +    -moz-transform-style: @style;
    +    -ms-transform-style: @style;
    +    -o-transform-style: @style;
    +    transform-style: @style;
    +}
    +.rotate(@deg:45deg){
    +    .transform(rotate(@deg));
    +}
    +.scale(@factor:.5){
    +    .transform(scale(@factor));
    +}
    +.translate(@x,@y){
    +    .transform(translate(@x,@y));
    +}
    +.translate3d(@x,@y,@z) {
    +    .transform(translate3d(@x,@y,@z));
    +}
    +.translateHardware(@x,@y) {
    +    .translate(@x,@y);
    +    -webkit-transform: translate3d(@x,@y,0);
    +    -moz-transform: translate3d(@x,@y,0);
    +    -o-transform: translate3d(@x,@y,0);
    +    -ms-transform: translate3d(@x,@y,0);
    +    transform: translate3d(@x,@y,0);
    +}
    +
    +
    +// Transitions
    +
    +.transition(@args:200ms) {
    +    -webkit-transition: @args;
    +    -moz-transition: @args;
    +    -o-transition: @args;
    +    -ms-transition: @args;
    +    transition: @args;
    +}
    +
    +/* Added by elsigh */
    +.transition-transform(@args) {
    +    -webkit-transition: -webkit-transform @args;
    +    -moz-transition: -moz-transform @args;
    +    -ms-transition: -ms-transform @args;
    +    -o-transition: -o-transform @args;
    +    transition: transform @args;
    +    }
    +
    +.transition-delay(@delay:0) {
    +    -webkit-transition-delay: @delay;
    +    -moz-transition-delay: @delay;
    +    -o-transition-delay: @delay;
    +    -ms-transition-delay: @delay;
    +    transition-delay: @delay;
    +}
    +.transition-duration(@duration:200ms) {
    +    -webkit-transition-duration: @duration;
    +    -moz-transition-duration: @duration;
    +    -o-transition-duration: @duration;
    +    -ms-transition-duration: @duration;
    +    transition-duration: @duration;
    +}
    +.transition-property(@property:all) {
    +    -webkit-transition-property: @property;
    +    -moz-transition-property: @property;
    +    -o-transition-property: @property;
    +    -ms-transition-property: @property;
    +    transition-property: @property;
    +}
    +.transition-timing-function(@function:ease) {
    +    -webkit-transition-timing-function: @function;
    +    -moz-transition-timing-function: @function;
    +    -o-transition-timing-function: @function;
    +    -ms-transition-timing-function: @function;
    +    transition-timing-function: @function;
    +}
    +
    +/* Added by elsigh */
    +.user-select(@args) {
    +    -webkit-user-select: @args;
    +    -moz-user-select: @args;
    +    -khtml-user-select: @args;
    +    -o-user-select: @args;
    +    -ms-user-select: @args;
    +    user-select: @args;
    +}
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/property_sheet.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/property_sheet.css
    new file mode 100644
    index 00000000..19ebaa4a
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/property_sheet.css
    @@ -0,0 +1,54 @@
    +/*
    +Copyright 2014 The Camlistore Authors
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +@import (less) "prefix-free.css";
    +
    +
    +.cam-property-sheet-container,
    +.cam-property-sheet-container td {
    +	font-size: 15px;
    +}
    +
    +.cam-property-sheet-title {
    +	background: #e5e5e5;
    +	color: #999;
    +	padding: 2px 6px;
    +}
    +
    +.cam-property-sheet-content {
    +	padding: 2px 6px;
    +	margin-bottom: 1em;
    +}
    +
    +.cam-property-sheet-content table {
    +	border-collapse: collapse;
    +	display: block;
    +	margin: -2px -6px;
    +}
    +
    +.cam-property-sheet-content td {
    +	border-bottom: 1px dashed #ccc;
    +	padding: 2px 6px;
    +}
    +
    +.cam-property-sheet-content td:first-child {
    +	border-right: 1px dashed #ccc;
    +	color: #666;
    +}
    +
    +.cam-property-sheet-content td:last-child {
    +	width: 100%;
    +}
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/property_sheet.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/property_sheet.js
    new file mode 100644
    index 00000000..41c60c09
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/property_sheet.js
    @@ -0,0 +1,56 @@
    +/*
    +Copyright 2014 The Camlistore Authors
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +goog.provide('cam.PropertySheet');
    +goog.provide('cam.PropertySheetContainer');
    +
    +goog.require('cam.style.ClassNameBuilder');
    +
    +cam.PropertySheet = React.createClass({
    +	displayName: 'PropertySheet',
    +
    +	propTypes: {
    +		className: React.PropTypes.string,
    +		title: React.PropTypes.string.isRequired,
    +	},
    +
    +	render: function() {
    +		return (
    +			React.DOM.div({className: new cam.style.ClassNameBuilder().add('cam-property-sheet').add(this.props.className).build()}, [
    +				React.DOM.div({className: 'cam-property-sheet-title'}, this.props.title),
    +				React.DOM.div({className: 'cam-property-sheet-content'}, this.props.children),
    +			])
    +		);
    +	},
    +});
    +
    +cam.PropertySheetContainer = React.createClass({
    +	displayName: 'PropertySheetContainer',
    +
    +	propTypes: {
    +		className: React.PropTypes.string,
    +		style: React.PropTypes.object,
    +	},
    +
    +	render: function() {
    +		return React.DOM.div({
    +				className: new cam.style.ClassNameBuilder().add('cam-property-sheet-container').add(this.props.className).build(),
    +				style: this.props.style,
    +			},
    +			this.props.children
    +		);
    +	},
    +});
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/pyramid_throbber.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/pyramid_throbber.css
    new file mode 100644
    index 00000000..1647fb1f
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/pyramid_throbber.css
    @@ -0,0 +1,98 @@
    +/*
    +Copyright 2014 The Camlistore Authors
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +@import (less) "prefix-free.css";
    +
    +
    +.cam-pyramid-throbber {
    +	position: relative;
    +}
    +
    +.cam-pyramid-throbber .lefttop {
    +	position: absolute;
    +	width: 0;
    +	height: 0;
    +	border-bottom: 50px solid rgb(220,220,220);
    +	border-left: 35px solid transparent;
    +	.animation(leftcolors 1.0s infinite ease-in-out);
    +}
    +
    +.cam-pyramid-throbber .leftbottom {
    +	position: absolute;
    +	top: 50px;
    +	width: 0;
    +	height: 0;
    +	border-top: 15px solid rgb(220,220,220);
    +	border-left: 35px solid transparent;
    +	.animation(leftcolors 1.0s infinite ease-in-out);
    +}
    +
    +.cam-pyramid-throbber .righttop {
    +	position: absolute;
    +	left: 35px;
    +	width: 0;
    +	height: 0;
    +	border-bottom: 50px solid rgb(180,180,180);
    +	border-right: 35px solid transparent;
    +	.animation(rightcolors 1.0s infinite ease-in-out);
    +}
    +
    +.cam-pyramid-throbber .rightbottom {
    +	position: absolute;
    +	left: 35px;
    +	top: 50px;
    +	width: 0;
    +	height: 0;
    +	border-top: 15px solid rgb(180,180,180);
    +	border-right: 35px solid transparent;
    +	.animation(rightcolors 1.0s infinite ease-in-out);
    +}
    +
    +@-webkit-keyframes leftcolors {
    +	0% { border-bottom-color: rgb(220,220,220); border-top-color: rgb(220,220,220) }
    +	50% { border-bottom-color: rgb(180,180,180); border-top-color: rgb(180,180,180) }
    +	100% { border-bottom-color: rgb(220,220,220); border-top-color: rgb(220,220,220) }
    +}
    +
    +@-webkit-keyframes rightcolors {
    +	0% { border-bottom-color: rgb(180,180,180); border-top-color: rgb(180,180,180) }
    +	50% { border-bottom-color: rgb(220,220,220); border-top-color: rgb(220,220,220) }
    +	100% { border-bottom-color: rgb(180,180,180); border-top-color: rgb(180,180,180) }
    +}
    +
    +@-moz-keyframes leftcolors {
    +	0% { border-bottom-color: rgb(220,220,220); border-top-color: rgb(220,220,220) }
    +	50% { border-bottom-color: rgb(180,180,180); border-top-color: rgb(180,180,180) }
    +	100% { border-bottom-color: rgb(220,220,220); border-top-color: rgb(220,220,220) }
    +}
    +
    +@-moz-keyframes rightcolors {
    +	0% { border-bottom-color: rgb(180,180,180); border-top-color: rgb(180,180,180) }
    +	50% { border-bottom-color: rgb(220,220,220); border-top-color: rgb(220,220,220) }
    +	100% { border-bottom-color: rgb(180,180,180); border-top-color: rgb(180,180,180) }
    +}
    +
    +@keyframes leftcolors {
    +	0% { border-bottom-color: rgb(220,220,220); border-top-color: rgb(220,220,220) }
    +	50% { border-bottom-color: rgb(180,180,180); border-top-color: rgb(180,180,180) }
    +	100% { border-bottom-color: rgb(220,220,220); border-top-color: rgb(220,220,220) }
    +}
    +
    +@keyframes rightcolors {
    +	0% { border-bottom-color: rgb(180,180,180); border-top-color: rgb(180,180,180) }
    +	50% { border-bottom-color: rgb(220,220,220); border-top-color: rgb(220,220,220) }
    +	100% { border-bottom-color: rgb(180,180,180); border-top-color: rgb(180,180,180) }
    +}
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/pyramid_throbber.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/pyramid_throbber.js
    new file mode 100644
    index 00000000..588f74ea
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/pyramid_throbber.js
    @@ -0,0 +1,46 @@
    +/*
    +Copyright 2014 The Camlistore Authors
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +goog.provide('cam.PyramidThrobber');
    +
    +goog.require('goog.math.Coordinate');
    +goog.require('goog.math.Size');
    +
    +cam.PyramidThrobber = React.createClass({
    +	propTypes: {
    +		pos: React.PropTypes.instanceOf(goog.math.Coordinate),
    +	},
    +
    +	render: function() {
    +		return React.DOM.div({style:this.getStyle_(), className:'cam-pyramid-throbber'},
    +			React.DOM.div({className:'lefttop'}),
    +			React.DOM.div({className:'leftbottom'}),
    +			React.DOM.div({className:'righttop'}),
    +			React.DOM.div({className:'rightbottom'})
    +		);
    +	},
    +
    +	getStyle_: function() {
    +		var result = {};
    +		if (goog.isDef(this.props.pos)) {
    +			result.left = this.props.pos.x;
    +			result.top = this.props.pos.y;
    +		}
    +		return result;
    +	}
    +});
    +
    +cam.PyramidThrobber.SIZE = new goog.math.Size(70, 85);
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/react_util.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/react_util.js
    new file mode 100644
    index 00000000..c61f1da4
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/react_util.js
    @@ -0,0 +1,87 @@
    +/*
    +Copyright 2014 The Camlistore Authors.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +goog.provide('cam.reactUtil');
    +
    +goog.require('goog.string');
    +
    +cam.reactUtil.mapOf = function(validator) {
    +	var validator = function(props, propName, componentName) {
    +		if (!props[propName]) {
    +			return;
    +		}
    +
    +		React.PropTypes.isObject(props, propName, componentName);
    +
    +		for (var child in props[propName]) {
    +			var childName = goog.string.subs('%s[%s]', componentName, child);
    +			validator(props[propName], child, childName);
    +		}
    +	};
    +
    +	validator.isRequired = React.PropTypes.object.isRequired;
    +	return validator;
    +};
    +
    +// Returns the appropriate vendor prefixed style property name. This is figured out by testing the presence of various property names on an actual DOM style object.
    +// The returned property is of the form 'fooBar' (if no prefix is needed), or 'WebkitFooBar' if a prefix is needed, which is the form React expects.
    +// @param {string} prop The property name to find.
    +// @param {CSSStyleDeclaration=} style A style object to test on. This can be any DOM style object, e.g., document.body.style.
    +// @return {?string} The appropriate property name to use, or null if the property is not supported in this environment.
    +cam.reactUtil.getVendorProp = function(prop, opt_testStyle) {
    +	if (!goog.isDef(opt_testStyle)) {
    +		opt_testStyle = document.body.style;
    +	}
    +
    +	if (goog.isDef(opt_testStyle[prop])) {
    +		return prop;
    +	}
    +
    +	var prefixes = ['webkit', 'moz', 'ie'];
    +	for (var i = 0, p; p = prefixes[i]; i++) {
    +		var candidate = p + goog.string.toTitleCase(prop);
    +		if (goog.isDef(opt_testStyle[candidate])) {
    +			// React expects vendor prefixed property names to be TitleCase.
    +			return goog.string.toTitleCase(candidate);
    +		}
    +	}
    +
    +	return null;
    +};
    +
    +// Returns a copy of an object with all properties vendor-prefixed as required by the current ua.
    +// @param {object} o The object to fix.
    +// @param {CSSStyleDeclaration=} style A style object to test on. This can be any DOM style object, e.g., document.body.style.
    +// @return {object} A copy of o with all properties vendor-prefixed as appropriate.
    +cam.reactUtil.getVendorProps = function(o, opt_testStyle) {
    +	var n = {};
    +	for (var p in o) {
    +		n[cam.reactUtil.getVendorProp(p, opt_testStyle)] = o[p];
    +	}
    +	return n;
    +};
    +
    +// Like cam.object.extend(), except that special care is taken to also merge together some known child properties that are part of React specifications.
    +// @param Object parentSpec
    +// @param Object childSpec
    +// @return Object merged spec
    +cam.reactUtil.extend = function(parentSpec, childSpec) {
    +	var result = cam.object.extend(parentSpec, childSpec);
    +	if (childSpec.propTypes) {
    +		result.propTypes = cam.object.extend(parentSpec.propTypes, childSpec.propTypes);
    +	}
    +	return result;
    +}
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe-no-wheel.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe-no-wheel.svg
    new file mode 100755
    index 00000000..2eea3683
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe-no-wheel.svg
    @@ -0,0 +1,23 @@
    +
    +
    +
    +
    +
    +
    +	
    +
    +
    \ No newline at end of file
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe-wheel.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe-wheel.svg
    new file mode 100755
    index 00000000..883aeb2b
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe-wheel.svg
    @@ -0,0 +1,28 @@
    +
    +
    +
    +
    +
    +
    +	
    +		
    +		
    +		
    +		
    +	
    +
    +
    \ No newline at end of file
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe1-16.png b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe1-16.png
    new file mode 100644
    index 0000000000000000000000000000000000000000..4e958c2d399c5a1830783dea6cec768ed253508d
    GIT binary patch
    literal 449
    zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6U4S$Y
    z{B+)352QE?JR*yM+OLB!qm#z$3ZNi+iKnkC`&}+>b{)}mX^RbkrZ{@KIEGl9?!CO<
    zOEyq|?Zf=;8>>{fvL1*DsdSw<%8{9ylku8lTLyz2f3+vd_+&e?B|2k#o%gC&~YDlaj(N@W&U-vpf}5aC1TUMC0|(
    zbascFdAt2bhj56igm6Ioi}DJ-z53ma?F>5wWP%(7SJ*3mkW9Yeq`gbSDQKP2>Y9%C
    zHx6jpB{1g3q<7};yUrkayzYhUnU;O+M|0SwPuxCx#nd0|jyjKn+5TBKr4`<132^z%
    zKjSdtHKPXAKOFn?ecy^-JedCHsB)dFx$K<>oXiq$&6>X)yZxifcTM|S9SzyGnLp!L;E%X!-i`Zm!+%`f@IUlMB710k?&V8Px#24YJ`L;wH)0002_L%V+f000SaNLh0L01ejw01ejxLMWSf00007bV*G`2i*z{
    z77GnuKp&X^00RF>L_t(o!>yM&XjVZGfWLkDqXzMcXcIh9Qg{_&qk7ef3xt|H#^79
    z&b71fcN_v<0}a3@zPtoqz989H2;!
    z2kK4$N44RbpiS}QKnD0skX{k{h*{$>XnhI4115okzfJeYyUAsiHVQX)6t=KxEh
    zjaEfU#fK_XATxsHn#|vUcfd@b1`Gk4fp0)hlmVAyzMKf?WDth$t&09I)+sZiqW+2S
    z{FZq>&{dF%MKR#xI#8GS3~&$F4?G9%8hJkuDZ5dEH?9k*JK|p*aLVjRuJ07hL*=SP
    z0v-S-fyV;Rl`QyG(GAY3gv(`qrpl%hvrQQS&P!eB0`3FzfLX0rAdIEJY~Y5#?K0mM
    zABU5k0)7Bbfnz`i@KvlhC0S?f_jr&F_6@DG~}LaPvcP#TWO+K{Xb
    z8^X^CZda>?NE3#pbVM*-ZtxoK@(SeNk=5nrx}PajGh9`V95oZf(-c%cj|vQMl3%$i+6Qaq+{XN(D>$
    zw5;k`50f_~O{FuMOjpv9$M_<*)e+P9l02TS*KsVrFKE;UjF-HGrsH)S=5gsY_yaw?
    V8ujrs9)kb?002ovPDHLkV1g|mgb@G$
    
    literal 0
    HcmV?d00001
    
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe1.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe1.svg
    new file mode 100755
    index 00000000..22bdd96e
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/safe1.svg
    @@ -0,0 +1,13 @@
    +
    +
    +
    +
    +	
    +		
    +		
    +		
    +		
    +	
    +	
    +
    +
    \ No newline at end of file
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/search_session.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/search_session.js
    new file mode 100644
    index 00000000..8b311148
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/search_session.js
    @@ -0,0 +1,279 @@
    +/*
    +Copyright 2013 Google Inc.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +goog.provide('cam.SearchSession');
    +
    +goog.require('goog.events.EventTarget');
    +goog.require('goog.Uri');
    +goog.require('goog.Uri.QueryData');
    +goog.require('goog.uri.utils');
    +
    +goog.require('cam.ServerConnection');
    +
    +// A search session is a standing query that notifies you when results change. It caches previous results and handles merging new data as it is received. It does not tell you _what_ changed; clients must reconcile as they see fit.
    +//
    +// TODO(aa): Only deltas should be sent from server to client
    +// TODO(aa): Need some way to avoid the duplicate query when websocket starts. Ideas:
    +// - Initial XHR query can also specify tag. This tag times out if not used rapidly. Send this same tag in socket query.
    +// - Socket assumes that client already has first batch of results (slightly racey though)
    +// - Prefer to use socket on client-side, test whether it works and fall back to XHR if not.
    +cam.SearchSession = function(connection, currentUri, query, opt_aroundBlobref) {
    +	goog.base(this);
    +
    +	this.connection_ = connection;
    +	this.currentUri_ = currentUri;
    +	this.initSocketUri_(currentUri);
    +	this.hasSocketError_ = false;
    +	this.query_ = query;
    +	this.around_ = opt_aroundBlobref;
    +	this.tag_ = 'q' + (this.constructor.instanceCount_++);
    +	this.continuation_ = this.getContinuation_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.NEW);
    +	this.socket_ = null;
    +	this.supportsWebSocket_ = false;
    +	this.isComplete_ = false;
    +
    +	this.resetData_();
    +};
    +goog.inherits(cam.SearchSession, goog.events.EventTarget);
    +
    +// We fire this event when the data changes in any way.
    +cam.SearchSession.SEARCH_SESSION_CHANGED = 'search-session-change';
    +
    +// We fire this event when the search session receives general server status data.
    +cam.SearchSession.SEARCH_SESSION_STATUS = 'search-session-status';
    +
    +// We fire this event when the search session encounters an error.
    +cam.SearchSession.SEARCH_SESSION_ERROR = 'search-session-error';
    +
    +// TODO(aa): This is only used by BlobItemContainer. Once we switch over to BlobItemContainerReact completely, it can be removed.
    +cam.SearchSession.SEARCH_SESSION_CHANGE_TYPE = {
    +	NEW: 1,
    +	APPEND: 2,
    +	UPDATE: 3
    +};
    +
    +cam.SearchSession.PAGE_SIZE_ = 50;
    +
    +cam.SearchSession.instanceCount_ = 0;
    +
    +cam.SearchSession.prototype.getQuery = function() {
    +	return this.query_;
    +};
    +
    +cam.SearchSession.prototype.getAround = function() {
    +	return this.around_;
    +};
    +
    +// Returns all the data we currently have loaded.
    +// It is guaranteed to return the following properties:
    +// blobs // non-null
    +// description
    +// description.meta
    +cam.SearchSession.prototype.getCurrentResults = function() {
    +	return this.data_;
    +};
    +
    +cam.SearchSession.prototype.hasSocketError = function() {
    +	return this.hasSocketError_;
    +};
    +
    +// Loads the next page of data. This is safe to call while a load is in progress; multiple calls for the same page will be collapsed. The SEARCH_SESSION_CHANGED event will be dispatched when the new data is available.
    +cam.SearchSession.prototype.loadMoreResults = function() {
    +	if (!this.continuation_) {
    +		return;
    +	}
    +
    +	var c = this.continuation_;
    +	this.continuation_ = null;
    +	c();
    +};
    +
    +// Returns true if it is known that all data which can be loaded for this query has been.
    +cam.SearchSession.prototype.isComplete = function() {
    +	return this.isComplete_;
    +}
    +
    +cam.SearchSession.prototype.supportsChangeNotifications = function() {
    +	return this.supportsWebSocket_;
    +};
    +
    +cam.SearchSession.prototype.refreshIfNecessary = function() {
    +	if (this.supportsWebSocket_) {
    +		return;
    +	}
    +
    +	this.continuation_ = this.getContinuation_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.UPDATE, null, Math.max(this.data_.blobs.length, this.constructor.PAGE_SIZE_));
    +	this.resetData_();
    +	this.loadMoreResults();
    +};
    +
    +cam.SearchSession.prototype.close = function() {
    +	if (this.socket_) {
    +		this.socket_.onerror = null;
    +		this.socket_.onclose = null;
    +		this.socket_.close();
    +	}
    +};
    +
    +cam.SearchSession.prototype.getMeta = function(blobref) {
    +	return this.data_.description.meta[blobref];
    +};
    +
    +cam.SearchSession.prototype.getResolvedMeta = function(blobref) {
    +	var meta = this.data_.description.meta[blobref];
    +	if (meta && meta.camliType == 'permanode') {
    +		var camliContent = cam.permanodeUtils.getSingleAttr(meta.permanode, 'camliContent');
    +		if (camliContent) {
    +			return this.data_.description.meta[camliContent];
    +		}
    +	}
    +	return meta;
    +};
    +
    +cam.SearchSession.prototype.getTitle = function(blobref) {
    +	var meta = this.getMeta(blobref);
    +	if (meta.camliType == 'permanode') {
    +		var title = cam.permanodeUtils.getSingleAttr(meta.permanode, 'title');
    +		if (title) {
    +			return title;
    +		}
    +	}
    +	var rm = this.getResolvedMeta(blobref);
    +	return (rm && rm.camliType == 'file' && rm.file.fileName) || (rm && rm.camliType == 'directory' && rm.dir.fileName) || '';
    +};
    +
    +cam.SearchSession.prototype.resetData_ = function() {
    +	this.data_ = {
    +		blobs: [],
    +		description: {
    +			meta: {}
    +		}
    +	};
    +};
    +
    +cam.SearchSession.prototype.initSocketUri_ = function(currentUri) {
    +	if (!goog.global.WebSocket) {
    +		return;
    +	}
    +
    +	this.socketUri_ = currentUri;
    +	this.socketUri_.setFragment('');
    +	var config = this.connection_.getConfig();
    +	this.socketUri_.setPath(goog.uri.utils.appendPath(config.searchRoot, 'camli/search/ws'));
    +	this.socketUri_.setQuery(goog.Uri.QueryData.createFromMap({authtoken: config.wsAuthToken || ''}));
    +	if (this.socketUri_.getScheme() == "https") {
    +		this.socketUri_.setScheme("wss");
    +	} else {
    +		this.socketUri_.setScheme("ws");
    +	}
    +};
    +
    +cam.SearchSession.prototype.getContinuation_ = function(changeType, opt_continuationToken, opt_limit) {
    +	return this.connection_.search.bind(this.connection_, this.query_, cam.ServerConnection.DESCRIBE_REQUEST, opt_limit || this.constructor.PAGE_SIZE_, opt_continuationToken,
    +		this.searchDone_.bind(this, changeType));
    +};
    +
    +cam.SearchSession.prototype.searchDone_ = function(changeType, result) {
    +	if (!result) {
    +		result = {};
    +	}
    +	if (!result.blobs) {
    +		result.blobs = [];
    +	}
    +	if (!result.description) {
    +		result.description = {};
    +	}
    +
    +	var changes = false;
    +
    +	if (changeType == this.constructor.SEARCH_SESSION_CHANGE_TYPE.APPEND) {
    +		changes = Boolean(result.blobs.length);
    +		this.data_.blobs = this.data_.blobs.concat(result.blobs);
    +		goog.mixin(this.data_.description.meta, result.description.meta);
    +	} else {
    +		changes = true;
    +		this.data_.blobs = result.blobs;
    +		this.data_.description = result.description;
    +	}
    +
    +	if (result.continue) {
    +		this.continuation_ = this.getContinuation_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.APPEND, result.continue);
    +	} else {
    +		this.continuation_ = null;
    +		this.isComplete_ = true;
    +	}
    +
    +	if (changes) {
    +		this.dispatchEvent({type: this.constructor.SEARCH_SESSION_CHANGED, changeType: changeType});
    +
    +		if (changeType == this.constructor.SEARCH_SESSION_CHANGE_TYPE.NEW || changeType == this.constructor.SEARCH_SESSION_CHANGE_TYPE.APPEND) {
    +			this.startSocketQuery_();
    +		}
    +	}
    +};
    +
    +cam.SearchSession.prototype.handleError_ = function(message) {
    +	this.hasSocketError_ = true;
    +	this.dispatchEvent({type: this.constructor.SEARCH_SESSION_ERROR});
    +};
    +
    +cam.SearchSession.prototype.handleStatus_ = function(data) {
    +	if (data.tag == '_status') {
    +		this.dispatchEvent({
    +			type: this.constructor.SEARCH_SESSION_STATUS,
    +			status: data.status,
    +		});
    +	}
    +};
    +
    +cam.SearchSession.prototype.startSocketQuery_ = function() {
    +	if (!this.socketUri_) {
    +		return;
    +	}
    +
    +	this.close();
    +
    +	var numResults = 0;
    +	if (this.data_ && this.data_.blobs) {
    +		numResults = this.data_.blobs.length;
    +	}
    +	var query = this.connection_.buildQuery(this.query_, cam.ServerConnection.DESCRIBE_REQUEST, Math.max(numResults, this.constructor.PAGE_SIZE_), null, this.around_);
    +
    +	this.socket_ = new WebSocket(this.socketUri_.toString());
    +	this.socket_.onopen = function() {
    +		var message = {
    +			tag: this.tag_,
    +			query: query
    +		};
    +		this.socket_.send(JSON.stringify(message));
    +	}.bind(this);
    +	this.socket_.onclose =
    +	this.socket_.onerror = function(e) {
    +		this.handleError_('WebSocket error - click to reload');
    +	}.bind(this);
    +	this.socket_.onmessage = function(e) {
    +		this.supportsWebSocket_ = true;
    +		this.handleStatus_(JSON.parse(e.data));
    +		// Ignore the first response.
    +		this.socket_.onmessage = function(e) {
    +			var result = JSON.parse(e.data);
    +			this.handleStatus_(result);
    +			if (result.tag == this.tag_) {
    +				this.searchDone_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.UPDATE, result.result);
    +			}
    +		}.bind(this);
    +	}.bind(this);
    +};
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/search_session_test.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/search_session_test.js
    new file mode 100644
    index 00000000..6fecf9ad
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/search_session_test.js
    @@ -0,0 +1,169 @@
    +/*
    +Copyright 2014 The Camlistore Authors
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	 http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +var assert = require('assert');
    +
    +goog.require('cam.SearchSession');
    +
    +
    +function MockServerConnection(response) {
    +	this.response_ = response;
    +}
    +
    +MockServerConnection.prototype.search = function(query, describe, limit, continuationToken, callback) {
    +	setImmediate(callback.bind(null, this.response_));
    +};
    +
    +
    +describe('cam.SearchSession', function() {
    +	var session = null;
    +	var response = {
    +		blobs: [
    +			{'blob': 'a'},
    +			{'blob': 'b'},
    +			{'blob': 'c'},
    +			{'blob': 'd'},
    +			{'blob': 'e'},
    +		],
    +		description: {
    +			meta: {
    +				a: {
    +					blobRef: 'a',
    +					camliType: 'file',
    +					file: {
    +						fileName: 'foo.txt',
    +					},
    +				},
    +				a2: {
    +					blobRef: 'a2',
    +					camliType: 'file',
    +					file: {
    +					},
    +				},
    +				b: {
    +					blobRef: 'b',
    +					camliType: 'permanode',
    +					permanode: {
    +						attr: {
    +							camliContent: ['a'],
    +							title: ['permanode b'],
    +						}
    +					}
    +				},
    +				b2: {
    +					blobRef: 'b2',
    +					camliType: 'permanode',
    +					permanode: {
    +						attr: {
    +							camliContent: ['a'],
    +						}
    +					}
    +				},
    +				c: {
    +					blobRef: 'c',
    +					camliType: 'permanode',
    +					permanode: {
    +						attr: {
    +						},
    +					}
    +				},
    +				d: {
    +					blobRef: 'd',
    +					camliType: 'permanode',
    +					permanode: {
    +						attr: {
    +							camliContent: ['b'],
    +						}
    +					}
    +				},
    +				e: {
    +					blobRef: 'e',
    +					camliType: 'permanode',
    +					permanode: {
    +						attr: {
    +							camliContent: ['_non_existant_'],
    +							title: 'permanode e',
    +						}
    +					}
    +				},
    +			}
    +		}
    +	};
    +
    +	before(function(done) {
    +		var currentUri = null;
    +		var query = null;
    +		session = new cam.SearchSession(new MockServerConnection(response), currentUri, query);
    +		session.addEventListener(cam.SearchSession.SEARCH_SESSION_CHANGED, function() {
    +			assert.equal(response.description.meta.a, session.getResolvedMeta('a'));
    +			done();
    +		});
    +		session.loadMoreResults();
    +	});
    +
    +	describe('#getResolvedMeta', function() {
    +		it('should resolve blobrefs correctly', function() {
    +			// a is not a permanode, so its resolved value is itself.
    +			assert.equal(response.description.meta.a, session.getResolvedMeta('a'));
    +
    +			// b is a permanode that points to a.
    +			assert.equal(response.description.meta.a, session.getResolvedMeta('b'));
    +
    +			// c is a permanode, but has no camliContent, so its resolved value is itself.
    +			assert.equal(response.description.meta.c, session.getResolvedMeta('c'));
    +
    +			// We currently only resolve one level of indirection via permanodes.
    +			assert.equal(response.description.meta.b, session.getResolvedMeta('d'));
    +
    +			// e is a permanode, but its camliContent doesn't exist. This is legitimate and can happen for a variety of reasons (e.g., during sync).
    +			assert.equal(null, session.getResolvedMeta('e'));
    +
    +			// z doesn't exist at all.
    +			assert.equal(null, session.getResolvedMeta('z'));
    +		});
    +	});
    +
    +	describe('#getTitle', function() {
    +		it('should create correct titles', function() {
    +			assert.strictEqual(response.description.meta.a.file.fileName, session.getTitle('a'));
    +			assert.strictEqual('', session.getTitle('a2'));
    +			assert.strictEqual('permanode b', session.getTitle('b'));
    +			assert.strictEqual(response.description.meta.a.file.fileName, session.getTitle('b2'));
    +			assert.strictEqual('permanode e', session.getTitle('e'));
    +		});
    +	});
    +});
    +
    +describe('cam.SearchSession', function() {
    +	var session = null;
    +	var response = {};
    +
    +	before(function() {
    +		var currentUri = null;
    +		var query = null;
    +		session = new cam.SearchSession(new MockServerConnection(response), currentUri, query);
    +	});
    +
    +	describe('new session, no results', function() {
    +		it('should not hit a null', function() {
    +			// resetData_ gives us a safe to use data_ (non null fields).
    +			assert(session.data_.blobs);
    +			assert(session.data_.description);
    +			assert(session.data_.description.meta);
    +		});
    +	});
    +
    +});
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/server_connection.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/server_connection.js
    new file mode 100644
    index 00000000..e8a28d0e
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/server_connection.js
    @@ -0,0 +1,633 @@
    +/*
    +Copyright 2013 Google Inc.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +goog.provide('cam.ServerConnection');
    +
    +goog.require('goog.string');
    +goog.require('goog.net.XhrIo');
    +goog.require('goog.Uri'); // because goog.net.XhrIo forgot to include it.
    +goog.require('goog.debug.ErrorHandler'); // because goog.net.Xhrio forgot to include it.
    +goog.require('goog.uri.utils');
    +
    +goog.require('cam.blob');
    +goog.require('cam.ServerType');
    +goog.require('cam.WorkerMessageRouter');
    +
    +// @fileoverview Connection to the blob server and API for the RPCs it provides. All blob index UI code should use this connection to contact the server.
    +// @param {cam.ServerType.DiscoveryDocument} config Discovery document for the current server.
    +// @param {Function=} opt_sendXhr Function for sending XHRs for testing.
    +// @constructor
    +cam.ServerConnection = function(config, opt_sendXhr) {
    +	this.config_ = config;
    +	this.sendXhr_ = opt_sendXhr || goog.net.XhrIo.send;
    +	this.worker_ = null;
    +};
    +
    +cam.ServerConnection.DESCRIBE_REQUEST = {
    +	// TODO(aa): This is not perfect. The describe request will return some data we don't care about:
    +	// - Properties we don't use
    +	// See: https://camlistore.org/issue/319
    +
    +	depth: 1,
    +	rules: [
    +		{
    +			attrs: ['camliContent', 'camliContentImage']
    +		},
    +		{
    +			ifCamliNodeType: 'foursquare.com:checkin',
    +			attrs: ['foursquareVenuePermanode']
    +		},
    +		{
    +			ifCamliNodeType: 'foursquare.com:venue',
    +			attrs: ['camliPath:photos'],
    +			rules: [
    +				{ attrs: ['camliPath:*'] }
    +			]
    +		}
    +	]
    +};
    +
    +cam.ServerConnection.prototype.getPermanodeWithContent = function(contentRef, success, opt_fail) {
    +	var query = {
    +		permanode: {
    +			attr: "camliContent",
    +			value: contentRef,
    +		},
    +	};
    +	var callback = function(result) {
    +		if (!result || !result.blobs || result.blobs.length == 0) {
    +			success();
    +			return;
    +		}
    +		success(result.blobs[0].blob);
    +	}
    +	this.search(query, null, null, null, callback);
    +};
    +
    +cam.ServerConnection.prototype.getWorker_ = function() {
    +	if (!this.worker_) {
    +		var r = new Date().getTime(); // For cachebusting the worker. Sigh. We need content stamping.
    +		this.worker_ = new cam.WorkerMessageRouter(new Worker('hash_worker.js?r=' + r));
    +	}
    +	return this.worker_;
    +};
    +
    +cam.ServerConnection.prototype.getConfig = function() {
    +	return this.config_;
    +};
    +
    +// @param {string} blobref blobref whose contents we want.
    +// @param {Function} success callback with data.
    +// @param {?Function} opt_fail optional failure calback
    +cam.ServerConnection.prototype.getBlobContents = function(blobref, success, opt_fail) {
    +	var path = goog.uri.utils.appendPath(
    +		this.config_.blobRoot, 'camli/' + blobref
    +	);
    +	this.sendXhr_(path,
    +		goog.bind(this.handleXhrResponseText_, this,
    +			{success: success, fail: opt_fail}
    +		)
    +	);
    +};
    +
    +// @param {goog.events.Event} e Event that triggered this
    +cam.ServerConnection.prototype.handleXhrResponseJson_ = function(callbacks, e) {
    +	var success = callbacks.success
    +	var fail = callbacks.fail
    +	var xhr = e.target;
    +	var error = !xhr.isSuccess();
    +	var result = null;
    +
    +	try {
    +		result = xhr.getResponseJson();
    +	} catch(err) {
    +		result = "Response was not valid JSON: " + xhr.getResponseText();
    +	}
    +
    +	if (error) {
    +		if (fail) {
    +			fail(result.error || result);
    +		} else {
    +			console.log('Failed XHR (JSON) in ServerConnection: ' + result.error || result);
    +		}
    +	} else {
    +		success(result);
    +	}
    +};
    +
    +// @param {Function} success callback with data.
    +// @param {?Function} opt_fail optional failure calback
    +cam.ServerConnection.prototype.discoSignRoot = function(success, opt_fail) {
    +	var path = goog.uri.utils.appendPath(this.config_.jsonSignRoot, '/camli/sig/discovery');
    +	this.sendXhr_(path, goog.bind(this.handleXhrResponseJson_, this, {success: success, fail: opt_fail}));
    +};
    +
    +// @param {function(cam.ServerType.StatusResponse)} success.
    +cam.ServerConnection.prototype.serverStatus = function(success) {
    +	var path = goog.uri.utils.appendPath(this.config_.statusRoot, 'status.json');
    +
    +	this.sendXhr_(path,
    +		goog.bind(this.handleXhrResponseJson_, this, {success: success, fail: function(msg) {
    +			console.log("serverStatus error: " + msg);
    +		}}));
    +};
    +
    +// @param {string} blobref root of the tree
    +// @param {Function} success callback with data.
    +// @param {?Function} opt_fail optional failure calback
    +cam.ServerConnection.prototype.getFileTree = function(blobref, success, opt_fail) {
    +	// TODO(mpl): do it relatively to a discovered root?
    +	var path = "./tree/" + blobref;
    +	this.sendXhr_(path, goog.bind(this.handleXhrResponseJson_, this, {success: success, fail: opt_fail}));
    +};
    +
    +
    +// @param {string} signer permanode must belong to signer.
    +// @param {string} attr searched attribute.
    +// @param {string} value value of the searched attribute.
    +// @param {Function} success.
    +// @param {Function=} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.permanodeOfSignerAttrValue = function(signer, attr, value, success, opt_fail) {
    +	var path = goog.uri.utils.appendPath(this.config_.searchRoot, 'camli/search/signerattrvalue');
    +	path = goog.uri.utils.appendParams(path,
    +		'signer', signer, 'attr', attr, 'value', value
    +	);
    +
    +	this.sendXhr_(
    +		path,
    +		goog.bind(this.handleXhrResponseJson_, this,
    +			{success: success, fail: opt_fail}
    +		)
    +	);
    +};
    +
    +// @param {string|object} query If string, will be sent as 'expression', otherwise will be sent as 'constraint'.
    +// @param {?object} opt_describe The describe property to send for the query
    +cam.ServerConnection.prototype.buildQuery = function(callerQuery, opt_describe, opt_limit, opt_continuationToken, opt_around) {
    +	var query = {
    +		// TODO(mpl): it'd be better to not ask for a sort when none is needed (less work for server),
    +		// e.g. for a plain BlobRefPrefix query.
    +		sort: "-created"
    +	};
    +
    +	if (goog.isString(callerQuery)) {
    +		query.expression = callerQuery;
    +	} else {
    +		query.constraint = callerQuery;
    +	}
    +
    +	if (opt_describe) {
    +		query.describe = opt_describe;
    +	}
    +	if (opt_limit) {
    +		query.limit = opt_limit;
    +	}
    +	if (opt_around) {
    +		query.around = opt_around;
    +	} else if (opt_continuationToken) {
    +		query.continue = opt_continuationToken;
    +	}
    +
    +	return query;
    +}
    +
    +// @param {string|object} query If string, will be sent as 'expression', otherwise will be sent as 'constraint'.
    +// @param {?object} opt_describe The describe property to send for the query
    +cam.ServerConnection.prototype.search = function(query, opt_describe, opt_limit, opt_continuationToken, callback) {
    +	var path = goog.uri.utils.appendPath(this.config_.searchRoot, 'camli/search/query');
    +	this.sendXhr_(path,
    +		goog.bind(this.handleXhrResponseJson_, this, {success: callback}),
    +		"POST", JSON.stringify(this.buildQuery(query, opt_describe, opt_limit, opt_continuationToken)));
    +};
    +
    +// Where is the target accessed via? (paths it's at)
    +// @param {string} signer owner of permanode.
    +// @param {string} target blobref of permanode we want to find paths to
    +// @param {Function} success.
    +// @param {Function=} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.pathsOfSignerTarget = function(target, success, opt_fail) {
    +	var path = goog.uri.utils.appendPath(
    +		this.config_.searchRoot, 'camli/search/signerpaths'
    +	);
    +	path = goog.uri.utils.appendParams(path, 'signer', this.config_.signing.publicKeyBlobRef, 'target', target);
    +	this.sendXhr_(path,
    +		goog.bind(this.handleXhrResponseJson_, this, {success: success, fail: opt_fail}));
    +};
    +
    +// @param {string} permanode Permanode blobref.
    +// @param {Function} success.
    +// @param {Function=} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.permanodeClaims = function(permanode, success, opt_fail) {
    +	var path = goog.uri.utils.appendPath(
    +		this.config_.searchRoot, 'camli/search/claims?permanode=' + permanode
    +	);
    +
    +	this.sendXhr_(
    +		path,
    +		goog.bind(this.handleXhrResponseJson_, this,
    +			{success: success, fail: opt_fail}
    +		)
    +	);
    +};
    +
    +// @param {Object} clearObj Unsigned object.
    +// @param {Function} success Success callback.
    +// @param {?Function} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.sign_ = function(clearObj, success, opt_fail) {
    +	var sigConf = this.config_.signing;
    +	if (!sigConf || !sigConf.publicKeyBlobRef) {
    +		this.failOrLog_(opt_fail, "Missing Camli.config.signing.publicKeyBlobRef");
    +		return;
    +	}
    +
    +	clearObj.camliSigner = sigConf.publicKeyBlobRef;
    +	var camVersion = clearObj.camliVersion;
    +	if (camVersion) {
    +		 delete clearObj.camliVersion;
    +	}
    +	var clearText = JSON.stringify(clearObj, null, "	");
    +	if (camVersion) {
    +		 clearText = "{\"camliVersion\":" + camVersion + ",\n" + clearText.substr("{\n".length);
    +	}
    +
    +	this.sendXhr_(
    +		sigConf.signHandler,
    +		goog.bind(this.handleXhrResponseText_, this,
    +			{success: success, fail: opt_fail}),
    +		"POST",
    +		"json=" + encodeURIComponent(clearText),
    +		{"Content-Type": "application/x-www-form-urlencoded"}
    +	);
    +};
    +
    +// @param {Object} signed Signed JSON blob (string) to verify.
    +// @param {Function} success Success callback.
    +// @param {?Function} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.verify_ = function(signed, success, opt_fail) {
    +	var sigConf = this.config_.signing;
    +	if (!sigConf || !sigConf.publicKeyBlobRef) {
    +		if (opt_fail) {
    +			opt_fail("Missing Camli.config.signing.publicKeyBlobRef");
    +		} else {
    +			console.log("Missing Camli.config.signing.publicKeyBlobRef");
    +		}
    +		return;
    +	}
    +	this.sendXhr_(
    +		sigConf.verifyHandler,
    +		goog.bind(this.handleXhrResponseText_, this,
    +			{success: success, fail: opt_fail}),
    +		"POST",
    +		"sjson=" + encodeURIComponent(signed),
    +		{"Content-Type": "application/x-www-form-urlencoded"}
    +	);
    +};
    +
    +// @param {goog.events.Event} e Event that triggered this
    +cam.ServerConnection.prototype.handleXhrResponseText_ = function(callbacks, e) {
    +	var fail = callbacks.fail;
    +	var xhr = e.target;
    +	var error = !xhr.isSuccess();
    +	var result = null;
    +	if (!error) {
    +		result = xhr.getResponseText();
    +		error = !result;
    +	}
    +	if (error) {
    +		if (fail) {
    +			fail(xhr.getLastError());
    +		} else {
    +			// TODO(bslatkin): Add a default failure event handler to this class.
    +			console.log('Failed XHR (text) in ServerConnection: ' + xhr.getLastError());
    +		}
    +		return;
    +	}
    +	callbacks.success(result);
    +};
    +
    +// @param {string} s String to upload.
    +// @param {Function} success Success callback.
    +// @param {?Function} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.uploadString_ = function(s, success, opt_fail) {
    +	var blobref = cam.blob.refFromString(s);
    +	var parts = [s];
    +	var bb = new Blob(parts);
    +	var fd = new FormData();
    +	fd.append(blobref, bb);
    +
    +	// TODO: hack, hard-coding the upload URL here.
    +	// Change the spec now that App Engine permits 32 MB requests
    +	// and permit a PUT request on the sha1?	Or at least let us
    +	// specify the well-known upload URL?	In cases like this, uploading
    +	// a new permanode, it's silly to even stat.
    +	this.sendXhr_(
    +		this.config_.blobRoot + "camli/upload",
    +		goog.bind(this.handleUploadString_, this,
    +			blobref,
    +			{success: success, fail: opt_fail}
    +		),
    +		"POST",
    +		fd
    +	);
    +};
    +
    +// @param {string} blobref Uploaded blobRef.
    +// @param {goog.events.Event} e Event that triggered this
    +cam.ServerConnection.prototype.handleUploadString_ = function(blobref, callbacks, e) {
    +	this.handleXhrResponseText_({
    +		success: function(resj) {
    +			if (!resj) {
    +				alert("upload failed; no response");
    +				return;
    +			}
    +			var resObj = JSON.parse(resj);
    +			if (!resObj.received || !resObj.received[0] || !resObj.received[0].blobRef) {
    +				alert("upload permanode fail, expected blobRef not in response");
    +				return;
    +			}
    +			if (callbacks.success) {
    +				callbacks.success(blobref);
    +			}
    +		},
    +		fail: callbacks.fail},
    +		e
    +	)
    +};
    +
    +cam.ServerConnection.prototype.failOrLog_ = function(fail, msg) {
    +	if (fail) {
    +		fail(msg);
    +	} else {
    +		console.log(msg);
    +	}
    +};
    +
    +// @param {Function} success Success callback.
    +// @param {?Function} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.createPermanode = function(success, opt_fail) {
    +	var json = {
    +		"camliVersion": 1,
    +		"camliType": "permanode",
    +		"random": ""+Math.random()
    +	};
    +	this.sign_(json,
    +		goog.bind(function(signed) {
    +			this.uploadString_(signed, success, opt_fail)
    +		}, this),
    +		goog.bind(function(msg) {
    +			this.failOrLog_(opt_fail, "create permanode: signing failed: " + msg);
    +		}, this)
    +	);
    +};
    +
    +// @param {string} permanode Permanode to change.
    +// @param {string} claimType What kind of claim: "add-attribute", "set-attribute"...
    +// @param {string} attribute What attribute the claim applies to.
    +// @param {string} value Attribute value.
    +// @param {Function} success Success callback.
    +// @param {?Function} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.changeAttribute_ = function(permanode, claimType, attribute, value, success, opt_fail) {
    +	var json = {
    +		"camliVersion": 1,
    +		"camliType": "claim",
    +		"permaNode": permanode,
    +		"claimType": claimType,
    +		// TODO(mpl): to (im)port.
    +		"claimDate": dateToRfc3339String(new Date()),
    +		"attribute": attribute,
    +		"value": value
    +	};
    +	this.sign_(json,
    +		goog.bind(function(signed) {
    +			this.uploadString_(signed, success, opt_fail)
    +		}, this),
    +		goog.bind(function(msg) {
    +			this.failOrLog_(opt_fail, "change attribute: signing failed: " + msg);
    +		}, this)
    +	);
    +};
    +
    +// @param {string} permanode Permanode to delete.
    +// @param {Function} success Success callback.
    +// @param {?Function} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.newDeleteClaim = function(permanode, success, opt_fail) {
    +	var json = {
    +		"camliVersion": 1,
    +		"camliType": "claim",
    +		"target": permanode,
    +		"claimType": "delete",
    +		"claimDate": dateToRfc3339String(new Date())
    +	};
    +	this.sign_(json,
    +		goog.bind(function(signed) {
    +			this.uploadString_(signed, success, opt_fail)
    +		}, this),
    +		goog.bind(function(msg) {
    +			this.failOrLog_(opt_fail, "delete attribute: signing failed: " + msg);
    +		}, this)
    +	);
    +};
    +
    +// @param {string} permanode Permanode blobref.
    +// @param {string} attribute Name of the attribute to set.
    +// @param {string} value Value to set the attribute to.
    +// @param {function(string)} success Success callback, called with blobref of uploaded file.
    +// @param {?Function} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.newSetAttributeClaim = function(permanode, attribute, value, success, opt_fail) {
    +	this.changeAttribute_(permanode, "set-attribute", attribute, value,
    +		success, opt_fail
    +	);
    +};
    +
    +
    +// @param {string} permanode Permanode blobref.
    +// @param {string} attribute Name of the attribute to add.
    +// @param {string} value Value of the added attribute.
    +// @param {function(string)} success Success callback, called with blobref of uploaded file.
    +// @param {?Function} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.newAddAttributeClaim = function(permanode, attribute, value, success, opt_fail) {
    +	this.changeAttribute_(permanode, "add-attribute", attribute, value,
    +		success, opt_fail
    +	);
    +};
    +
    +// @param {string} permanode Permanode blobref.
    +// @param {string} attribute Name of the attribute to delete.
    +// @param {string} value Value of the attribute to delete.
    +// @param {function(string)} success Success callback, called with blobref of uploaded file.
    +// @param {?Function} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.newDelAttributeClaim = function(permanode, attribute, value, success, opt_fail) {
    +	this.changeAttribute_(permanode, "del-attribute", attribute, value,
    +		success, opt_fail
    +	);
    +};
    +
    +// @param {File} file File to be uploaded.
    +// @param {function(string)} success Success callback, called with blobref of
    +// uploaded file.
    +// @param {?Function} opt_fail Optional fail callback.
    +// @param {?Function} opt_onContentsRef Optional callback to set contents during upload.
    +cam.ServerConnection.prototype.uploadFile = function(file, success, opt_fail, opt_onContentsRef) {
    +	this.getWorker_().sendMessage('ref', file, function(ref) {
    +		if (opt_onContentsRef) {
    +			opt_onContentsRef(ref);
    +		}
    +		this.camliUploadFileHelper_(file, ref, success, opt_fail);
    +	}.bind(this));
    +};
    +
    +// camliUploadFileHelper uploads the provided file with contents blobref contentsBlobRef
    +// and returns a blobref of a file blob.	It does not create any permanodes.
    +// Most callers will use camliUploadFile instead of this helper.
    +//
    +// camliUploadFileHelper only uploads chunks of the file if they don't already exist
    +// on the server. It starts by assuming the file might already exist on the server
    +// and, if so, uses an existing (but re-verified) file schema ref instead.
    +// @param {File} file File to be uploaded.
    +// @param {string} contentsBlobRef Blob ref of file as sha1'd locally.
    +// @param {function(string)} success function(fileBlobRef) of the
    +// server-validated or just-uploaded file schema blob.
    +// @param {?Function} opt_fail Optional fail callback.
    +cam.ServerConnection.prototype.camliUploadFileHelper_ = function(file, contentsBlobRef, success, opt_fail) {
    +	if (!this.config_.uploadHelper) {
    +		this.failOrLog_(opt_fail, "no uploadHelper available");
    +		return;
    +	}
    +
    +	var doUpload = goog.bind(function() {
    +		var fd = new FormData();
    +		fd.append("modtime", dateToRfc3339String(file.lastModifiedDate));
    +		fd.append("ui-upload-file-helper-form", file);
    +		this.sendXhr_(
    +			this.config_.uploadHelper,
    +			goog.bind(this.handleUpload_, this,
    +				file, contentsBlobRef, {success: success, fail: opt_fail}
    +			),
    +			"POST",
    +			fd
    +		);
    +	}, this);
    +
    +	this.findExistingFileSchemas_(
    +		contentsBlobRef,
    +		goog.bind(this.dupCheck_, this,
    +			doUpload, contentsBlobRef, success
    +		),
    +		opt_fail
    +	)
    +}
    +
    +// @param {File} file File to be uploaded.
    +// @param {string} contentsBlobRef Blob ref of file as sha1'd locally.
    +// @param {goog.events.Event} e Event that triggered this
    +cam.ServerConnection.prototype.handleUpload_ = function(file, contentsBlobRef, callbacks, e) {
    +	this.handleXhrResponseText_({
    +		success: goog.bind(function(res) {
    +			var resObj = JSON.parse(res);
    +			if (resObj.got && resObj.got.length == 1 && resObj.got[0].fileref) {
    +				var fileblob = resObj.got[0].fileref;
    +				console.log("uploaded " + contentsBlobRef + " => file blob " + fileblob);
    +				callbacks.success(fileblob);
    +			} else {
    +				this.failOrLog_(callbacks.fail, "failed to upload " + file.name + ": " + contentsBlobRef + ": " + JSON.stringify(res, null, 2));
    +			}
    +		}, this),
    +		fail: callbacks.fail},
    +		e
    +	)
    +};
    +
    +// @param {string} wholeDigestRef file digest.
    +// @param {Function} success callback with data.
    +// @param {?Function} opt_fail optional failure calback
    +cam.ServerConnection.prototype.findExistingFileSchemas_ = function(wholeDigestRef, success, opt_fail) {
    +	var path = goog.uri.utils.appendPath(this.config_.searchRoot, 'camli/search/files');
    +	path = goog.uri.utils.appendParam(path, 'wholedigest', wholeDigestRef);
    +
    +	this.sendXhr_(
    +		path,
    +		goog.bind(this.handleXhrResponseJson_, this,
    +			{success: success, fail: opt_fail}
    +		)
    +	);
    +};
    +
    +// @param {Function} doUpload fun that takes care of uploading.
    +// @param {string} contentsBlobRef Blob ref of file as sha1'd locally.
    +// @param {Function} success Success callback.
    +// @param {Object} res result from the wholedigest search.
    +cam.ServerConnection.prototype.dupCheck_ = function(doUpload, contentsBlobRef, success, res) {
    +	var remain = res.files;
    +	var checkNext = goog.bind(function(files) {
    +		if (files.length == 0) {
    +			doUpload();
    +			return;
    +		}
    +		// TODO: verify filename and other file metadata in the
    +		// file json schema match too, not just the contents
    +		var checkFile = files[0];
    +		console.log("integrity checking the reported dup " + checkFile);
    +
    +		// TODO(mpl): see about passing directly a ref of files maybe instead of a copy?
    +		// just being careful for now.
    +		this.sendXhr_(
    +			this.config_.downloadHelper + checkFile + "/?verifycontents=" + contentsBlobRef,
    +			goog.bind(this.handleVerifycontents_, this,
    +				contentsBlobRef, files.slice(), checkNext, success),
    +			"HEAD"
    +		);
    +	}, this);
    +	checkNext(remain);
    +}
    +
    +// @param {string} contentsBlobRef Blob ref of file as sha1'd locally.
    +// @param {Array.} files files to check.
    +// @param {Function} checkNext fun, recursive call.
    +// @param {Function} success Success callback.
    +// @param {goog.events.Event} e Event that triggered this
    +cam.ServerConnection.prototype.handleVerifycontents_ = function(contentsBlobRef, files, checkNext, success, e) {
    +	var xhr = e.target;
    +	var error = !(xhr.isComplete() && xhr.getStatus() == 200);
    +	var checkFile = files.shift();
    +
    +	if (error) {
    +		console.log("integrity check failed on " + checkFile);
    +		checkNext(files);
    +		return;
    +	}
    +	if (xhr.getResponseHeader("X-Camli-Contents") == contentsBlobRef) {
    +		console.log("integrity check passed on " + checkFile + "; using it.");
    +		success(checkFile);
    +	} else {
    +		checkNext(files);
    +	}
    +};
    +
    +// Format |dateVal| as specified by RFC 3339.
    +function dateToRfc3339String(dateVal) {
    +	// Return a string containing |num| zero-padded to |length| digits.
    +	var pad = function(num, length) {
    +		var numStr = "" + num;
    +		while (numStr.length < length) {
    +			numStr = "0" + numStr;
    +		}
    +		return numStr;
    +	};
    +
    +	return goog.string.subs("%s-%s-%sT%s:%s:%sZ",
    +		dateVal.getUTCFullYear(), pad(dateVal.getUTCMonth() + 1, 2), pad(dateVal.getUTCDate(), 2),
    +		pad(dateVal.getUTCHours(), 2), pad(dateVal.getUTCMinutes(), 2), pad(dateVal.getUTCSeconds(), 2));
    +};
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/server_type.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/server_type.js
    new file mode 100644
    index 00000000..339eb9c9
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/server_type.js
    @@ -0,0 +1,136 @@
    +/**
    + * @fileoverview Helpers and types for JSON objects returned by the server.
    + */
    +goog.provide('cam.ServerType');
    +
    +
    +/**
    + * @typedef {{
    + *   currentPermanode: string,
    + *   name: string,
    + *   prefix: Array.
    + * }}
    + */
    +cam.ServerType.DiscoveryRoot;
    +
    +
    +/**
    + * @typedef {{
    + *   blobRoot: string,
    + *   directoryHelper: string,
    + *   downloadHelper: string,
    + *   helpRoot: string,
    + *   jsonSignRoot: string,
    + *   ownerName: string,
    + *   publishRoots: Array.,
    + *   searchRoot: string,
    + *   statusRoot: string,
    + *   storageGeneration: string,
    + *   storageInitTime: string,
    + *   signing: cam.ServerType.SigningDiscoveryDocument,
    + *   uploadHelper: string
    + * }}
    + */
    +cam.ServerType.DiscoveryDocument;
    +
    +/**
    + * @typedef {{
    + *   publicKey: string,
    + *   publicKeyBlobRef: string,
    + *   publicKeyId: string,
    + *   signHandler: string,
    + *   verifyHandler: string
    + * }}
    + */
    +cam.ServerType.SigningDiscoveryDocument;
    +
    +/**
    + * @typedef {{
    + *   fileName: string,
    + *   mimeType: string,
    + *   size: number
    + * }}
    + */
    +cam.ServerType.IndexerFileMeta;
    +
    +
    +/**
    + * @typedef {{
    + *   title: string,
    + *   camliContent: Array.
    + * }}
    + */
    +cam.ServerType.IndexerPermanodeAttrMeta;
    +
    +
    +/**
    + * @typedef {{
    + *   attr: cam.ServerType.IndexerPermanodeAttrMeta?
    + * }}
    + */
    +cam.ServerType.IndexerPermanodeMeta;
    +
    +
    +/**
    + * @typedef {{
    + *   blobRef: string,
    + *   camliType: string,
    + *   file: cam.ServerType.IndexerFileMeta?,
    + *   mimeType: string,
    + *   permanode: cam.ServerType.IndexerPermanodeMeta?,
    + *   size: number,
    + * }}
    + */
    +cam.ServerType.IndexerMeta;
    +
    +
    +/**
    + * @typedef {Object.}
    + */
    +cam.ServerType.IndexerMetaBag;
    +
    +/**
    + * @typedef {{
    + *   blobref: string,
    + *   modtime: string,
    + *   owner: string
    + * }}
    +*/
    +cam.ServerType.SearchRecentItem;
    +
    +/**
    + * @typedef {{
    + *   recent: Array.,
    + *   meta: cam.ServerType.IndexerMetaBag
    + * }}
    +*/
    +cam.ServerType.SearchRecentResponse;
    +
    +/**
    + * @typedef {{
    + *   permanode: string
    + * }}
    +*/
    +cam.ServerType.SearchWithAttrItem;
    +
    +/**
    + * @typedef {{
    + *   withAttr: Array.,
    + *   meta: cam.ServerType.IndexerMetaBag
    + * }}
    +*/
    +cam.ServerType.SearchWithAttrResponse;
    +
    +/**
    + * @typedef {{
    + *   meta: cam.ServerType.IndexerMetaBag
    + * }}
    +*/
    +cam.ServerType.DescribeResponse;
    +
    +/**
    + * @typedef {{
    + *   version: string,
    + * }}
    +*/
    +cam.ServerType.StatusResponse;
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sidebar.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sidebar.css
    new file mode 100644
    index 00000000..157d9ff6
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sidebar.css
    @@ -0,0 +1,100 @@
    +/*
    +Copyright 2014 The Camlistore Authors.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +@import (less) "prefix-free.css";
    +
    +/* TODO: can the positioning (top: 38px) be pulled from the header.css? */
    +.cam-sidebar {
    +	width: 250px;
    +	height: 100%;
    +	position: fixed;
    +	top: 38px;
    +	right: 0;
    +
    +	background-color: #e6e6e6;
    +	color: #444;
    +
    +	.transform(translate3d(0, 0, 0));
    +	.transition-transform(100ms ease-out);
    +
    +	&.cam-sidebar-hidden {
    +		.transform(translate3d(100%, 0, 0));
    +	}
    +}
    +
    +.cam-sidebar {
    +	padding: 5px 0px;
    +}
    +
    +.cam-sidebar, .cam-sidebar-collapsible-section-header {
    +	> .header {
    +		border-bottom: 1px solid #ccc;
    +		display: inline-block;
    +		width: 100%;
    +		cursor: default;
    +		font-family: 'Open Sans', sans-serif;
    +		font-weight: 100;
    +		font-size: 14px;
    +		line-height: 38px;
    +		padding: 0 28px;
    +		text-align: left;
    +		vertical-align: middle;
    +		white-space: nowrap;
    +	}
    +
    +	> button {
    +		width: 100%;
    +		height: 38px;
    +		cursor: pointer;
    +		background: transparent;
    +		border: none;
    +		font-family: 'Open Sans', sans-serif;
    +		font-weight: 100;
    +		font-size: 14px;
    +		padding: 0 28px;
    +		position: relative;
    +		text-align: left;
    +		white-space: nowrap;
    +
    +		> i {
    +			color: #666;
    +			cursor: pointer;
    +			display: block;
    +			left: 2px;
    +			line-height: 38px;
    +			position: absolute;
    +			text-align: center;
    +			top: 0;
    +			width: 26px;
    +		}
    +
    +		&:focus {
    +			outline: none;
    +		}
    +
    +		&:hover {
    +			background: #d6d6d6;
    +		}
    +
    +		&:active {
    +			outline: none;
    +		}
    +	}
    +}
    +
    +.cam-sidebar-section {
    +	padding: 0 28px;
    +}
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sidebar.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sidebar.js
    new file mode 100644
    index 00000000..4093730b
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sidebar.js
    @@ -0,0 +1,147 @@
    +/*
    +Copyright 2014 The Camlistore Authors
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +goog.provide('cam.Sidebar');
    +
    +goog.require('goog.array');
    +goog.require('goog.object');
    +goog.require('goog.string');
    +
    +goog.require('cam.ServerConnection');
    +
    +cam.Sidebar = React.createClass({
    +	displayName: 'Sidebar',
    +
    +	propTypes: {
    +		isExpanded: React.PropTypes.bool.isRequired,
    +		header: React.PropTypes.renderable,
    +		mainControls: React.PropTypes.arrayOf(
    +			React.PropTypes.shape(
    +				{
    +					displayTitle: React.PropTypes.string.isRequired,
    +					control: React.PropTypes.renderable.isRequired,
    +				}
    +			)
    +		),
    +		selectionControls: React.PropTypes.arrayOf(React.PropTypes.renderable).isRequired,
    +		selectedItems: React.PropTypes.object.isRequired,
    +	},
    +
    +	getInitialState: function() {
    +		return {
    +			openControls: [],	// all controls that are currently 'open'
    +		};
    +	},
    +
    +	render: function() {
    +		return React.DOM.div(
    +			{
    +				className: React.addons.classSet({
    +					'cam-sidebar': true,
    +					'cam-sidebar-hidden': !this.props.isExpanded,
    +				})
    +			},
    +			this.props.header,
    +			this.props.selectionControls,
    +			this.getMainControls_()
    +		);
    +	},
    +
    +	getMainControls_: function() {
    +		return this.props.mainControls.map(
    +			function(c) {
    +				return cam.CollapsibleControl(
    +				{
    +					key: c.displayTitle,
    +					control: c.control,
    +					isOpen: this.isControlOpen_(c.displayTitle),
    +					onToggleOpen: this.handleToggleControlOpen_,
    +					title: c.displayTitle
    +				});
    +			}.bind(this)
    +		);
    +	},
    +
    +	handleToggleControlOpen_: function(displayTitle) {
    +		var currentlyOpen = this.state.openControls;
    +
    +		if(!this.isControlOpen_(displayTitle)) {
    +			currentlyOpen.push(displayTitle);
    +		} else {
    +			goog.array.remove(currentlyOpen, displayTitle);
    +		}
    +
    +		this.setState({openControls : currentlyOpen});
    +	},
    +
    +	isControlOpen_: function(displayTitle) {
    +		return goog.array.contains(this.state.openControls, displayTitle);
    +	}
    +});
    +
    +cam.CollapsibleControl = React.createClass({
    +	displayName: 'CollapsibleControl',
    +
    +	propTypes: {
    +		control: React.PropTypes.renderable.isRequired,
    +		isOpen: React.PropTypes.bool.isRequired,
    +		onToggleOpen: React.PropTypes.func,
    +		title: React.PropTypes.string.isRequired
    +	},
    +
    +	getControl_: function() {
    +		if(!this.props.control || !this.props.isOpen) {
    +			return null;
    +		}
    +
    +		return React.DOM.div(
    +			{
    +				className: 'cam-sidebar-section'
    +			},
    +			this.props.control
    +		);
    +	},
    +
    +	render: function() {
    +		return React.DOM.div(
    +			{
    +				className: 'cam-sidebar-collapsible-section-header'
    +			},
    +			React.DOM.button(
    +				{
    +					onClick: this.handleToggleOpenClick_,
    +				},
    +				React.DOM.i(
    +					{
    +						className: React.addons.classSet({
    +							'fa': true,
    +							'fa-angle-down': this.props.isOpen,
    +							'fa-angle-right': !this.props.isOpen
    +						}),
    +						key: 'toggle-sidebar-section'
    +					}
    +				),
    +				this.props.title
    +			),
    +			this.getControl_()
    +		);
    +	},
    +
    +	handleToggleOpenClick_: function(e) {
    +		e.preventDefault();
    +		this.props.onToggleOpen(this.props.title);
    +	}
    +});
    diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sigdebug.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sigdebug.js
    new file mode 100644
    index 00000000..ce47b1ed
    --- /dev/null
    +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sigdebug.js
    @@ -0,0 +1,128 @@
    +/*
    +Copyright 2011 Google Inc.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +goog.provide('cam.DebugPage');
    +
    +goog.require('goog.dom');
    +goog.require('goog.events.EventType');
    +goog.require('goog.ui.Component');
    +
    +goog.require('cam.ServerConnection');
    +
    +// TODO(mpl): add button on index page (toolbar?) to come here.
    +// @param {cam.ServerType.DiscoveryDocument} config Global config of the current server this page is being rendered for.
    +// @param {goog.dom.DomHelper=} opt_domHelper DOM helper to use.
    +cam.DebugPage = function(config, opt_domHelper) {
    +	goog.base(this, opt_domHelper);
    +
    +	this.config_ = config;
    +	this.sigdisco_ = null;
    +	this.connection_ = new cam.ServerConnection(config);
    +
    +};
    +goog.inherits(cam.DebugPage, goog.ui.Component);
    +
    +cam.DebugPage.prototype.enterDocument = function() {
    +	cam.DebugPage.superClass_.enterDocument.call(this);
    +
    +	// set up listeners
    +	goog.events.listen(goog.dom.getElement('discobtn'),
    +		goog.events.EventType.CLICK,
    +		this.discoRoot_,
    +		false, this);
    +	goog.events.listen(goog.dom.getElement('sigdiscobtn'),
    +		goog.events.EventType.CLICK,
    +		this.discoJsonSignRoot_,
    +		false, this);
    +	goog.events.listen(goog.dom.getElement('addkeyref'),
    +		goog.events.EventType.CLICK,
    +		this.addKeyRef_,
    +		false, this);
    +	goog.events.listen(goog.dom.getElement('sign'),
    +		goog.events.EventType.CLICK,
    +		this.doSign_,
    +		false, this);
    +	goog.events.listen(goog.dom.getElement('verify'),
    +		goog.events.EventType.CLICK,
    +		this.doVerify_,
    +		false, this);
    +};
    +
    +cam.DebugPage.prototype.exitDocument = function() {
    +	cam.DebugPage.superClass_.exitDocument.call(this);
    +};
    +
    +cam.DebugPage.prototype.discoRoot_ = function(e) {
    +	var disco = "
    " + JSON.stringify(this.config_, null, 2) + "
    "; + goog.dom.getElement("discores").innerHTML = disco; +}; + +cam.DebugPage.prototype.discoJsonSignRoot_ = function() { + this.connection_.discoSignRoot( + goog.bind(function(sigdisco) { + this.sigdisco_ = sigdisco; + var disco = "
    " + JSON.stringify(sigdisco, null, 2) + "
    "; + goog.dom.getElement("sigdiscores").innerHTML = disco; + }, this) + ) +}; + +cam.DebugPage.prototype.addKeyRef_ = function() { + if (!this.sigdisco_) { + alert("must do jsonsign discovery first"); + return; + } + var clearta = goog.dom.getElement("clearjson"); + var j; + try { + j = JSON.parse(clearta.value); + } catch (x) { + alert(x); + return + } + j.camliSigner = this.sigdisco_.publicKeyBlobRef; + clearta.value = JSON.stringify(j, null, 2); +} + +cam.DebugPage.prototype.doSign_ = function() { + // We actually do not need sigdisco since sign_ will pull all the needed info from the config_ instead. But I'm leaving the check as the debug check is also a sort of demo. + if (!this.sigdisco_) { + alert("must do jsonsign discovery first"); + return; + } + var clearta = goog.dom.getElement("clearjson"); + var clearObj = JSON.parse(clearta.value); + this.connection_.sign_(clearObj, + function(response) { + goog.dom.getElement("signedjson").value = response; + } + ) +} + +cam.DebugPage.prototype.doVerify_ = function() { + // We actually do not need sigdisco since sign_ will pull all the needed info from the config_ instead. But I'm leaving the check as the debug check is also a sort of demo. + if (!this.sigdisco_) { + alert("must do jsonsign discovery first"); + return; + } + var signedta = goog.dom.getElement("signedjson"); + this.connection_.verify_(signedta.value, + function(response) { + var text = "
    " + response + "
    "; + goog.dom.getElement("verifyinfo").innerHTML = text; + } + ) +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/spinner.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/spinner.css new file mode 100644 index 00000000..3ab94aa4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/spinner.css @@ -0,0 +1,30 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.cam-spinner { + position: relative; + background-size: 100%; + overflow: hidden; +} + +.cam-spinner>div { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: 100%; +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/spinner.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/spinner.js new file mode 100644 index 00000000..63a1b4f0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/spinner.js @@ -0,0 +1,90 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.Spinner'); + +goog.require('goog.dom'); +goog.require('goog.events.EventHandler'); +goog.require('goog.style'); +goog.require('goog.math.Coordinate'); +goog.require('goog.math.Size'); +goog.require('goog.ui.Control'); + +goog.require('cam.AnimationLoop'); +goog.require('cam.style'); + +// An indeterminite progress meter using the safe icon. +// @param {goog.dom.DomHelper} domHelper +cam.Spinner = function(domHelper) { + goog.base(this, null, this.dom_); + + this.dom_ = domHelper; + this.eh_ = new goog.events.EventHandler(this); + this.animationLoop_ = new cam.AnimationLoop(this.dom_.getWindow()); + this.currentRotation_ = 0; +}; + +goog.inherits(cam.Spinner, goog.ui.Control); + +cam.Spinner.prototype.backgroundImage = "safe-no-wheel.svg"; + +cam.Spinner.prototype.foregroundImage = "safe-wheel.svg"; + +cam.Spinner.prototype.degreesPerSecond = 500; + +// The origin the safe wheel rotates around, expressed as a fraction of the image's width and height. +cam.Spinner.prototype.wheelRotationOrigin_ = new goog.math.Coordinate(0.37, 0.505); + +cam.Spinner.prototype.createDom = function() { + this.background_ = this.dom_.createDom('div', 'cam-spinner', this.dom_.createDom('div')); + this.foreground_ = this.background_.firstChild; + + cam.style.setURLStyle(this.background_, 'background-image', this.backgroundImage); + cam.style.setURLStyle(this.foreground_, 'background-image', this.foregroundImage); + + // TODO(aa): This will need to be configurable. Not sure how makes sense yet. + var size = new goog.math.Size(75, 75); + goog.style.setSize(this.background_, size); + + // We should be able to set the origin as a percentage directly, but the browsers end up rounding differently, and we get less off-center spinning on the whole if we set this using pixels. + var origin = new goog.math.Coordinate(size.width, size.height); + cam.style.setTransformOrigin( + this.foreground_, + origin.scale(this.wheelRotationOrigin_.x, this.wheelRotationOrigin_.y)); + + this.eh_.listen(this.animationLoop_, cam.AnimationLoop.FRAME_EVENT_TYPE, this.updateRotation_); + + this.decorateInternal(this.background_); +}; + +cam.Spinner.prototype.isRunning = function() { + return this.animationLoop_.isRunning(); +}; + +cam.Spinner.prototype.start = function() { + this.animationLoop_.start(); +}; + +cam.Spinner.prototype.stop = function() { + this.animationLoop_.stop(); +}; + +cam.Spinner.prototype.updateRotation_ = function(e) { + rotation = e.delay / 1000 * this.degreesPerSecond; + this.currentRotation_ += rotation; + this.currentRotation_ %= 360; + cam.style.setRotation(this.foreground_, this.currentRotation_); +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/spinner_test.html b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/spinner_test.html new file mode 100644 index 00000000..c744ef10 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/spinner_test.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sprited_animation.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sprited_animation.js new file mode 100644 index 00000000..f95370d7 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sprited_animation.js @@ -0,0 +1,70 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.SpritedAnimation'); + +goog.require('cam.SpritedImage'); +goog.require('cam.object'); + +cam.SpritedAnimation = React.createClass({ + propTypes: { + className: React.PropTypes.string, + loopDelay: React.PropTypes.number, + interval: React.PropTypes.number, + numFrames: React.PropTypes.number.isRequired, + sheetWidth: React.PropTypes.number.isRequired, + spriteHeight: React.PropTypes.number.isRequired, + spriteWidth: React.PropTypes.number.isRequired, + src: React.PropTypes.string.isRequired, + startFrame: React.PropTypes.number, + style: React.PropTypes.object, + }, + + getInitialState: function() { + return { + index: this.props.startFrame || 0, + } + }, + + componentDidMount: function(root) { + this.scheduleFrame_(); + }, + + scheduleFrame_: function() { + var interval = function() { + if (goog.isDef(this.props.loopDelay) && this.state.index == (this.props.numFrames - 1)) { + return this.props.loopDelay; + } + if (goog.isDef(this.props.interval)) { + return this.props.interval; + } + return 30; + }; + this.timerId_ = window.setTimeout(function() { + this.setState({ + index: ++this.state.index % this.props.numFrames + }, this.scheduleFrame_); + }.bind(this), interval.call(this)); + }, + + componentWillUnmount: function() { + window.clearInterval(this.timerId_); + }, + + render: function() { + return cam.SpritedImage(cam.object.extend(this.props, {index: this.state.index})); + } +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sprited_image.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sprited_image.js new file mode 100644 index 00000000..9e7cf703 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/sprited_image.js @@ -0,0 +1,56 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.SpritedImage'); + +goog.require('goog.object'); +goog.require('goog.string'); + +goog.require('cam.object'); +goog.require('cam.reactUtil'); + +cam.SpritedImage = React.createClass({ + propTypes: { + className: React.PropTypes.string, + index: React.PropTypes.number.isRequired, + sheetWidth: React.PropTypes.number.isRequired, + spriteHeight: React.PropTypes.number.isRequired, + spriteWidth: React.PropTypes.number.isRequired, + src: React.PropTypes.string.isRequired, + style: React.PropTypes.object, + }, + + render: function() { + return ( + React.DOM.div({ + className: this.props.className, + style: cam.object.extend(this.props.style, { + height: this.props.spriteHeight, + overflow: 'hidden', + width: this.props.spriteWidth, + }) + }, + React.DOM.img({src: this.props.src, style: this.getImgStyle_()}))); + }, + + getImgStyle_: function() { + var x = this.props.index % this.props.sheetWidth; + var y = Math.floor(this.props.index / this.props.sheetWidth); + return cam.reactUtil.getVendorProps({ + transform: goog.string.subs('translate3d(%spx, %spx, 0)', -x * this.props.spriteWidth, -y * this.props.spriteHeight), + }); + } +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/style.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/style.js new file mode 100644 index 00000000..20064067 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/style.js @@ -0,0 +1,87 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.style'); +goog.provide('cam.style.ClassNameBuilder'); + +goog.require('goog.math.Coordinate'); +goog.require('goog.string'); +goog.require('goog.style'); + +// Returns |url| wrapped in url() so that it can be used as a CSS property value. +// @param {string} url +// @returns {string} +cam.style.getURLValue = function(url) { + return goog.string.subs('url(%s)', url); +}; + +// Sets a style property to a URL value. +// @param {Element} elm +// @param {string} dashedCSSProperty The CSS property to set, formatted with dashes, in the CSS style, not camelCase. +// @param {string} url +cam.style.setURLStyle = function(elm, dashedCSSProperty, url) { + goog.style.setStyle(elm, dashedCSSProperty, cam.style.getURLValue(url)); +}; + +// @param {Element} elm +// @param {goog.math.Coordinate} origin +// @param {string=} opt_unit The CSS units the origin is in. If unspecified, defaults to pixels. +cam.style.setTransformOrigin = function(elm, origin, opt_unit) { + var unit = opt_unit || 'px'; + goog.style.setStyle(elm, 'transform-origin', goog.string.subs('%s%s %s%s', origin.x, unit, origin.y, unit)); +}; + +// Note that this currently clears any previous CSS transform. Currently we only +// needs to support rotate(). +// @param {Element} elm +// @param {number} degrees +cam.style.setRotation = function(elm, degrees) { + goog.style.setStyle(elm, 'transform', goog.string.subs('rotate(%sdeg)', degrees)); +}; + + +// Utility to build a space-separated className property. +cam.style.ClassNameBuilder = function() { + this.names_ = {}; +}; + +// Maybe add the specified class. +// @param {?string} name Class to add. If falsey, not added. +// @param {boolean=} yes Whether to add. If unspecified or falsey, not added. +// @return {cam.style.ClassNameBuilder} +cam.style.ClassNameBuilder.prototype.add = function(name, yes) { + if (!name) { + return this; + } + + if (!goog.isDef(yes)) { + yes = true; + } + + if (yes) { + this.names_[name] = true; + } else { + delete this.names_[name]; + } + + return this; +}; + +// Return the space-separated className. +// @return {string} +cam.style.ClassNameBuilder.prototype.build = function() { + return goog.object.getKeys(this.names_).join(' '); +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/tags_control.css b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/tags_control.css new file mode 100644 index 00000000..9f28aca5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/tags_control.css @@ -0,0 +1,114 @@ +/* +Copyright 2014 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +@import (less) "prefix-free.css"; + +@color-button-border: #39463C; +@color-button-partial: #eee; +@color-button-full: #81A18A; +@color-button-hover: #576D5D; + +@control-width: 300px; + +.cam-addtagsinput-form { + margin: 5px 0; + + > input { + width: 100%; + padding: 0; + } + + > div { + margin: 3px; + color: red; + font-size: 12px; + font-style: italic; + } +} + +.cam-edittagscontrol-main { + max-height: 300px; + overflow-y: auto; +} + +.cam-edittagscontrol-button-group { + margin: 3px; + display: inline-block; + + > button { + font-size: 13px; + border: 1px solid @color-button-border; + } + + > button:active:enabled { + position: relative; + top: 1px; + } +} + +.cam-edittagscontrol-button-all-tagged { + background-color: @color-button-full; + color: #fff; + padding: 2px 7px 2px 10px; + margin-right: 0px; + margin-left: 0px; + + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + + text-shadow: 0px 1px 0px #2f6627; +} + +.cam-edittagscontrol-button-some-tagged { + background-color: @color-button-partial; + padding: 2px 10px; + + margin-right: 0px; + margin-left: 0px; + + cursor: pointer; + + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + + &:hover:enabled { + background-color: @color-button-full; + color: #ffffff; + } +} + +.cam-edittagscontrol-button-remove-tag { + background-color: @color-button-full; + color: #fff; + margin-left: -1px; + margin-right: 0px; + padding: 2px 10px 2px 7px; + + cursor: pointer; + + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; + + &:hover:enabled { + background-color: @color-button-hover; + } +} diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/tags_control.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/tags_control.js new file mode 100644 index 00000000..e3321c1a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/tags_control.js @@ -0,0 +1,340 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// ENHANCEMENTS: +// should the control have a hot-key to launch? +// should the control be draggable within the window? Is there a better strategy for not hiding permanodes you want to select (@see: http://jsfiddle.net/Af9Jt/2/) +// discuss: create type-ahead list of existing tags for add tag input (this will require mods to the supporting service - likely creation of attribute index?) + +goog.provide('cam.TagsControl'); + +goog.require('goog.array'); +goog.require('goog.labs.Promise'); +goog.require('goog.object'); +goog.require('goog.Uri'); + +goog.require('cam.permanodeUtils'); +goog.require('cam.reactUtil'); +goog.require('cam.ServerConnection'); + +cam.TagsControl = React.createClass({ + displayName: 'TagsControl', + + propTypes: { + selectedItems: React.PropTypes.object.isRequired, + searchSession: React.PropTypes.shape({getMeta:React.PropTypes.func.isRequired}), + serverConnection: React.PropTypes.instanceOf(cam.ServerConnection).isRequired, + }, + + doesBlobHaveTag: function(blobref, tag) { + var blobmeta = this.props.searchSession.getMeta(blobref); + + if (blobmeta && blobmeta.camliType == 'permanode') { + var tags = blobmeta.permanode.attr.tag; + + if (tags) { + return goog.array.contains(tags, tag); + } + } + + return false; + }, + + executePromises: function(componentId, promises, callbackSuccess) { + goog.labs.Promise.all(promises).thenCatch(function(e) { + console.error('%s: error executing promises: %s', componentId, e); + alert('The system encountered an error updating tags: ' + e); + }).then(function(results) { + if (results) { + console.log('%s: successfully completed %d of %d promises', componentId, results.length, promises.length); + + if (callbackSuccess) { + callbackSuccess(); + } + } else { + // TODO: I'm not sure this is ever reached, but keep for now and monitor + console.error('%s: results object is empty', componentId); + } + }).then(function() { + console.log('%s: operation complete', componentId); + }); + }, + + render: function() { + var props = this.props; + var blobrefs = goog.object.getKeys(props.selectedItems); + var blobs = blobrefs.map(function(blobref) { + return props.searchSession.getMeta(blobref); + }); + + return React.DOM.div( + { + className: 'cam-tagscontrol-main' + }, + React.DOM.div( + { + className: 'cam-tagscontrol-header' + } + ), + cam.AddTagsInput( + { + blobrefs: blobrefs, + serverConnection: this.props.serverConnection, + doesBlobHaveTag: this.doesBlobHaveTag, + executePromises: this.executePromises + } + ), + cam.EditTagsControl( + { + blobs: blobs, + blobrefs: blobrefs, + serverConnection: this.props.serverConnection, + doesBlobHaveTag: this.doesBlobHaveTag, + executePromises: this.executePromises + } + ) + ); + } +}); + +cam.AddTagsInput = React.createClass({ + displayName: 'AddTagsInput', + + PLACEHOLDER: 'Add tag(s) [val1,val2,...]', + + propTypes: { + blobrefs: React.PropTypes.array.isRequired, + serverConnection: React.PropTypes.instanceOf(cam.ServerConnection).isRequired, + doesBlobHaveTag: React.PropTypes.func.isRequired, + executePromises: React.PropTypes.func.isRequired + }, + + getInitialState: function() { + return { + inputValue: null, + statusMessage: null + }; + }, + + componentDidMount: function() { + this.getInputNode().focus(); + }, + + getInputNode: function() { + return this.refs['inputField'].getDOMNode(); + }, + + handleOnSubmit_: function(e) { + e.preventDefault(); + + var inputVal = this.getInputNode().value; + if (goog.string.isEmpty(inputVal)) { + this.setState({statusMessage: 'Please provide at least one tag value'}); + } else { + var tags = inputVal.split(',').map(function(s) { return s.trim(); }); + if (tags.some(function(t) { return !t })) { + this.setState({statusMessage: 'At least one invalid value was supplied'}); + } else { + this.executeAddTags_(tags); + } + } + }, + + handleOnChange_: function(e) { + this.setState({statusMessage: null}); + this.setState({inputValue: e.target.value}); + }, + + handleOnFocus_: function(e) { + this.setState({statusMessage: null}); + }, + + handleAddSuccess_: function() { + this.setState({inputValue: ''}); + }, + + executeAddTags_: function(tags) { + var blobrefs = this.props.blobrefs; + var doesBlobHaveTag = this.props.doesBlobHaveTag; + var sc = this.props.serverConnection; + var promises = []; + + blobrefs.forEach(function(pm) { + tags.forEach(function(tag) { + if (!doesBlobHaveTag(pm, tag)) { + promises.push(new goog.labs.Promise(sc.newAddAttributeClaim.bind(sc, pm, 'tag', tag))); + } + }); + }); + + this.props.executePromises('AddTag', promises, this.handleAddSuccess_); + }, + + getStatusMessageItem_: function() { + if (!this.state.statusMessage) { + return null; + } + + return React.DOM.div({}, this.state.statusMessage); + }, + + render: function() { + return React.DOM.form( + { + className: 'cam-addtagsinput-form', + onSubmit: this.handleOnSubmit_, + }, + React.DOM.input( + { + onChange: this.handleOnChange_, + onFocus: this.handleOnFocus_, + placeholder: this.PLACEHOLDER, + ref: 'inputField', + value: this.state.inputValue, + } + ), + this.getStatusMessageItem_() + ); + } +}); + +cam.EditTagsControl = React.createClass({ + displayName: 'EditTagsControl', + + propTypes: { + blobs: React.PropTypes.array.isRequired, + blobrefs: React.PropTypes.array.isRequired, + serverConnection: React.PropTypes.instanceOf(cam.ServerConnection).isRequired, + doesBlobHaveTag: React.PropTypes.func.isRequired, + executePromises: React.PropTypes.func.isRequired + }, + + handleApplyTag_: function(e) { + e.preventDefault(); + var tag = e.target.value; + this.executeApplyTag_(tag); + }, + + handleRemoveTag_: function(e) { + e.preventDefault(); + var tag = e.target.value; + this.executeRemoveTag_(tag); + }, + + executeApplyTag_: function(tag) { + var blobrefs = this.props.blobrefs; + var doesBlobHaveTag = this.props.doesBlobHaveTag; + var sc = this.props.serverConnection; + var promises = []; + + blobrefs.forEach(function(pm) { + if (!doesBlobHaveTag(pm, tag)) { + promises.push(new goog.labs.Promise(sc.newAddAttributeClaim.bind(sc, pm, 'tag', tag))); + } + }); + + this.props.executePromises('ApplyTag', promises); + }, + + executeRemoveTag_: function(tag) { + var blobrefs = this.props.blobrefs; + var doesBlobHaveTag = this.props.doesBlobHaveTag; + var sc = this.props.serverConnection; + var promises = []; + + blobrefs.forEach(function(pm) { + if (doesBlobHaveTag(pm, tag)) { + promises.push(new goog.labs.Promise(sc.newDelAttributeClaim.bind(sc, pm, 'tag', tag))); + } + }); + + this.props.executePromises('DeleteTag', promises); + }, + + getApplyTagButton_: function(numBlobs, tag, allTags) { + var totalHits = allTags[tag]; + if (totalHits == numBlobs) { + return React.DOM.button( + { + key: 'apply-tag-' + tag, + className: 'cam-edittagscontrol-button-all-tagged', + disabled: true + }, + tag + ); + } + + return React.DOM.button( + { + key: 'apply-tag-' + tag, + className: 'cam-edittagscontrol-button-some-tagged', + title: 'Apply tag to all selected items', + onClick: this.handleApplyTag_, + value: tag + }, + tag + ); + }, + + render: function() { + var tagControls = []; + var allTags = {}; + + var numBlobs = this.props.blobs.length; + + this.props.blobs.forEach(function(blobmeta) { + if (blobmeta && blobmeta.camliType == 'permanode') { + var tags = blobmeta.permanode.attr.tag; + if (tags) { + tags.forEach(function(tag) { + if (!allTags.hasOwnProperty(tag)) { + allTags[tag] = 0; + } + ++allTags[tag]; + }); + } + } else { + console.log('EditTagsControl: blob not a permanode!'); + } + }); + + goog.object.getKeys(allTags).sort().forEach(function(tag) { + tagControls.push(React.DOM.div( + { + className: 'cam-edittagscontrol-button-group' + }, + this.getApplyTagButton_(numBlobs, tag, allTags), + React.DOM.button( + { + key:'del-tag-' + tag, + title: 'Remove tag from all selected items', + className: 'cam-edittagscontrol-button-remove-tag', + onClick: this.handleRemoveTag_, + value: tag + }, + 'x' + ) + )); + }.bind(this)); + + return React.DOM.div( + { + className: 'cam-edittagscontrol-main', + }, + tagControls + ); + } +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/target.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/target.svg new file mode 100644 index 00000000..1fe9f0a1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/target.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/thumber.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/thumber.js new file mode 100644 index 00000000..dc683961 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/thumber.js @@ -0,0 +1,65 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.Thumber'); + +goog.require('goog.string'); + +// Utility to efficiently choose thumbnail URLs for use by the UI. +// +// Sizes are bucketized for cache friendliness. Also, the last requested size is remembered, and if the requested size is smaller than the last size, then we continue using the old URL. +cam.Thumber = function(pathname, opt_aspect) { + this.pathname_ = pathname; + this.lastHeight_ = 0; + this.aspect_ = opt_aspect || 1; +}; + +// We originally just used powers of 2, but we need sizes between 200 and 400 all the time in the UI and it seemed wasteful to jump to 512. Having an explicit list will make it easier to tune the buckets more in the future if necessary. +cam.Thumber.SIZES = [64, 128, 256, 375, 500, 750, 1000, 1500, 2000]; + +cam.Thumber.fromImageMeta = function(imageMeta) { + return new cam.Thumber(goog.string.subs('thumbnail/%s/%s', imageMeta.blobRef, (imageMeta.file && imageMeta.file.fileName) || imageMeta.blobRef + '.jpg'), + imageMeta.image.width / imageMeta.image.height); +}; + +// @param {number|goog.math.Size} minSize The minimum size of the required thumbnail. If this is a number, it is the minimum height. If it is goog.math.Size, then it is the min size of both dimensions. +cam.Thumber.prototype.getSrc = function(minSize) { + var minWidth, minHeight; + if (typeof minSize == 'number') { + minHeight = minSize; + minWidth = 0; + } else { + minWidth = minSize.width; + minHeight = minSize.height; + } + + this.lastHeight_ = this.getSizeToRequest_(minWidth, minHeight); + return goog.string.subs('%s?mh=%s&tv=%s', this.pathname_, this.lastHeight_, goog.global.CAMLISTORE_CONFIG ? goog.global.CAMLISTORE_CONFIG.thumbVersion : 1); +}; + +cam.Thumber.prototype.getSizeToRequest_ = function(minWidth, minHeight) { + if (this.lastHeight_ >= minHeight && ((this.lastHeight_ * this.aspect_) >= minWidth)) { + return this.lastHeight_; + } + var newHeight; + for (var i = 0; i < cam.Thumber.SIZES.length; i++) { + newHeight = cam.Thumber.SIZES[i]; + if (newHeight >= minHeight && ((newHeight * this.aspect_) >= minWidth)) { + break; + } + } + return newHeight; +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/thumber_test.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/thumber_test.js new file mode 100644 index 00000000..099df549 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/thumber_test.js @@ -0,0 +1,62 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var assert = require('assert'); + +goog.require('goog.math.Size'); +goog.require('goog.Uri'); + +goog.require('cam.Thumber'); + + +describe('cam.Thumber', function() { + describe('#getSrc', function() { + it('it should bucketize properly', function() { + var thumber = new cam.Thumber('foo.png'); + assert.equal(128, goog.Uri.parse(thumber.getSrc(100)).getParameterValue('mh')); + assert.equal(128, goog.Uri.parse(thumber.getSrc(128)).getParameterValue('mh')); + assert.equal(256, goog.Uri.parse(thumber.getSrc(129)).getParameterValue('mh')); + assert.equal(256, goog.Uri.parse(thumber.getSrc(256)).getParameterValue('mh')); + }); + + it('should max out at a sane size', function() { + var thumber = new cam.Thumber('foo.png'); + var maxSize = cam.Thumber.SIZES[cam.Thumber.SIZES.length - 1]; + assert.equal(maxSize, goog.Uri.parse(thumber.getSrc(1999)).getParameterValue('mh')); + assert.equal(maxSize, goog.Uri.parse(thumber.getSrc(2000)).getParameterValue('mh')); + assert.equal(maxSize, goog.Uri.parse(thumber.getSrc(2001)).getParameterValue('mh')); + }); + + it('should only increase in size, never decrease', function() { + var thumber = new cam.Thumber('foo.png'); + assert.equal(64, goog.Uri.parse(thumber.getSrc(50)).getParameterValue('mh')); + assert.equal(64, goog.Uri.parse(thumber.getSrc(64)).getParameterValue('mh')); + assert.equal(128, goog.Uri.parse(thumber.getSrc(65)).getParameterValue('mh')); + assert.equal(128, goog.Uri.parse(thumber.getSrc(50)).getParameterValue('mh')); + assert.equal(256, goog.Uri.parse(thumber.getSrc(129)).getParameterValue('mh')); + }); + + it('should handle Size objects properly', function() { + var thumber = new cam.Thumber('foo.png', 2); + assert.equal(128, goog.Uri.parse(thumber.getSrc(new goog.math.Size(100, 100))).getParameterValue('mh')); + thumber = new cam.Thumber('foo.png', 0.5); + assert.equal(256, goog.Uri.parse(thumber.getSrc(new goog.math.Size(100, 100))).getParameterValue('mh')); + + assert.equal(256, goog.Uri.parse(thumber.getSrc(new goog.math.Size(128, 100))).getParameterValue('mh')); + assert.equal(375, goog.Uri.parse(thumber.getSrc(new goog.math.Size(129, 100))).getParameterValue('mh')); + }); + }); +}); diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/trash.svg b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/trash.svg new file mode 100644 index 00000000..b7d534c3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/trash.svg @@ -0,0 +1,147 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/twitter-logo.png b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/twitter-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1b1562d432cf21cd9fb6045ff22efd6963608899 GIT binary patch literal 3462 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 o`FiZ> + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/worker_message_router.js b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/worker_message_router.js new file mode 100644 index 00000000..f5e06382 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/worker_message_router.js @@ -0,0 +1,101 @@ +/* +Copyright 2014 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('cam.WorkerMessageRouter'); + +goog.require('goog.string'); + +// Convenience for sending request/response style messages to and from workers. +// @param {!Worker} worker The DOM worker to wrap. +// @constructor +cam.WorkerMessageRouter = function(worker) { + this.worker_ = worker; + this.nextMessageId_ = 1; + + // name->handler - See registerHandler() + // @type Object. + this.handlers_ = {}; + + // messageid->callback - See sendMessage() + // @type Object. + this.pendingMessages_ = {}; + + this.worker_.addEventListener('message', this.handleMessage_.bind(this)); +}; + +// Send a message over the worker, optionally expecting a response. +// @param {!string} name The name of the message to send. +// @param {!*} msg The message content +// @param {?function(*)} opt_callback The function to receive the response. +cam.WorkerMessageRouter.prototype.sendMessage = function(name, msg, opt_callback) { + var messageId = 0; + if (opt_callback) { + messageId = this.nextMessageId_++; + this.pendingMessages_[messageId] = opt_callback; + } + this.worker_.postMessage({ + messageId: messageId, + name: name, + message: msg + }); +}; + +// Registers a function to handle a particular named message type. +// @param {!string} name The name of the message type to handle. +// @param {!function(*, function(*))} handler The function to call to return the reply to the client. +cam.WorkerMessageRouter.prototype.registerHandler = function(name, handler) { + this.handlers_[name] = handler; +}; + +cam.WorkerMessageRouter.prototype.handleMessage_ = function(e) { + if (!goog.isObject(e.data) || !goog.isDef(e.data.messageId)) { + return; + } + + if (goog.isDef(e.data.name)) { + this.handleRequest_(e.data); + } else { + this.handleReply_(e.data); + } +}; + +cam.WorkerMessageRouter.prototype.handleRequest_ = function(request) { + var handler = this.handlers_[request.name]; + if (!handler) { + throw new Error(goog.string.subs('No registered handler with name: %s', request.name)); + } + + var sendReply = function(reply) { + if (!request.messageId) { + return; + } + this.worker_.postMessage({ + messageId: request.messageId, + message: reply + }); + }.bind(this); + + handler(request.message, sendReply); +}; + +cam.WorkerMessageRouter.prototype.handleReply_ = function(reply) { + var callback = this.pendingMessages_[reply.messageId]; + if (!callback) { + throw new Error('Could not find callback for pending message: %s', reply.messageId); + } + delete this.pendingMessages_[reply.messageId]; + callback(reply.message); +}; diff --git a/vendor/github.com/camlistore/camlistore/server/camlistored/ui/wsdebug.html b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/wsdebug.html new file mode 100644 index 00000000..8cf98359 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/camlistored/ui/wsdebug.html @@ -0,0 +1,74 @@ + + + + + + + + + +

    websocket debug

    + +
    +
    + + + diff --git a/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/README b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/README new file mode 100644 index 00000000..4717393c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/README @@ -0,0 +1,5 @@ +This was an early App Engine blobserver-only implementation in Python. It had no +index, no search, no UI, no crypto, etc. + +The blob server is this directory is no longer actively developed, superceded +by the main Go implementation, which can now run on App Engine. diff --git a/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/app.yaml b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/app.yaml new file mode 100644 index 00000000..570f6dd1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/app.yaml @@ -0,0 +1,27 @@ +application: camlistore +version: 1 +api_version: 1 +runtime: python + +handlers: +- url: /remote_api + script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py + login: admin + +# Upload completion URL must not be accessible by any users. Only by +# going through Blobstore API upload URL. +- url: /upload_complete + login: admin + script: main.py + +- url: /js + static_dir: ../../clients/js + +- url: /static + static_dir: static + +# off for now: +# secure: always + +- url: .* + script: main.py diff --git a/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/config.py b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/config.py new file mode 100644 index 00000000..7a5a5b6c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/config.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python + +# TODO(bslatkin): Do something with this password. +# Used for Basic Auth over HTTPS. +PASSWORD = 'foo' + +MAX_UPLOAD_SIZE = 2 * 1024 * 1024 diff --git a/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/index.yaml b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/index.yaml new file mode 100644 index 00000000..de27a470 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/index.yaml @@ -0,0 +1,9 @@ +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. diff --git a/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/main.py b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/main.py new file mode 100644 index 00000000..788e1e29 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/main.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python +# +# Camlistore blob server for App Engine. +# +# Derived from Brad's Brackup-gae utility: +# http://github.com/bradfitz/brackup-gae-server +# +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Upload server for camlistore. + +To test: + +# Stat -- 200 response +curl -v \ + -d camliversion=1 \ + http://localhost:8080/camli/stat + +# Upload -- 200 response +curl -v -L \ + -F sha1-126249fd8c18cbb5312a5705746a2af87fba9538=@./test_data.txt \ + + +# Put with bad blob_ref parameter -- 400 response +curl -v -L \ + -F sha1-22a7fdd575f4c3e7caa3a55cc83db8b8a6714f0f=@./test_data.txt \ + + +# Get present -- the blob +curl -v http://localhost:8080/camli/\ +sha1-126249fd8c18cbb5312a5705746a2af87fba9538 + +# Get missing -- 404 +curl -v http://localhost:8080/camli/\ +sha1-22a7fdd575f4c3e7caa3a55cc83db8b8a6714f0f + +# Check present -- 200 with only headers +curl -I http://localhost:8080/camli/\ +sha1-126249fd8c18cbb5312a5705746a2af87fba9538 + +# Check missing -- 404 with empty list response +curl -I http://localhost:8080/camli/\ +sha1-22a7fdd575f4c3e7caa3a55cc83db8b8a6714f0f + +# List -- 200 with list of blobs (just one) +curl -v http://localhost:8080/camli/enumerate-blobs&limit=1 + +# List offset -- 200 with list of no blobs +curl -v http://localhost:8080/camli/enumerate-blobs?after=\ +sha1-126249fd8c18cbb5312a5705746a2af87fba9538 + +""" + +import cgi +import hashlib +import logging +import urllib +import wsgiref.handlers + +from google.appengine.ext import blobstore +from google.appengine.ext import db +from google.appengine.ext import webapp +from google.appengine.ext.webapp import blobstore_handlers + +import config + + +class Blob(db.Model): + """Some content-addressable blob. + + The key is the algorithm, dash, and the lowercase hex digest: + "sha1-f1d2d2f924e986ac86fdf7b36c94bcdf32beec15" + """ + + # The actual bytes. + blob = blobstore.BlobReferenceProperty(indexed=False) + + # Size. (already in the blobinfo, but denormalized for speed) + size = db.IntegerProperty(indexed=False) + + +class HelloHandler(webapp.RequestHandler): + """Present ourselves to the world.""" + + def get(self): + self.response.out.write('Hello! This is an AppEngine Camlistore ' + 'blob server.

    ') + self.response.out.write('js frontend') + + +class ListHandler(webapp.RequestHandler): + """Return chunks that the server has.""" + + def get(self): + after_blob_ref = self.request.get('after') + limit = max(1, min(1000, int(self.request.get('limit') or 1000))) + query = Blob.all().order('__key__') + if after_blob_ref: + query.filter('__key__ >', db.Key.from_path(Blob.kind(), after_blob_ref)) + blob_ref_list = query.fetch(limit) + + self.response.headers['Content-Type'] = 'text/javascript' + out = [ + '{\n' + ' "blobs": [' + ] + if blob_ref_list: + out.extend([ + '\n ', + ',\n '.join( + '{"blobRef": "%s", "size": %d}' % + (b.key().name(), b.size) for b in blob_ref_list), + '\n ', + ]) + if blob_ref_list and len(blob_ref_list) == limit: + out.append( + '],' + '\n "continueAfter": "%s"\n' + '}' % blob_ref_list[-1].key().name()) + else: + out.append( + ']\n' + '}' + ) + self.response.out.write(''.join(out)) + + +class GetHandler(blobstore_handlers.BlobstoreDownloadHandler): + """Gets a blob with the given ref.""" + + def head(self, blob_ref): + self.get(blob_ref) + + def get(self, blob_ref): + blob = Blob.get_by_key_name(blob_ref) + if not blob: + self.error(404) + return + self.send_blob(blob.blob, 'application/octet-stream') + + +class StatHandler(webapp.RequestHandler): + """Handler to return a URL for a script to get an upload URL.""" + + def stat_key(self): + return "stat" + + def get(self): + self.handle() + + def post(self): + self.handle() + + def handle(self): + if self.request.get('camliversion') != '1': + self.response.headers['Content-Type'] = 'text/plain' + self.response.out.write('Bad parameter: "camliversion"') + self.response.set_status(400) + return + + blob_ref_list = [] + for key, value in self.request.params.items(): + if not key.startswith('blob'): + continue + try: + int(key[len('blob'):]) + except ValueError: + logging.exception('Bad parameter: %s', key) + self.response.headers['Content-Type'] = 'text/plain' + self.response.out.write('Bad parameter: "%s"' % key) + self.response.set_status(400) + return + else: + blob_ref_list.append(value) + + key_name = self.stat_key() + + self.response.headers['Content-Type'] = 'text/javascript' + out = [ + '{\n' + ' "maxUploadSize": %d,\n' + ' "uploadUrl": "%s",\n' + ' "uploadUrlExpirationSeconds": 600,\n' + ' "%s": [\n' + % (config.MAX_UPLOAD_SIZE, + blobstore.create_upload_url('/upload_complete'), + key_name) + ] + + already_have = db.get([ + db.Key.from_path(Blob.kind(), b) for b in blob_ref_list]) + if already_have: + out.extend([ + '\n ', + ',\n '.join( + '{"blobRef": "%s", "size": %d}' % + (b.key().name(), b.size) for b in already_have if b is not None), + '\n ', + ]) + out.append( + ']\n' + '}' + ) + self.response.out.write(''.join(out)) + + +class PostUploadHandler(StatHandler): + + def stat_key(self): + return "received" + + +class UploadHandler(blobstore_handlers.BlobstoreUploadHandler): + """Handle blobstore post, as forwarded by notification agent.""" + + def compute_blob_ref(self, hash_func, blob_key): + """Computes the blob ref for a blob stored using the given hash function. + + Args: + hash_func: The name of the hash function (sha1, md5) + blob_key: The BlobKey of the App Engine blob containing the blob's data. + + Returns: + A newly computed blob_ref for the data. + """ + hasher = hashlib.new(hash_func) + last_index = 0 + while True: + data = blobstore.fetch_data( + blob_key, last_index, last_index + blobstore.MAX_BLOB_FETCH_SIZE - 1) + if not data: + break + hasher.update(data) + last_index += len(data) + + return '%s-%s' % (hash_func, hasher.hexdigest()) + + def store_blob(self, blob_ref, blob_info, error_messages): + """Store blob information. + + Writes a Blob to the datastore for the uploaded file. + + Args: + blob_ref: The file that was uploaded. + upload_file: List of BlobInfo records representing the uploads. + error_messages: Empty list for storing error messages to report to user. + """ + if not blob_ref.startswith('sha1-'): + error_messages.append('Only sha1 supported for now.') + return + + if len(blob_ref) != (len('sha1-') + 40): + error_messages.append('Bogus blobRef.') + return + + found_blob_ref = self.compute_blob_ref('sha1', blob_info.key()) + if blob_ref != found_blob_ref: + error_messages.append('Found blob ref %s, expected %s' % + (found_blob_ref, blob_ref)) + return + + def txn(): + logging.info('Saving blob "%s" with size %d', blob_ref, blob_info.size) + blob = Blob(key_name=blob_ref, blob=blob_info.key(), size=blob_info.size) + blob.put() + db.run_in_transaction(txn) + + def post(self): + """Do upload post.""" + error_messages = [] + blob_info_dict = {} + + for key, value in self.request.params.items(): + if isinstance(value, cgi.FieldStorage): + if 'blob-key' in value.type_options: + blob_info = blobstore.parse_blob_info(value) + blob_info_dict[value.name] = blob_info + logging.info("got blob: %s" % value.name) + self.store_blob(value.name, blob_info, error_messages) + + if error_messages: + logging.error('Upload errors: %r', error_messages) + blobstore.delete(blob_info_dict.values()) + self.response.set_status(303) + # TODO: fix up this format + self.response.headers.add_header("Location", '/error?%s' % '&'.join( + 'error_message=%s' % urllib.quote(m) for m in error_messages)) + else: + query = ['/nonstandard/upload_complete?camliversion=1'] + query.extend('blob%d=%s' % (i + 1, k) + for i, k in enumerate(blob_info_dict.iterkeys())) + self.response.set_status(303) + self.response.headers.add_header("Location", str('&'.join(query))) + + +class ErrorHandler(webapp.RequestHandler): + """The blob put failed.""" + + def get(self): + self.response.headers['Content-Type'] = 'text/plain' + self.response.out.write('\n'.join(self.request.get_all('error_message'))) + self.response.set_status(400) + + +class DebugUploadForm(webapp.RequestHandler): + def get(self): + self.response.headers['Content-Type'] = 'text/html' + uploadurl = blobstore.create_upload_url('/upload_complete') + self.response.out.write('

    ' % uploadurl) + self.response.out.write('') + self.response.out.write('
    ') + + +APP = webapp.WSGIApplication( + [ + ('/', HelloHandler), + ('/debug/upform', DebugUploadForm), + ('/camli/enumerate-blobs', ListHandler), + ('/camli/stat', StatHandler), + ('/camli/([^/]+)', GetHandler), + ('/nonstandard/upload_complete', PostUploadHandler), + ('/upload_complete', UploadHandler), # Admin only. + ('/error', ErrorHandler), + ], + debug=True) + + +def main(): + wsgiref.handlers.CGIHandler().run(APP) + + +if __name__ == '__main__': + main() diff --git a/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/static/style.css b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/static/style.css new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/static/style.css @@ -0,0 +1 @@ +// TODO diff --git a/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/test_data.txt b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/test_data.txt new file mode 100644 index 00000000..a26826a5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/gae-py-blobserver/test_data.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque at tortor in tellus accumsan euismod. Quisque scelerisque velit vel nisi ornare lacinia. Vivamus viverra eleifend congue. Maecenas dolor magna, rhoncus vitae fermentum id, convallis id. diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/.gitignore b/vendor/github.com/camlistore/camlistore/server/sigserver/.gitignore new file mode 100644 index 00000000..426570e3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/.gitignore @@ -0,0 +1,4 @@ +camsigd +sigserver +*.6 +*.8 diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/camsigd.go b/vendor/github.com/camlistore/camlistore/server/sigserver/camsigd.go new file mode 100644 index 00000000..45bd9fd3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/camsigd.go @@ -0,0 +1,83 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// The sigserver is a stand-alone JSON signing and verification server. +// +// TODO(bradfitz): as of 2012-01-10 this is very old and superceded by +// the general server and pkg/serverconfig. We should just make it +// possible to configure a signing-only server with +// serverconfig/genconfig.go. I think we basically already can. Then +// we can delete this. +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + + "camlistore.org/pkg/auth" + "camlistore.org/pkg/blob" + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/webserver" +) + +var accessPassword string + +var flagPubKeyDir = flag.String("pubkey-dir", "test/pubkey-blobs", + "Temporary development hack; directory to dig-xxxx.camli public keys.") + +// TODO: for now, the only implementation of the blobref.Fetcher +// interface for fetching public keys is the "local, from disk" +// implementation used for testing. In reality we'd want to be able +// to fetch these from blobservers. +var pubKeyFetcher = blob.NewSimpleDirectoryFetcher(*flagPubKeyDir) + +func handleRoot(conn http.ResponseWriter, req *http.Request) { + fmt.Fprintf(conn, "camsigd") +} + +func handleCamliSig(conn http.ResponseWriter, req *http.Request) { + handler := func(conn http.ResponseWriter, req *http.Request) { + httputil.BadRequestError(conn, "Unsupported path or method.") + } + + switch req.Method { + case "POST": + switch req.URL.Path { + case "/camli/sig/sign": + handler = auth.RequireAuth(handleSign, auth.OpSign) + case "/camli/sig/verify": + handler = handleVerify + } + } + handler(conn, req) +} + +func main() { + flag.Parse() + + mode, err := auth.FromEnv() + if err != nil { + log.Fatal(err) + } + auth.SetMode(mode) + + ws := webserver.New() + ws.HandleFunc("/", handleRoot) + ws.HandleFunc("/camli/sig/", handleCamliSig) + ws.Serve() +} diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/client.pl b/vendor/github.com/camlistore/camlistore/server/sigserver/client.pl new file mode 100755 index 00000000..cce21000 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/client.pl @@ -0,0 +1,50 @@ +#!/usr/bin/perl + +use strict; +use LWP::UserAgent; +use HTTP::Request; +use HTTP::Request::Common; +use Getopt::Long; + +my $keyid = "26F5ABDA"; +my $server = "http://localhost:2856"; +GetOptions("keyid=s" => \$keyid, + "server=s" => \$server) + or usage(); + +$server =~ s!/$!!; + +my $file = shift or usage(); +-f $file or usage("$file isn't a file"); + +my $json = do { undef $/; open(my $fh, $file); <$fh> }; + +sub usage { + my $err = shift; + if ($err) { + print STDERR "Error: $err\n"; + } + print STDERR "Usage: client.pl [OPTS] \n"; + print STDERR "Options:\n"; + print STDERR " --keyid=\n"; + print STDERR " --server=http://host:port\n"; + exit(1); +} + +my $req = POST("$server/camli/sig/sign", + "Authorization" => "Basic dGVzdDp0ZXN0", # test:test + Content => { + "json" => $json, + "keyid" => $keyid, + }); + +my $ua = LWP::UserAgent->new; +my $res = $ua->request($req); +unless ($res->is_success) { + die "Failure: " . $res->status_line . ": " . $res->content; +} + +print $res->content; + + + diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/run.sh b/vendor/github.com/camlistore/camlistore/server/sigserver/run.sh new file mode 100755 index 00000000..27a38b25 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/run.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +export CAMLI_PASSWORD=test +make && ./sigserver "$@" diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/sign.go b/vendor/github.com/camlistore/camlistore/server/sigserver/sign.go new file mode 100644 index 00000000..b262c461 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/sign.go @@ -0,0 +1,54 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/jsonsign" + "fmt" + "net/http" +) + +const kMaxJsonLength = 1024 * 1024 + +func handleSign(conn http.ResponseWriter, req *http.Request) { + if !(req.Method == "POST" && req.URL.Path == "/camli/sig/sign") { + httputil.BadRequestError(conn, "Inconfigured handler.") + return + } + + req.ParseForm() + + jsonStr := req.FormValue("json") + if jsonStr == "" { + httputil.BadRequestError(conn, "Missing json parameter") + return + } + if len(jsonStr) > kMaxJsonLength { + httputil.BadRequestError(conn, "json parameter too large") + return + } + + sreq := &jsonsign.SignRequest{UnsignedJSON: jsonStr, Fetcher: pubKeyFetcher} + signedJson, err := sreq.Sign() + if err != nil { + // TODO: some aren't really a "bad request" + httputil.BadRequestError(conn, fmt.Sprintf("%v", err)) + return + } + conn.Write([]byte(signedJson)) +} diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/spec.txt b/vendor/github.com/camlistore/camlistore/server/sigserver/spec.txt new file mode 100644 index 00000000..3381caa6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/spec.txt @@ -0,0 +1,44 @@ +Sign: + +(https) POST /camli/sig/sign +WWW-Authenticate: [user] [b64pass] + + json=[json to sign] + keyid=[GnuPG key id / implementation dependent] + +On good response: + HTTP 200 OK + (signed blob) + +else: (if signing fails) + HTTP 4xx/5xx + + +TODO(bslatkin): Should the sign response be a more specific value, so +we can tell the difference between a temporary server error and a signing +failure? For verification purposes we need that characteristic anyways. + +--- + +Verify: + +(https) POST /camli/sig/verify + + sjson=[signed json to verify] + (proposed) keyarmored=[GnuPG armored key] + +On good response: + HTTP 200 OK + + YES + +else: (if verification fails) + HTTP 200 OK + + + + +Verify will look in the object to find the "camliSigner" key and use that +blobref's contents (assumed to be a public key) to verify the signature on +the object. Configuring the signing server to have the public key blobref +is out of scope. diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/test/00-start.t b/vendor/github.com/camlistore/camlistore/server/sigserver/test/00-start.t new file mode 100644 index 00000000..e1c41d2c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/test/00-start.t @@ -0,0 +1,20 @@ +#!/usr/bin/perl + +use strict; +use Test::More; +use FindBin; +use lib "$FindBin::Bin"; +use CamsigdTest; + +my $server = CamsigdTest::start(); + +ok($server, "Started the server") or BAIL_OUT("can't start the server"); + +my $ua = LWP::UserAgent->new; +my $req = HTTP::Request->new("GET", $server->root . "/"); +my $res = $ua->request($req); +ok($res, "got an HTTP response") or done_testing(); +ok($res->is_success, "HTTP response is successful"); + +done_testing(3); + diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/test/10-sign.t b/vendor/github.com/camlistore/camlistore/server/sigserver/test/10-sign.t new file mode 100644 index 00000000..bc430c58 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/test/10-sign.t @@ -0,0 +1,96 @@ +#!/usr/bin/perl + +use strict; +use Test::More; +use FindBin; +use lib "$FindBin::Bin"; +use CamsigdTest; +use JSON::Any; +use HTTP::Request::Common; + +my $server = CamsigdTest::start(); +ok($server, "Started the server") or BAIL_OUT("can't start the server"); + +my $ua = LWP::UserAgent->new; + +use constant CAMLI_SIGNER => "sha1-82e6f3494f698aa498d5906349c0aa0a183d89a6"; + +my $j = JSON::Any->new; +my $json = $j->objToJson({ "camliVersion" => 1, + "camliSigner" => CAMLI_SIGNER, + "foo" => "bar", + }); + +# Sign it. +my $sjson; +{ + my $req = req("sign", { "json" => $json }); + my $res = $ua->request($req); + ok($res, "got an HTTP sig response") or done_testing(); + ok($res->is_success, "HTTP sig response is successful") or done_testing(); + $sjson = $res->content; + print "Got signed: $sjson"; + like($sjson, qr/camliSig/, "contains camliSig substring"); + + my $sobj = $j->jsonToObj($sjson); + is($sobj->{"foo"}, "bar", "key foo is still bar"); + is($sobj->{"camliVersion"}, 1, "key camliVersion is still 1"); + ok(defined $sobj->{"camliSig"}, "has camliSig key"); + ok(defined $sobj->{"camliSigner"}, "has camliSigner key"); + is(scalar keys %$sobj, 4, "total of 3 keys in signed object"); +} + +# Verify it. +{ + my $req = req("verify", { "sjson" => $sjson }); + my $res = $ua->request($req); + ok($res, "got an HTTP verify response") or done_testing(); + ok($res->is_success, "HTTP verify response is successful") or done_testing(); + print "Verify response: " . $res->content; + my $vobj = $j->jsonToObj($res->content); + ok(defined($vobj->{'signatureValid'}), "has 'signatureValid' key"); + ok($vobj->{'signatureValid'}, "signature is valid"); + my $vdat = $vobj->{'verifiedData'}; + ok(defined($vdat), "has verified data"); + is($vdat->{'camliSigner'}, CAMLI_SIGNER, "signer matches"); + is($vdat->{'foo'}, "bar") +} + +# Verification that should fail. +{ + my $req = req("verify", { "sjson" => "{}" }); + my $res = $ua->request($req); + ok($res, "got an HTTP verify response") or done_testing(); + ok($res->is_success, "HTTP verify response is successful") or done_testing(); + print "Verify response: " . $res->content; + my $vobj = $j->jsonToObj($res->content); + ok(defined($vobj->{'signatureValid'}), "has 'signatureValid' key"); + is(0, $vobj->{'signatureValid'}, "signature is properly invalid"); + ok(!defined($vobj->{'verifiedData'}), "no verified data key"); + ok(defined($vobj->{'errorMessage'}), "has an error message"); +} + +# Imposter! Verification should fail. +{ + my $eviljson = q{{"camliVersion":1,"camliSigner":"sha1-82e6f3494f698aa498d5906349c0aa0a183d89a6","foo":"evilbar","camliSig":"iQEcBAABAgAGBQJM+tnUAAoJEIUeCLJL7Fq1ruwH/RplOpmrTK51etXUHayRGN0RM0Jxttjwa0pPuiHr7fJifaZo2pvMZOMAttjFEP/HMjvpSVi8P7awBFXXlCTj0CAlexsmCsPEHzITXe3siFzH+XCSmfHNPYYti0apQ2+OcWNnzqWXLiEfP5yRVXxcxoWuxYlnFu+mfw5VdjrJpIa+n3Ys5D4zUPVCSNtF4XV537czqfd9AiSfKCY/aL2NuZykl4WtP3JgYl8btE84EjNLFasQDstcWOvp7rrP6T8hQQotw5/F4SmmFM6ybkWXk/Wkax3XpzW9qL00VqhxHd4JIWaSzSV/WcSQwCoLWc7uXttOWgVtMIhzpjeMlqt1gc0==QYU2"}}; + my $req = req("verify", { "sjson" => $eviljson }); + my $res = $ua->request($req); + ok($res, "got an HTTP verify response") or done_testing(); + ok($res->is_success, "HTTP verify response is successful") or done_testing(); + print "Verify response: " . $res->content; + my $vobj = $j->jsonToObj($res->content); + ok(defined($vobj->{'signatureValid'}), "has 'signatureValid' key"); + is(0, $vobj->{'signatureValid'}, "signature is properly invalid"); + ok(!defined($vobj->{'verifiedData'}), "no verified data key"); + ok(defined($vobj->{'errorMessage'}), "has an error message"); + like($vobj->{'errorMessage'}, qr/bad signature: RSA verification error/, "verification error"); +} + +done_testing(29); + +sub req { + my ($method, $post_params) = @_; + return POST($server->root . "/camli/sig/" . $method, + "Authorization" => "Basic dGVzdDp0ZXN0", # test:test + Content => $post_params); +} diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/test/CamsigdTest.pm b/vendor/github.com/camlistore/camlistore/server/sigserver/test/CamsigdTest.pm new file mode 100644 index 00000000..b238898d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/test/CamsigdTest.pm @@ -0,0 +1,78 @@ +#!/usr/bin/perl +# +# Common test library for camsigd (sigserver) + +package CamsigdTest; + +use strict; +use Test::More; +use FindBin; +use LWP::UserAgent; +use HTTP::Request; +use Fcntl; + +our $BINARY = "$FindBin::Bin/../sigserver"; + +sub start { + my ($port_rd, $port_wr, $exit_rd, $exit_wr); + my $flags; + pipe $port_rd, $port_wr; + pipe $exit_rd, $exit_wr; + + $flags = fcntl($port_wr, F_GETFD, 0); + fcntl($port_wr, F_SETFD, $flags & ~FD_CLOEXEC); + $flags = fcntl($exit_rd, F_GETFD, 0); + fcntl($exit_rd, F_SETFD, $flags & ~FD_CLOEXEC); + + $ENV{TESTING_PORT_WRITE_FD} = fileno($port_wr); + $ENV{TESTING_CONTROL_READ_FD} = fileno($exit_rd); + $ENV{CAMLI_PASSWORD} = "test"; + + die "Binary $BINARY doesn't exist\n" unless -x $BINARY; + + my $pid = fork; + die "Failed to fork" unless defined($pid); + if ($pid == 0) { + # child + exec $BINARY, "-listen=:0"; + die "failed to exec: $!\n"; + } + close($exit_rd); # child owns this side + close($port_wr); # child owns this side + + print "Waiting for server to start...\n"; + my $line = <$port_rd>; + close($port_rd); + + # Parse the port line out + chomp $line; + # print "Got port line: $line\n"; + die "Failed to start, no port info." unless $line =~ /:(\d+)$/; + my $port = $1; + + return CamsigdTest::Server->new($pid, $port, $exit_wr); +} + +package CamsigdTest::Server; + +sub new { + my ($class, $pid, $port, $pipe_writer) = @_; + return bless { + pid => $pid, + port => $port, + pipe_writer => $pipe_writer, + }; +} + +sub DESTROY { + my $self = shift; + my $pipe = $self->{pipe_writer}; + syswrite($pipe, "EXIT\n", 5); +} + +sub root { + my $self = shift; + return "http://localhost:$self->{port}"; +} + +1; diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/test/doc.tmp b/vendor/github.com/camlistore/camlistore/server/sigserver/test/doc.tmp new file mode 100644 index 00000000..2dcc98c1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/test/doc.tmp @@ -0,0 +1 @@ +{"camliVersion":1,"foo":"bar" \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/test/pubkey-blobs/sha1-82e6f3494f698aa498d5906349c0aa0a183d89a6.camli b/vendor/github.com/camlistore/camlistore/server/sigserver/test/pubkey-blobs/sha1-82e6f3494f698aa498d5906349c0aa0a183d89a6.camli new file mode 100644 index 00000000..bb94ce58 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/test/pubkey-blobs/sha1-82e6f3494f698aa498d5906349c0aa0a183d89a6.camli @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +mQENBEzgoVsBCAC/56aEJ9BNIGV9FVP+WzenTAkg12k86YqlwJVAB/VwdMlyXxvi +bCT1RVRfnYxscs14LLfcMWF3zMucw16mLlJCBSLvbZ0jn4h+/8vK5WuAdjw2YzLs +WtBcjWn3lV6tb4RJz5gtD/o1w8VWxwAnAVIWZntKAWmkcChCRgdUeWso76+plxE5 +aRYBJqdT1mctGqNEISd/WYPMgwnWXQsVi3x4z1dYu2tD9uO1dkAff12z1kyZQIBQ +rexKYRRRh9IKAayD4kgS0wdlULjBU98aeEaMz1ckuB46DX3lAYqmmTEL/Rl9cOI0 +Enpn/oOOfYFa5h0AFndZd1blMvruXfdAobjVABEBAAG0JUNhbWxpIFRlc3RlciA8 +Y2FtbGktdGVzdEBleGFtcGxlLmNvbT6JATgEEwECACIFAkzgoVsCGwMGCwkIBwMC +BhUIAgkKCwQWAgMBAh4BAheAAAoJECkxpnwm9avaHE0IAJ/pMZgiURl3kefrFMAV +7ei0XDfTekZOwDRcZWTVQ/A97phpzO8t78qLYbFeHuq3myNhrlVO9Gyp+2V904rN +dudoHLhpegf5TNeHGmAGHBxcooMPMp0JyIDnUBxtCNGxgWfbKpEDRsQAjkCc7sR0 +H+OegzlEf6JZGzEhV5ohOioTsC1DmJNoQsRz5Kes7sLoAzpQCbCv4yv+1o+mnzgW +9qPJXKxcScc0t2YTvcvpJ7LV8no1OP6vpYqB1A9Pzze6XFBlcXOUKbRKk0fEIV/u +pU3ph1fF7wlyRgA4A3iPwDC4BgVmHYkz9nYPn+7IcT/dDig5SWU+n7WZgGeyv75y +0Ue5AQ0ETOChWwEIALuHxKI+oSH+eeMSXhxcSUXnhp4cUeyvOV7oNPYcmsDclF0Y +7y8NrSPiEZod9vSTEDMq7hd3BG+feCBqjgR4qtmoXguJhWcnJqDBk5iAMuuAph9O +CC8QLACMJPhoxQ0UtDPKlpG4X8kLK1woHd716ulPl2KLjTgd6K4kCGj+CV5Ekn6u +IJj+3IPbYDOwk1l06ksimwQAY4dA1CXOTviH1bVqR6CzuzVPg4hcryWDva1rEO5c +LcOR8Wk/thANFLSNjqX8UgtGXhFZRWxKetFDQiX5f2BKoqTVYvD3pqt+zzyLNFAz +xhMc3cyFfqM8yQdzdEey/DIWtMoDqZCSVMJ63N8AEQEAAYkBHwQYAQIACQUCTOCh +WwIbDAAKCRApMaZ8JvWr2mHACACkco+fAfRK+gmprF2m8E0Bp1frwFH0g4RJVHXQ +BUDbg7OZbWumzD4Br28si6XDVMP6fLOeyD0EHYb6LhAHDkBLqx6e3kKG1mQ8fMIV +O4YMQfskYH2FJqlCtgMnM8N3oslPBTpZedNPSUq7HJh2pKr9GIDi1V+Hgc/qEigE +dj9f2zSSaKZdC4eL73GvlQOh+4XqgaMnMiKfI+/2WlRaJs1KOgKmIp5yHt0qY0ef +y+40BY/z9pMjyUvr/Wwp8KXArw0NAwzp8NUl5fNxRg9XWQWLn6hW8ydR20X3t2ym +iNSWzNQiTT6k7fumOABCoSZsow/AJxQSxqKOJBjgpKjIKCgY +=ru0J +-----END PGP PUBLIC KEY BLOCK----- diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/test/sig.tmp b/vendor/github.com/camlistore/camlistore/server/sigserver/test/sig.tmp new file mode 100644 index 00000000..95538f6b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/test/sig.tmp @@ -0,0 +1,6 @@ +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.10 (GNU/Linux) + +iQEcBAABAgAGBQJM9KDoAAoJECkxpnwm9avabeYH/2+Rm1FjSDKIxUlF+RCvaKWYflJuCtazJTWezud3CL+q2DSWEl8o7z6TUDB15w8nzRlLDEXqqGYPec76eyoyh4R98A2oxmms1nJY1HFXWN4LFUcinOBnM175f5qyiFr0c64sSMaBt21Qkt6Ncecg7NpTyl31Uz3JmlG7SZRm5yL08shbNR0AvTSnwUAwyWiy+v9qwvK3VoAxA2CXgJDTudEjf8MoMna0MmF43hWSdqGkqVao5rJtpru+iMHXkaqrgX24go1PRwVOyz6mJdgkqnYMqGinYAw+w05s09wfpQ/xLEuCCYfehtLGcSPEPkfFD701hgo/9OR1w+hdrrFKSNo= +=Nzxs +-----END PGP SIGNATURE----- diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/test/test-keyring.gpg b/vendor/github.com/camlistore/camlistore/server/sigserver/test/test-keyring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..3d20ba6837e26057e38b773a00e79af92f52acb6 GIT binary patch literal 1196 zcmV;d1XKH&0SyF9;GtUq2mrt5ri3TZO(11`6;u9OH>XSqAlGR;>58Sml|TpeaCFIX zUmN0VB=tp9U!9C>a?N-wx7;ydcg)M2!(OH?QbGkH?`@qUpNM|{%gW_zfOb4KV>0Yo z&|Hma_my6)Z-hzDm@N76B%w zQ`ToK8lyxZCx2Ok%!3KmT?-Y9e0a}SSi5UO_T#m7Kp%fyv(`+RK!8xK>`Gx2QHRnB z0jz`KND|WrWl*@mQ{NhRMvTu_B)A?r4SnSSil&({3;h{=aN;x)dT0KFj(vez<{bbQ zcUgB<`0stZf0!-kcTLK#c1`7!Y2Ll2I6$k#M1P`L8!;hQnjtzW6R<5qn3HHi#B=1QtnR|- z13FL%u&?7Q{??DCpEwrwqsd&XTuH|?w`LQ)%jqYw)$)2ZIR3Atihpcb`PKK$Z#^ znSf`qzrJ$ON3a3_1GxbW1We$eTLB0FyNAT0KA|E0dE*jZ99&67=Z2mfQS7fdUg$LT z9Gbw~lwBC_FAc3D;t`r1_VkkwGb-*EcLZ;rcpz$y1bC|1s9p<+g=Z%wpuv-vfHLcV zrXNlSFAyvMj3oGI#SIj+Gs>2cxL?T&D_kfY-u3F~PnTkgjW`|Xt|SO({s~@0l76lr znEu>@+h8-WlUa1?OCp;D0Aq(h)FsYN_=nZCYDb{6yERXPh+MBFgT1Y55bj(p!;$f6 zKeiAJ6ts?EN3#4f7PQI(sgROX!g}1_01*KI0f_-01Q-DV00{*GOyHqg0vikf3JDM?F{XSb z^{d)pzz6`Oa*v+@^h){(sjOY5@J#`ySL?u0^n-*+RCUk=K-+_}nQd#P%sv6HZ!C+Y z!&Jlie6ya&Jp>(w`YsR$4nRw*9-iJphSp>}e8Lqwh73XbBw&4oCaFTU11B@XccRHp z1v*)I(@#lCyBwHyq^kWGfa29(hk?)P5-0?AKVRE4l4zz~3x|vEaj%sFq5FmEfukof zBA+Ag_F7b0Ce2DZ0;VFKavt3(V@IFM?lc9D^Y)V?$xG|~Y$@=iz^@Gr0}Sc#)g|Tg zaYheUSp|!qs8;hQQQJlLw``_})RxTDB27M|?fa%U079WAY@-jrClnIKqK+gO;H0R? KC@2`P0ssRNTnzyL literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/test/test-keyring2.gpg b/vendor/github.com/camlistore/camlistore/server/sigserver/test/test-keyring2.gpg new file mode 100644 index 0000000000000000000000000000000000000000..ded7d5151dfbfb7a3c78487e9b4a5173fac49fed GIT binary patch literal 1202 zcmV;j1Wo&y0SyF9`qozg2msMS7aR1l5OrP4g-&1Xz@s_KW-h}1%ZWHtsk5HT7C9J= zJVDUcaIqA)(G1;=L51Sr)T*IMhcwF(O+r&uZ33S;)KR^4iM7j7g}=?S&Gpr+@cBql zl#+2RUB=OhDo>Stdhc^m!Rnf;jcI^@Oc2JvR!)VsAXI;N$~7(}RP%Rpq@^{V{G0Hc zy}~TqhRR*{7o00)^d@qPM1o|Lw7@A3mni?7O9kdi7cBdFal4oJrYeQ*fM18%=oAev zbY=|%Wn71F-zNcFPGaPcVOc5Io`q@Ly;c*}JXDaI+|aH#q7cWjX$IL=X5O+w!Yzg& zU_WwF;tU8_Gpo}hdKmx_0RRECD??#zY-u1=Wpi|Ob7gWMRCjM6JY!*PY-uiZWpi{u zWq4t2aBO8RV{dIfi2*nS69EDMA_W3W`qozh8v_Ol2?z%R0tOWb0tpHW1Qr4V0RkQY z0vCV)3JDN}9tg5a>{_*RjtBswiH|9$ign%e)0XUK=^QDtpERG8l4wQ5bFkd}-cnn@ zgxCq)M8ynbJI*XHq)#R%pL5r#Kk?x4UK`acBpNS(@n)zW^O>|mJRA<<5UG{>2;XE* zq_^y}wzlzh?lxPPOV9hBuCrwu>iyDSO^#w$v7BbXrbHI9?G{>|%q-nOE34m>3uM7> zTc)UBRKS4gHLg!(2H64)go#Dz;hd z7jz2b0SB^#V3r74Hxf14Ap^B9F?)^Kv56jT--3W2E`*r8pdj78q#FbKN2H5DI@jrZLXI0YwWmvvF_S zwes!(!{^F@&X(MSJ(0>3C0RRDs0Urby0RjLC1p-X^)>i@>3;+rV z5QQEHvP+oIwgYi}0fSga0NhDa zjGCmpgz|BN_Tm>2Xc$-~*M(`)cXe)4hjL?Aa6IH)JN5{t6mAA7yftQgCj@`19!dYd zL^kq|^>cZ#I?DC#!o%*fx$SJsJ`U-)+dg?+h?ueQ1-ouhw;JNKZQf!@LMthqMh>;+ znEcL!X)l&_Gz_^w7WuONVLi7-ODc;i4w@~>wjoaBdj5i7m2zJ)bza*Tzxq3eGK!g= zR&xuFbJi+{swXDhQ47fP&%|<%B~3lQ0BNO_TU(hf4uqSnS}JzV8(jh4Nw*0P9l#jH Q_82C$#xxPx# literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/test/test-secring.gpg b/vendor/github.com/camlistore/camlistore/server/sigserver/test/test-secring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..bca3ad0392b59249597bc1ec8a93a5d9f6141758 GIT binary patch literal 2498 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 MsK_WN7_b5W05^l1$p8QV literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/test/test-secring2.gpg b/vendor/github.com/camlistore/camlistore/server/sigserver/test/test-secring2.gpg new file mode 100644 index 0000000000000000000000000000000000000000..f4b7ed22286696c1f8ee02530e882fc2252bbdaf GIT binary patch literal 2504 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 S7{&G&Cbh;iA)nZ=0ssKko~)Aq literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/test/test.json b/vendor/github.com/camlistore/camlistore/server/sigserver/test/test.json new file mode 100644 index 00000000..c9a36957 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/test/test.json @@ -0,0 +1,5 @@ +{ + "foo": "bar", + "blah": "baz" } + + diff --git a/vendor/github.com/camlistore/camlistore/server/sigserver/verify.go b/vendor/github.com/camlistore/camlistore/server/sigserver/verify.go new file mode 100644 index 00000000..d2d96464 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/sigserver/verify.go @@ -0,0 +1,64 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +/* + + $ gpg --no-default-keyring --keyring=/tmp/foo --import --armor test/pubkey-blobs/sha1-82e6f3494f69 + + $ gpg --no-default-keyring --keyring=/tmp/foo --verify sig.tmp doc.tmp ; echo $? + gpg: Signature made Mon 29 Nov 2010 10:59:52 PM PST using RSA key ID 26F5ABDA + gpg: Good signature from "Camli Tester " + gpg: WARNING: This key is not certified with a trusted signature! + gpg: There is no indication that the signature belongs to the owner. + Primary key fingerprint: FBB8 9AA3 20A2 806F E497 C049 2931 A67C 26F5 ABDA0 + +*/ + +import ( + "camlistore.org/pkg/httputil" + "camlistore.org/pkg/jsonsign" + "net/http" +) + +func handleVerify(conn http.ResponseWriter, req *http.Request) { + if !(req.Method == "POST" && req.URL.Path == "/camli/sig/verify") { + httputil.BadRequestError(conn, "Inconfigured handler.") + return + } + + req.ParseForm() + sjson := req.FormValue("sjson") + if sjson == "" { + httputil.BadRequestError(conn, "Missing sjson parameter.") + return + } + + m := make(map[string]interface{}) + + vreq := jsonsign.NewVerificationRequest(sjson, pubKeyFetcher) + if vreq.Verify() { + m["signatureValid"] = 1 + m["verifiedData"] = vreq.PayloadMap + } else { + m["signatureValid"] = 0 + m["errorMessage"] = vreq.Err.Error() + } + + conn.WriteHeader(http.StatusOK) // no HTTP response code fun, error info in JSON + httputil.ReturnJSON(conn, m) +} diff --git a/vendor/github.com/camlistore/camlistore/server/tester/bs-test.pl b/vendor/github.com/camlistore/camlistore/server/tester/bs-test.pl new file mode 100755 index 00000000..5fad55ef --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/server/tester/bs-test.pl @@ -0,0 +1,430 @@ +#!/usr/bin/perl +# +# Test script to run against a Camli blobserver to test its compliance +# with the spec. + +use strict; +use Getopt::Long; +use LWP; +use Test::More; + +my $user; +my $password; +my $implopt; +GetOptions("user" => \$user, + "password" => \$password, + "impl=s" => \$implopt, + ) or usage(); + +my $impl; +my %args = (user => $user, password => $password); +if ($implopt eq "go") { + $impl = Impl::Go->new(%args); +} elsif ($implopt eq "appengine") { + $impl = Impl::AppEngine->new(%args); +} else { + die "The --impl flag must be 'go' or 'appengine'.\n"; +} + +ok($impl->start, "Server started"); + +$impl->verify_no_blobs; # also tests some of enumerate +$impl->test_stat_and_upload; +$impl->test_upload_corrupt_blob; # blobref digest doesn't match + +# TODO: test multiple uploads in a batch +# TODO: test uploads in serial (using each response's next uploadUrl) +# TODO: test enumerate boundaries +# TODO: interrupt a POST upload in the middle; verify no straggler on +# disk in subsequent GET +# .... +# test auth works on bogus password? (auth still undefined) +# TODO: test stat with both GET and POST (currently just POST) + +done_testing(); + +sub usage { + die "Usage: bs-test.pl [--user= --password=] --impl={go,appengine}\n"; +} + +package Impl; +use HTTP::Request::Common; +use LWP::UserAgent; +use JSON::Any; +use Test::More; +use Digest::SHA1 qw(sha1_hex); +use URI::URL (); +use Data::Dumper; + +sub new { + my ($class, %args) = @_; + return bless \%args, $class; +} + +sub post { + my ($self, $path, $form) = @_; + $path ||= ""; + $form ||= {}; + return POST($self->path($path), + "Authorization" => "Basic dGVzdDp0ZXN0", # test:test + Content => $form); +} + +sub upload_request { + my ($self, $upload_url, $blobref_to_blob_map) = @_; + my @content; + my $n = 0; + foreach my $key (sort keys %$blobref_to_blob_map) { + $n++; + # TODO: the App Engine client refused to work unless the Content-Type + # is set. This should be clarified in the docs (MUST?) and update the + # test suite and Go server accordingly (to fail if not present). + push @content, $key => [ + undef, "filename$n", + "Content-Type" => "application/octet-stream", + Content => $blobref_to_blob_map->{$key}, + ]; + } + + return POST($upload_url, + "Content_Type" => 'form-data', + "Authorization" => "Basic dGVzdDp0ZXN0", # test:test + Content => \@content); +} + +sub get { + my ($self, $path, $form) = @_; + $path ||= ""; + $form ||= {}; + return GET($self->path($path), + "Authorization" => "Basic dGVzdDp0ZXN0", # test:test + %$form); +} + +sub head { + my ($self, $path, $form) = @_; + $path ||= ""; + $form ||= {}; + return HEAD($self->path($path), + "Authorization" => "Basic dGVzdDp0ZXN0", # test:test + %$form); +} + +sub ua { + my $self = shift; + return ($self->{_ua} ||= LWP::UserAgent->new(agent => "camli/blobserver-tester")); +} + +sub root { + my $self= shift; + return $self->{root} or die "No 'root' for $self"; +} + +sub path { + my $self = shift; + my $path = shift || ""; + return $self->root . $path; +} + +sub get_json { + my ($self, $req, $msg, $opts) = @_; + $opts ||= {}; + + my $res = $self->ua->request($req); + ok(defined($res), "got response for HTTP request '$msg'"); + + if ($res->code =~ m!^30[123]$! && $opts->{follow_redirect}) { + my $location = $res->header("Location"); + if ($res->code == "303") { + $req->method("GET"); + } + my $new_uri = URI::URL->new($location, $req->uri)->abs; + diag("Old URI was " . $req->uri); + diag("New is " . $new_uri); + diag("Redirecting HTTP request '$msg' to $location ($new_uri)"); + $req->uri($new_uri); + $res = $self->ua->request($req); + ok(defined($res), "got redirected response for HTTP request '$msg'"); + } + + ok($res->is_success, "successful response for HTTP request '$msg'") + or diag("Status was: " . $res->status_line); + my $json = JSON::Any->jsonToObj($res->content); + is("HASH", ref($json), "JSON parsed for HTTP request '$msg'") + or BAIL_OUT("expected JSON response"); + return $json; +} + +sub get_upload_json { + my ($self, $req) = @_; + return $self->get_json($req, "upload", { follow_redirect => 1 }) +} + +sub verify_no_blobs { + my $self = shift; + my $req = $self->get("/camli/enumerate-blobs", { + "after" => "", + "limit" => 10, + }); + my $json = $self->get_json($req, "enumerate empty blobs"); + ok(defined($json->{'blobs'}), "enumerate has a 'blobs' key"); + is("ARRAY", ref($json->{'blobs'}), "enumerate's blobs key is an array"); + is(0, scalar @{$json->{'blobs'}}, "no blobs on server"); +} + +sub test_stat_and_upload { + my $self = shift; + my ($req, $res); + + my $blob = "This is a line.\r\nWith mixed newlines\rFoo\nAnd binary\0data.\0\n\r."; + my $blobref = "sha1-" . sha1_hex($blob); + + # Bogus method. + $req = $self->head("/camli/stat", { + "camliversion" => 1, + "blob1" => $blobref, + }); + $res = $self->ua->request($req); + ok(!$res->is_success, "returns failure for HEAD on /camli/stat"); + + # Correct method, but missing camliVersion. + $req = $self->post("/camli/stat", { + "blob1" => $blobref, + }); + $res = $self->ua->request($req); + ok(!$res->is_success, "returns failure for missing camliVersion param on stat"); + + # Valid pre-upload + $req = $self->post("/camli/stat", { + "camliversion" => 1, + "blob1" => $blobref, + }); + my $jres = $self->get_json($req, "valid stat"); + diag("stat response: " . Dumper($jres)); + ok($jres, "valid stat JSON response"); + for my $f (qw(stat maxUploadSize uploadUrl uploadUrlExpirationSeconds)) { + ok(defined($jres->{$f}), "required field '$f' present"); + } + is(scalar(keys %$jres), 4, "Exactly 4 JSON keys returned"); + my $statList = $jres->{stat}; + is(ref($statList), "ARRAY", "stat is an array"); + is(scalar(@$statList), 0, "server doesn't have this blob yet."); + like($jres->{uploadUrlExpirationSeconds}, qr/^\d+$/, "uploadUrlExpirationSeconds is numeric"); + my $upload_url = URI::URL->new($jres->{uploadUrl}, $self->root)->abs; + ok($upload_url, "valid uploadUrl"); + # TODO: test & clarify in spec: are relative URLs allowed in uploadUrl? + # App Engine seems to do it already, and makes it easier, so probably + # best to clarify that they're relative. + + # Do the actual upload + my $upreq = $self->upload_request($upload_url, { + $blobref => $blob, + }); + diag("upload request: " . $upreq->as_string); + my $upres = $self->get_upload_json($upreq); + ok($upres, "Upload was success"); + print STDERR "# upload response: ", Dumper($upres); + + for my $f (qw(uploadUrlExpirationSeconds uploadUrl maxUploadSize received)) { + ok(defined($upres->{$f}), "required upload response field '$f' present"); + } + is(scalar(keys %$upres), 4, "Exactly 4 JSON keys returned"); + + like($upres->{uploadUrlExpirationSeconds}, qr/^\d+$/, "uploadUrlExpirationSeconds is numeric"); + is(ref($upres->{received}), "ARRAY", "'received' is an array") + or BAIL_OUT(); + my $got = $upres->{received}; + is(scalar(@$got), 1, "got one file"); + is($got->[0]{blobRef}, $blobref, "received[0] 'blobRef' matches"); + is($got->[0]{size}, length($blob), "received[0] 'size' matches"); + + # TODO: do a get request, verify that we get it back. +} + +sub test_upload_corrupt_blob { + my $self = shift; + my ($req, $res); + + my $blob = "A blob, pre-corruption."; + my $blobref = "sha1-" . sha1_hex($blob); + $blob .= "OIEWUROIEWURLKJDSLKj CORRUPT"; + + $req = $self->post("/camli/stat", { + "camliversion" => 1, + "blob1" => $blobref, + }); + my $jres = $self->get_json($req, "valid stat"); + my $upload_url = URI::URL->new($jres->{uploadUrl}, $self->root)->abs; + # TODO: test & clarify in spec: are relative URLs allowed in uploadUrl? + # App Engine seems to do it already, and makes it easier, so probably + # best to clarify that they're relative. + + # Do the actual upload + my $upreq = $self->upload_request($upload_url, { + $blobref => $blob, + }); + diag("corrupt upload request: " . $upreq->as_string); + my $upres = $self->get_upload_json($upreq); + my $got = $upres->{received}; + is(ref($got), "ARRAY", "corrupt upload returned a 'received' array"); + is(scalar(@$got), 0, "didn't get any files (it was corrupt)"); +} + +package Impl::Go; +use base 'Impl'; +use FindBin; +use LWP::UserAgent; +use HTTP::Request; +use Fcntl; +use File::Temp (); + +sub start { + my $self = shift; + + $self->{_tmpdir_obj} = File::Temp->newdir(); + my $tmpdir = $self->{_tmpdir_obj}->dirname; + + die "Failed to create temporary directory." unless -d $tmpdir; + + system("$FindBin::Bin/../../build.pl", "server/go/blobserver") + and die "Failed to build Go blobserver."; + + my $bindir = "$FindBin::Bin/../go/blobserver/"; + my $binary = "$bindir/blobserver"; + + chdir($bindir) or die "filed to chdir to $bindir: $!"; + system("make") and die "failed to run make in $bindir"; + + my ($port_rd, $port_wr, $exit_rd, $exit_wr); + my $flags; + pipe $port_rd, $port_wr; + pipe $exit_rd, $exit_wr; + + $flags = fcntl($port_wr, F_GETFD, 0); + fcntl($port_wr, F_SETFD, $flags & ~FD_CLOEXEC); + $flags = fcntl($exit_rd, F_GETFD, 0); + fcntl($exit_rd, F_SETFD, $flags & ~FD_CLOEXEC); + + $ENV{TESTING_PORT_WRITE_FD} = fileno($port_wr); + $ENV{TESTING_CONTROL_READ_FD} = fileno($exit_rd); + $ENV{CAMLI_PASSWORD} = "test"; + + die "Binary $binary doesn't exist\n" unless -x $binary; + + my $pid = fork; + die "Failed to fork" unless defined($pid); + if ($pid == 0) { + # child + my @args = ($binary, "-listen=:0", "-root=$tmpdir"); + print STDERR "# Running: [@args]\n"; + exec @args; + die "failed to exec: $!\n"; + } + close($exit_rd); # child owns this side + close($port_wr); # child owns this side + + print "Waiting for Go server to start...\n"; + my $line = <$port_rd>; + close($port_rd); + + # Parse the port line out + chomp $line; + # print "Got port line: $line\n"; + die "Failed to start, no port info." unless $line =~ /:(\d+)$/; + $self->{port} = $1; + $self->{root} = "http://localhost:$self->{port}"; + print STDERR "# Running on $self->{root} ...\n"; + + # Keep a reference to this to write "EXIT\n" to in order + # to cleanly shutdown the child camlistored process. + # If we close it, the child also dies, though. + $self->{_exit_wr} = $exit_wr; + return 1; +} + +sub DESTROY { + my $self = shift; + syswrite($self->{_exit_wr}, "EXIT\n"); +} + +package Impl::AppEngine; +use base 'Impl'; +use IO::Socket::INET; +use Time::HiRes (); + +sub start { + my $self = shift; + + my $dev_appserver = `which dev_appserver.py`; + chomp $dev_appserver; + unless ($dev_appserver && -x $dev_appserver) { + $dev_appserver = "$ENV{HOME}/sdk/google_appengine/dev_appserver.py"; + unless (-x $dev_appserver) { + die "No dev_appserver.py in \$PATH nor in \$HOME/sdk/google_appengine/dev_appserver.py\n"; + } + } + + $self->{_tempdir_blobstore_obj} = File::Temp->newdir(); + $self->{_tempdir_datastore_obj} = File::Temp->newdir(); + my $datapath = $self->{_tempdir_blobstore_obj}->dirname . "/datastore-file"; + my $blobdir = $self->{_tempdir_datastore_obj}->dirname; + + my $port; + while (1) { + $port = int(rand(30000) + 1024); + my $sock = IO::Socket::INET->new(Listen => 5, + LocalAddr => '127.0.0.1', + LocalPort => $port, + ReuseAddr => 1, + Proto => 'tcp'); + if ($sock) { + last; + } + } + $self->{port} = $port; + $self->{root} = "http://localhost:$self->{port}"; + + my $pid = fork; + die "Failed to fork" unless defined($pid); + if ($pid == 0) { + my $appdir = "$FindBin::Bin/../appengine/blobserver"; + + # child + my @args = ($dev_appserver, + "--clear_datastore", # kinda redundant as we made a temp dir + "--datastore_path=$datapath", + "--blobstore_path=$blobdir", + "--port=$port", + $appdir); + print STDERR "# Running: [@args]\n"; + exec @args; + die "failed to exec: $!\n"; + } + $self->{pid} = $pid; + + my $last_print = 0; + for (1..15) { + my $now = time(); + if ($now != $last_print) { + print STDERR "# Waiting for appengine app to start...\n"; + $last_print = $now; + } + my $res = $self->ua->request($self->get("/")); + if ($res && $res->is_success) { + print STDERR "# Up."; + last; + } + Time::HiRes::sleep(0.1); + } + return 1; +} + +sub DESTROY { + my $self = shift; + kill 3, $self->{pid} if $self->{pid}; +} + +1; + + + diff --git a/vendor/github.com/camlistore/camlistore/third_party/README b/vendor/github.com/camlistore/camlistore/third_party/README new file mode 100644 index 00000000..501e46fc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/README @@ -0,0 +1,10 @@ +External packages which Camlistore depends on. + +These are not under Camlistore copyright/license. See the respective projects +for their copyright & licensing details. + +These are mirrored into Camlistore for hermetic build reasons, as well +as enabling local patching to work with an ever-changing upstream Go +project. (not all projects will follow Go tip as closely) + + diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/.gitattributes b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/.gitattributes new file mode 100644 index 00000000..b65f2a9f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/.gitattributes @@ -0,0 +1,2 @@ +*.go filter=gofmt +*.cgo filter=gofmt diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/.gitignore b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/.gitignore new file mode 100644 index 00000000..2b286ca9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/.gitignore @@ -0,0 +1,8 @@ +*~ +.#* +## the next line needs to start with a backslash to avoid looking like +## a comment +\#*# +.*.swp + +*.test diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/LICENSE b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/LICENSE new file mode 100644 index 00000000..d369cb82 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/LICENSE @@ -0,0 +1,93 @@ +Copyright (c) 2013, 2014 Tommi Virtanen. +Copyright (c) 2009, 2011, 2012 The Go Authors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +The following included software components have additional copyright +notices and license terms that may differ from the above. + + +File fuse.go: + +// Adapted from Plan 9 from User Space's src/cmd/9pfuse/fuse.c, +// which carries this notice: +// +// The files in this directory are subject to the following license. +// +// The author of this software is Russ Cox. +// +// Copyright (c) 2006 Russ Cox +// +// Permission to use, copy, modify, and distribute this software for any +// purpose without fee is hereby granted, provided that this entire notice +// is included in all copies of any software which is or includes a copy +// or modification of this software and in all copies of the supporting +// documentation for such software. +// +// THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED +// WARRANTY. IN PARTICULAR, THE AUTHOR MAKES NO REPRESENTATION OR WARRANTY +// OF ANY KIND CONCERNING THE MERCHANTABILITY OF THIS SOFTWARE OR ITS +// FITNESS FOR ANY PARTICULAR PURPOSE. + + +File fuse_kernel.go: + +// Derived from FUSE's fuse_kernel.h +/* + This file defines the kernel interface of FUSE + Copyright (C) 2001-2007 Miklos Szeredi + + + This -- and only this -- header file may also be distributed under + the terms of the BSD Licence as follows: + + Copyright (C) 2001-2007 Miklos Szeredi. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. +*/ diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/README.md b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/README.md new file mode 100644 index 00000000..471b2b25 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/README.md @@ -0,0 +1,23 @@ +bazil.org/fuse -- Filesystems in Go +=================================== + +`bazil.org/fuse` is a Go library for writing FUSE userspace +filesystems. + +It is a from-scratch implementation of the kernel-userspace +communication protocol, and does not use the C library from the +project called FUSE. `bazil.org/fuse` embraces Go fully for safety and +ease of programming. + +Here’s how to get going: + + go get bazil.org/fuse + +Website: http://bazil.org/fuse/ + +Github repository: https://github.com/bazillion/fuse + +API docs: http://godoc.org/bazil.org/fuse + +Our thanks to Russ Cox for his fuse library, which this project is +based on. diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/debug.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/debug.go new file mode 100644 index 00000000..be9f900d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/debug.go @@ -0,0 +1,21 @@ +package fuse + +import ( + "runtime" +) + +func stack() string { + buf := make([]byte, 1024) + return string(buf[:runtime.Stack(buf, false)]) +} + +func nop(msg interface{}) {} + +// Debug is called to output debug messages, including protocol +// traces. The default behavior is to do nothing. +// +// The messages have human-friendly string representations and are +// safe to marshal to JSON. +// +// Implementations must not retain msg. +var Debug func(msg interface{}) = nop diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/.gitignore b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/.gitignore new file mode 100644 index 00000000..6ebe2d17 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/.gitignore @@ -0,0 +1,4 @@ +/*.seq.svg + +# not ignoring *.seq.png; we want those committed to the repo +# for embedding on Github diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/README.md b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/README.md new file mode 100644 index 00000000..54ed0e59 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/README.md @@ -0,0 +1,6 @@ +# bazil.org/fuse documentation + +See also API docs at http://godoc.org/bazil.org/fuse + +- [The mount sequence](mount-sequence.md) +- [Writing documentation](writing-docs.md) diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux-error-init.seq b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux-error-init.seq new file mode 100644 index 00000000..89cf1515 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux-error-init.seq @@ -0,0 +1,32 @@ +seqdiag { + app; + fuse [label="bazil.org/fuse"]; + fusermount; + kernel; + mounts; + + app; + fuse [label="bazil.org/fuse"]; + fusermount; + kernel; + mounts; + + app -> fuse [label="Mount"]; + fuse -> fusermount [label="spawn, pass socketpair fd"]; + fusermount -> kernel [label="open /dev/fuse"]; + fusermount -> kernel [label="mount(2)"]; + kernel ->> mounts [label="mount is visible"]; + fusermount <-- kernel [label="mount(2) returns"]; + fuse <<-- fusermount [diagonal, label="exit, receive /dev/fuse fd", leftnote="on Linux, successful exit here\nmeans the mount has happened,\nthough InitRequest might not have yet"]; + app <-- fuse [label="Mount returns\nConn.Ready is already closed"]; + + app -> fuse [label="fs.Serve"]; + fuse => kernel [label="read /dev/fuse fd", note="starts with InitRequest"]; + fuse -> app [label="Init"]; + fuse <-- app [color=red]; + fuse -> kernel [label="write /dev/fuse fd", color=red]; + kernel -> kernel [label="set connection\nstate to error", color=red]; + fuse <-- kernel; + ... conn.MountError == nil, so it is still mounted ... + ... call conn.Close to clean up ... +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux-error-init.seq.png b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux-error-init.seq.png new file mode 100644 index 0000000000000000000000000000000000000000..fea214f71f210fe41eb204cf925a9b8b81305c5d GIT binary patch literal 29163 zcmeFZ2~<;X66-5vmL9& zEeO8(WG#2@+`+J(M-OkD$PMn8Yj~ii_}6U8_8H?8??-l+Rwxd68_tP)8$M&5l|pgv zo*CDln2NWn@HQl+S$Mjt-L)EhXs1(t~9=GvRM!ZmEF( zAq5O!5nY&=3>QfW-eQJ~n)g9ME_|hye>3%$Dy^kPn2wbxG?0p2UM{63wST~>(VXzu zvb!-RrncMe>v+yoOZ)Jf)xiC+@4K+ihMlrNRn$l?DM@}8WUWPt=_ zDfKB_u?1V;y;_82dZW*#&8GugTwJELeGRpoalYeT?Ll2@b=z$#^rNzWRiAu8=AbSn zgw>6`#!()o?==g#4xfI{DGFpHRUhDc=QT<+tG730hg=$XM6g6QaepbtHH*t!xjYoemzY}h~BkT=q9%HtJz& zNZ%{BMu|fbHHWyJhkLq?p9l&L4sKjw|0Gwl(w#hVTINuLy-NIjQjTCO$s+Wfm9_du zsYFC2y17wVor9Z*?0Z5DW{sqxMo&-rWo*2le^muo^hF<(J@DKPjjM8#`4HLl1CDc! z2ps3wNNjFnd3OJN?*J*;`Kp4I`isbIjSfV1TYHdIMyk*2{*R;C>#zY0!I2yS%;q0+ zA;fkRDxiGrSlh(WsRlo0y!dtMpS~556;Pw^%~W-LErXP~%GaXxjmJg{Ut34}Fh!*P zcEY^hM^OG54?crGl~zH$s_4b{ZP9ZCKgT}E4lf|uxLKWjG1CKE zJ;YTs+d(~H#9!OrFxspOJGsC25o4VHc*P5^X3p2RgE0q%v$!Fs&ju|$+%wOP7`TTM z5FPzx(V^n?t0ls!=%H8s6-2`OZc&e0R$o?zl&)Jv+d0u@mdXKoMIRwi9L7FU+?2x; z-3y$lGRyp|KZ)jmafKohiO@gGPqJsIfm~F5YWiefG|`o9+}CaFOXYmje=l*QYWQZj z85>s(v4fx&tAk z-3&pZ0CX9F%i8}p4ZpxyXGI5DVdeg%oY!R47uvB`y1Ib zd28E?S!oA%U#t(2*pJTcc(SIP%OmpFCTXWZzqmfs{_UoMSELOeqTTBr)OC}xUl|Cg zdm@X%))4pg-VtAm9G~d@HXoB%thQIAO!QkMTSd|t)IV?2SYJ^{YVMTK+Wc;o7v6LY zT*LNX>C8W?`3B%<9RVITBWCWa+C^?lphLG)dnC-Smt=i}e`4 z$$_3Fv@|QMkkOk3gJN0NchDv3swa}sS-nRelm<~tAG2~KNo;PXeI8$oNW;=97K{#& z@jbC_6JHXZ&*x9?_1#UTJ7DB5k*x-^d-#Pig5i5qxrKVkC2hh*y6H&EibPm5TQ5yV zH%z)U=p!VA+|Rwi*AZo+I{w!jrLk2-!GA%^=HNg7)>qc*CgsAa<8OkQCV$yg6Hw*e zZCdu~cYZN3)sNEjeA69|{xCXYwlfQCYp45vBW|ZuL46&@N|t3| zom((Ab}SX^!B$s2!Mtape8v!%wNCv1z?P>Y-F#UeU-pq7DWa1{*<>jj`y7Y~0~sR8*8#Oj{6J_DRyYIA!0^CB|qKV^kz7V+>Ejs&Y7AukXN^*Y@vpaSmF0 zV++mL$f#8EDb`q)nw}o!>>L8!vfXBAt^QCtZ|S>LYEn!|>IPjBkl`=AMN-U3jH!vq zx4;_iN1C2~?epNdt0Yk^)^LN9izqDR5S8@6f1rld`IW{eJqDgs`BZ!4?|zuE3fItC_nWT3r@aEnT1?37J;{3yMfwlKyv z&ScWaG;lJE6qpDxmfeL>f5Qkh_$ZJYhOmI0YUrcGxYoPr=_AM2loUUH92y=HvTB@u z6uizc5{a~zEeOqV-ELnuv3nXr5HG7Gh%sIIB!`rRF$Y#pI@lu1!A1vJSm==>UxU{q z?{#)w8Hk~%k6G&Jn3{aJ-Nnd1VH)uES(qN&Yl~s7ZxyU!^j1_oBn-kRm(qF` zMQPMU8W|b26Z-zu^Zk(_LiZW?Z>m|!Nu&dvNBF=Y%Kq(LF?1> z^kVyoFAihsU$D*c<~67?Y>Je;37==|V;x4gatzgM#WunETz{`N0_H_SKPo-F8=TNe zDr-Ry(Zyf9D*qwj5jd3%7zZYFyGLBL-FR1+f80~8OssBU9mo@b6D|$bt#R+d^uFNJ zNQLF(FobEyr;&A9hUNGvQG)d^w(Ffduwq8F&G1rNFIX_Ql9P#ruiWZZwA&4=di=dj z-@p)mFN`|&NEIpBev82* z$&|i}jxh!)4J+(xSJ>yc8on;3;eM>0p>vhx)W@pVG@K^ds2{am%XU$WM~=B#uf+Fc zAsHRrhy>WvFPL5(HV6~41!Kj^FcT@k7*=C0|6P@rF>O@;TvfegTQ|eA=<n+arJbF!}l!ypNG@W1X8Y%&K$mA!>BHafG_wtwc>#v-W6y0pqiQ zk@2|it%qe;(id#!Z`idV%&-gIg?x1g`%y~R+z1Yo8Drl$GVw?|>qF4}@SRD6dFN$R z4SZXYEr&3NE^MR*Bcx!RHt-0P{i23E5_|XH*%PEX;!(+dhK7{e&WeKa{rSUq%pmhL>!WgBkgzy12#lB&;YU@ec%`)_UL%Y7vq^M)KX|0|NV>VP$q!$B2y{{i@5baBi5cT6&%_xNVqsJ9;Z-~Slb`n`!OQhvA% z-os2&MLXOlcF>*+!9%~fb4373TTtF`KOx3{A5$zPauFg~8|1cOJj~KuYUR6tgtHRLIe+91ji7NA7{V2cu4LZ!2_WlJCQ% zx?2%*o!`yZ+@JuABvOGkr8|``mSj#7_H_0JN^aN}@rw0Kr+lY?tjLxtloqfa&P6C2 zMpI|vI=I~rx}mxOk7cO2s#XVzW9|w#J6LHueme3RW`QPq`Uq1(%!}%1Swf^F_PnSp zi!sh1MaOfC{=&1NEx{_=lP7HHtHt(Jj*@;y#L&mvo40g12;x1?ISn#bnM(B#W!-nE z{}7~=U&NR|8_?^Tka<}EINMY=PP4)la)9f zMSSY+?jHOa8je zkgBoyKH-4V$JQa}mnQoB`QDZ4qA*ckvx`B>UGPhWRbe}?4⋙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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux.seq b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux.seq new file mode 100644 index 00000000..a1cafc7a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux.seq @@ -0,0 +1,41 @@ +seqdiag { + // seqdiag -T svg -o doc/mount-osx.svg doc/mount-osx.seq + app; + fuse [label="bazil.org/fuse"]; + fusermount; + kernel; + mounts; + + app -> fuse [label="Mount"]; + fuse -> fusermount [label="spawn, pass socketpair fd"]; + fusermount -> kernel [label="open /dev/fuse"]; + fusermount -> kernel [label="mount(2)"]; + kernel ->> mounts [label="mount is visible"]; + fusermount <-- kernel [label="mount(2) returns"]; + fuse <<-- fusermount [diagonal, label="exit, receive /dev/fuse fd", leftnote="on Linux, successful exit here\nmeans the mount has happened,\nthough InitRequest might not have yet"]; + app <-- fuse [label="Mount returns\nConn.Ready is already closed", rightnote="InitRequest and StatfsRequest\nmay or may not be seen\nbefore Conn.Ready,\ndepending on platform"]; + + app -> fuse [label="fs.Serve"]; + fuse => kernel [label="read /dev/fuse fd", note="starts with InitRequest"]; + fuse => app [label="FS/Node/Handle methods"]; + fuse => kernel [label="write /dev/fuse fd"]; + ... repeat ... + + ... shutting down ... + app -> fuse [label="Unmount"]; + fuse -> fusermount [label="fusermount -u"]; + fusermount -> kernel; + kernel <<-- mounts; + fusermount <-- kernel; + fuse <<-- fusermount [diagonal]; + app <-- fuse [label="Unmount returns"]; + + // actually triggers before above + fuse <<-- kernel [diagonal, label="/dev/fuse EOF"]; + app <-- fuse [label="fs.Serve returns"]; + + app -> fuse [label="conn.Close"]; + fuse -> kernel [label="close /dev/fuse fd"]; + fuse <-- kernel; + app <-- fuse; +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux.seq.png b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-linux.seq.png new file mode 100644 index 0000000000000000000000000000000000000000..af373dd284c52c60e65a9514bfc077785feef99f GIT binary patch literal 44615 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-osx-error-init.seq b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-osx-error-init.seq new file mode 100644 index 00000000..3bb2b39a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-osx-error-init.seq @@ -0,0 +1,32 @@ +seqdiag { + app; + fuse [label="bazil.org/fuse"]; + wait [label="callMount\nhelper goroutine"]; + mount_osxfusefs; + kernel; + + app -> fuse [label="Mount"]; + fuse -> kernel [label="open /dev/osxfuseN"]; + fuse -> mount_osxfusefs [label="spawn, pass fd"]; + fuse -> wait [label="goroutine", note="blocks on cmd.Wait"]; + app <-- fuse [label="Mount returns"]; + + mount_osxfusefs -> kernel [label="mount(2)"]; + + app -> fuse [label="fs.Serve"]; + fuse => kernel [label="read /dev/osxfuseN fd", note="starts with InitRequest,\nalso seen before mount exits:\ntwo StatfsRequest calls"]; + fuse -> app [label="Init"]; + fuse <-- app [color=red]; + fuse -> kernel [label="write /dev/osxfuseN fd", color=red]; + fuse <-- kernel; + + mount_osxfusefs <-- kernel [label="mount(2) returns", color=red]; + wait <<-- mount_osxfusefs [diagonal, label="exit", color=red]; + app <<-- wait [diagonal, label="mount has failed,\nclose Conn.Ready", color=red]; + + // actually triggers before above + fuse <<-- kernel [diagonal, label="/dev/osxfuseN EOF"]; + app <-- fuse [label="fs.Serve returns"]; + ... conn.MountError != nil, so it was was never mounted ... + ... call conn.Close to clean up ... +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-osx-error-init.seq.png b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-osx-error-init.seq.png new file mode 100644 index 0000000000000000000000000000000000000000..e96589c13452cba6d6e0d821ab0b487dbb4656a6 GIT binary patch literal 32618 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 fuse [label="Mount"]; + fuse -> kernel [label="open /dev/osxfuseN"]; + fuse -> mount_osxfusefs [label="spawn, pass fd"]; + fuse -> wait [label="goroutine", note="blocks on cmd.Wait"]; + app <-- fuse [label="Mount returns"]; + + mount_osxfusefs -> kernel [label="mount(2)"]; + + app -> fuse [label="fs.Serve"]; + fuse => kernel [label="read /dev/osxfuseN fd", note="starts with InitRequest,\nalso seen before mount exits:\ntwo StatfsRequest calls"]; + fuse => app [label="FS/Node/Handle methods"]; + fuse => kernel [label="write /dev/osxfuseN fd"]; + ... repeat ... + + kernel ->> mounts [label="mount is visible"]; + mount_osxfusefs <-- kernel [label="mount(2) returns"]; + wait <<-- mount_osxfusefs [diagonal, label="exit", leftnote="on OS X, successful exit\nhere means we finally know\nthe mount has happened\n(can't trust InitRequest,\nkernel might have timed out\nwaiting for InitResponse)"]; + + app <<-- wait [diagonal, label="mount is ready,\nclose Conn.Ready", rightnote="InitRequest and StatfsRequest\nmay or may not be seen\nbefore Conn.Ready,\ndepending on platform"]; + + ... shutting down ... + app -> fuse [label="Unmount"]; + fuse -> kernel [label="umount(2)"]; + kernel <<-- mounts; + fuse <-- kernel; + app <-- fuse [label="Unmount returns"]; + + // actually triggers before above + fuse <<-- kernel [diagonal, label="/dev/osxfuseN EOF"]; + app <-- fuse [label="fs.Serve returns"]; + + app -> fuse [label="conn.Close"]; + fuse -> kernel [label="close /dev/osxfuseN"]; + fuse <-- kernel; + app <-- fuse; +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-osx.seq.png b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/doc/mount-osx.seq.png new file mode 100644 index 0000000000000000000000000000000000000000..7e310f9144ba42896efc4ad1b124b823bb5cf15b GIT binary patch literal 51408 zcmeFZ2Ut_t+BUo?B37`BAW}rff`wv1krIU&tbl?DmJl>5QUXY%2!upMnNb8hqNp?- z1(Xs%ASfV-1yB*tp#+H#5HU&;Ite5s`JWy1oGE(V_q_l2eb@J2*Y9;aW0JksUVD|N z-1q&gUw7JXo;F!)GJ+t}wr=^!5kZt02%<_by_IjctZebJWp0CM3!aXhq*xy* z3A?ucr?J(m-X|RWXu{D}g03L5m+6$s|Irt(>@NSp4nh04@y-AL5gnedqtT5IZf`ym z-I_0E9Pdh+I8Ql?+<>u3m(E=^To-qbe9q_jFlGc<5vd+fTh*huc_x0U8AsFlxkPAl zTN<@pk|oo3l68}xH_%0TtA=x>Q9hrBG0h*;Lb1$>pC{{T7^xU*cBjHMQ?H9WovBOj<)v&U&KYWc=&F0&0EF#q%w_#QFS1fv8u}c_v0Viw9`>*G*NW#`& zT(s?N*5?zUfk8n*Zv(MKh3fA4n6XWgS$}>eV;IBB+PIA|Nk4L(Ut4%6iA1tWty^MO zA1%_L{LZ~SB(92Rka=D(DHr-N1+i~4oM3pqPB_}(jY@fGO9X4$ZVld|5nLdXVb3b+W^!hb54?J(| zV$lMbfzBc3nDS40QpaA6C?{E6UHYZ{YC3Nk*}YOhX$xB*>85p=?qP-1{%iSt8Sa}* z8X+}2qvGzb9LcZKbT5g>ZX(7pC)c79i9ucI=V`A?@O%8&WXU`BPq1Lo>4mj1FF-Pd zp;<=dI;!e-B{``Ys=dm)E8}>ZZ4BkLb05aO61o22K@X)9+|^%Q@Mvz34Hj-_G>)vA zd$n9E(=yHJn##RD4h>EZtO{el8AoV+J8;U|TyZFiMqmXe2M3Dr?qvx>%uc5yd|(Y#JjB5dX~fLEYc=d$%GTj=T`?2ux=0!p zYsqEHQ<145f<5F(mW|f!Y8@JjH=axp6}9$Gw|`6wXHW3gQ_OpkBXvIBYD6WlI`o?! z<>Q12JtXdc{@RE9vMkO%X~ZzMOVBh-W|H2nJzuj0X>afe%V`r>9j@n&(U{dFrQ3G7 zX{_4fdk$RRVh7wh0-F;t_xMvome*0HIH{3fTR@x~Gg?~d9bmTNG%gRDP{j5yw|M5Y zl0SHqFv{)9YPWorKG-uTvC^aM5tnDNvxH$T7{vYsjX8#OA}}S#54L*qtT^l;^J7KH zfqK8~sT-V~%TMfKrU%^=#tGN5BAUw7I^x5#*=yY{S1&-8U&FastYb;f2k<=SbFEYf z_xT!JeNH!PAIW0um14%nIOc2oTR}@{Ppon5+Xy<}q~(-)6Z11|pV=L1qjp`RNdz;* z&hUak+ys^>Z*I&%Cp+B@?=zK4Z5JbXZ8i}O*%Ca$yL_4A+L6%wg*~o$$ z(LtCz*i=;T*=mJwt7eeabW)D?d(jM*?n1iv%i_;P!7OjyErLz}uVgSs+UBZpU?o|0 zEtg*^RWGFKs2(I;>~+GIb{|U(6468?e;)@4I?#KS-6ya(3GYb06H6`j5G+6Bip4(c zVTEOpl5_=LeQo*J*p=$SA>qB@fNcA%Z#_12P?;ZWa0O}vS=?X`OX@T<@hTy!u_|*k zO3HieWY*NHHH@31(9G}=ZGVlQCW)HqGw)uf#~Ej?bY^+)(sC@>`2e#voxZJym|}Mu zDI6?>bCAXoP)lifwP#MQ*>#e^vRfkB4;o-?;e=~!4xaY3de z`U`5GH(S6Y(R)8tb)}{SVqCZ_Wp$k|^0LeKgw?1Zv>6ly#;lO+n@3X-{ynQ9OrSVQ zJzN3lQ%~Qhi2m^UlUbxgY{#t&ixURcBZ$AYE!?nhUrJ_i_@Y}_my7#@hkvu<`*`79 z$TR}GaPbFfXK|}=qN>WzHq}5@q42rb~P4k|9Ne^7|wP(!RyYR&!CGB7%5R6u=ekT(6*s1_(dT8vnTF zZyzJo4@FB73==Bb!S{TzFW}MM?&RW-%J#JQ5_M}0JkMHHzjFOu4TV2=C)DSiJ0Kg* zH}BJ4k(N4jr?a!2vom4ay$dGOyC=+79>$#L#!1w!i(F*javNpfaECF6ZK$%xD((_B zNn>N>IGjEho{gzyQFSu)3aGPQeOzaSqQX3sl}%)#@>6k@bVMtlIg z5tdmKVo`p*=BQt7ZLJ|^aCA+iI_lx-*W9LZyB#C?5=a9tme4UldRFH&@mmbm24dNE zi!XBr2M1j|JUkK#-ty1}?r}>To`^Ll3vkK}GO@e^T~0E;dom#mOs=##L&|oP^*YI( zX3CD7naT*2_|xMZ_cMoE;ofBQ;dOPm*0wf#8`Xmry6jh+K~LyF67i*3R1$HQcLqs1 z%As2Z>80$8&ev;uyE28FSWr+vcK1{DvP49w;94k_j&jViAf!`(T-6h*3QF|brs6*)**;A}3GE0S5zma~=Cd8aK z2z_u)jVLDUKdbKetIbC8Sf`#$jNa(_V@vCy2fbL#J0aT73l}c*&M_DP2bu2h%I2Yw zVmEn{B#S(Zvl^D^4%2T+8|fmWRP@=now768 z@L;UV%0=u7yJ*Rq*odJ4c-WS|tbF-=>BCo_S5S!3K%YhLsrDn~M9dfm> zw;^Y5%X58a6r~=p%^)Mbr9yi|I$0AdQ#6iLJ|IzpIZ74j%^8+i4P#bx%Shd1S9JZ# zAmR{CE5EDpBIrP<{x27@u+O^4RXs~$vt$Z55p(#dm007K-|KtT8)Q4t{UtO0=Ks$Y zTFch2h-kM75jRKfY>#tj`%HdLJaAy<`pCW>nf@->nLFrf&A%s`FpSxP&ZF8tPB z1j4+SW6d$9zyxKF`4 z!+&lbQ#3|Do?ra!k0u_-94Se6! z%YVIK#tGrsqyJ<$nJ7+$|5APf#{qwq-ax*7R57e<)+trWzfpH;W6-MeuLJj~)6<j)wGJ)J>%%ikfxWhn{JcCJ|faoXhFK1&bqR941VrHt7@?@?M%P>Ck}h`*uD#d#bc|wyl$%!Kn>L(Io6^ zdLlbGi*h_KVs}%OX7UesUfCA39W{NVbC=JJmk)dixnkM`_{Lvv&STE}tjA5=H?S<< z0#6oIlJ{%eY#0{O$ZORk3#TJ+U$ZTuGTTNSa%{#1MdO?6PoqRcO06QWJX&wL+0;!Qs0@^r

    c-|0Yh5nbnWe>Fq#RUjzyPdpQrTT8 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 0 +} + +// Flushes notes whether a FUSE Flush call has been seen. +type Flushes struct { + rec MarkRecorder +} + +var _ = fs.HandleFlusher(&Flushes{}) + +func (r *Flushes) Flush(req *fuse.FlushRequest, intr fs.Intr) fuse.Error { + r.rec.Mark() + return nil +} + +func (r *Flushes) RecordedFlush() bool { + return r.rec.Recorded() +} + +type Recorder struct { + mu sync.Mutex + val interface{} +} + +// Record that we've seen value. A nil value is indistinguishable from +// no value recorded. +func (r *Recorder) Record(value interface{}) { + r.mu.Lock() + r.val = value + r.mu.Unlock() +} + +func (r *Recorder) Recorded() interface{} { + r.mu.Lock() + val := r.val + r.mu.Unlock() + return val +} + +type RequestRecorder struct { + rec Recorder +} + +// Record a fuse.Request, after zeroing header fields that are hard to +// reproduce. +// +// Make sure to record a copy, not the original request. +func (r *RequestRecorder) RecordRequest(req fuse.Request) { + hdr := req.Hdr() + *hdr = fuse.Header{} + r.rec.Record(req) +} + +func (r *RequestRecorder) Recorded() fuse.Request { + val := r.rec.Recorded() + if val == nil { + return nil + } + return val.(fuse.Request) +} + +// Setattrs records a Setattr request and its fields. +type Setattrs struct { + rec RequestRecorder +} + +var _ = fs.NodeSetattrer(&Setattrs{}) + +func (r *Setattrs) Setattr(req *fuse.SetattrRequest, resp *fuse.SetattrResponse, intr fs.Intr) fuse.Error { + tmp := *req + r.rec.RecordRequest(&tmp) + return nil +} + +func (r *Setattrs) RecordedSetattr() fuse.SetattrRequest { + val := r.rec.Recorded() + if val == nil { + return fuse.SetattrRequest{} + } + return *(val.(*fuse.SetattrRequest)) +} + +// Fsyncs records an Fsync request and its fields. +type Fsyncs struct { + rec RequestRecorder +} + +var _ = fs.NodeFsyncer(&Fsyncs{}) + +func (r *Fsyncs) Fsync(req *fuse.FsyncRequest, intr fs.Intr) fuse.Error { + tmp := *req + r.rec.RecordRequest(&tmp) + return nil +} + +func (r *Fsyncs) RecordedFsync() fuse.FsyncRequest { + val := r.rec.Recorded() + if val == nil { + return fuse.FsyncRequest{} + } + return *(val.(*fuse.FsyncRequest)) +} + +// Mkdirs records a Mkdir request and its fields. +type Mkdirs struct { + rec RequestRecorder +} + +var _ = fs.NodeMkdirer(&Mkdirs{}) + +// Mkdir records the request and returns an error. Most callers should +// wrap this call in a function that returns a more useful result. +func (r *Mkdirs) Mkdir(req *fuse.MkdirRequest, intr fs.Intr) (fs.Node, fuse.Error) { + tmp := *req + r.rec.RecordRequest(&tmp) + return nil, fuse.EIO +} + +// RecordedMkdir returns information about the Mkdir request. +// If no request was seen, returns a zero value. +func (r *Mkdirs) RecordedMkdir() fuse.MkdirRequest { + val := r.rec.Recorded() + if val == nil { + return fuse.MkdirRequest{} + } + return *(val.(*fuse.MkdirRequest)) +} + +// Symlinks records a Symlink request and its fields. +type Symlinks struct { + rec RequestRecorder +} + +var _ = fs.NodeSymlinker(&Symlinks{}) + +// Symlink records the request and returns an error. Most callers should +// wrap this call in a function that returns a more useful result. +func (r *Symlinks) Symlink(req *fuse.SymlinkRequest, intr fs.Intr) (fs.Node, fuse.Error) { + tmp := *req + r.rec.RecordRequest(&tmp) + return nil, fuse.EIO +} + +// RecordedSymlink returns information about the Symlink request. +// If no request was seen, returns a zero value. +func (r *Symlinks) RecordedSymlink() fuse.SymlinkRequest { + val := r.rec.Recorded() + if val == nil { + return fuse.SymlinkRequest{} + } + return *(val.(*fuse.SymlinkRequest)) +} + +// Links records a Link request and its fields. +type Links struct { + rec RequestRecorder +} + +var _ = fs.NodeLinker(&Links{}) + +// Link records the request and returns an error. Most callers should +// wrap this call in a function that returns a more useful result. +func (r *Links) Link(req *fuse.LinkRequest, old fs.Node, intr fs.Intr) (fs.Node, fuse.Error) { + tmp := *req + r.rec.RecordRequest(&tmp) + return nil, fuse.EIO +} + +// RecordedLink returns information about the Link request. +// If no request was seen, returns a zero value. +func (r *Links) RecordedLink() fuse.LinkRequest { + val := r.rec.Recorded() + if val == nil { + return fuse.LinkRequest{} + } + return *(val.(*fuse.LinkRequest)) +} + +// Mknods records a Mknod request and its fields. +type Mknods struct { + rec RequestRecorder +} + +var _ = fs.NodeMknoder(&Mknods{}) + +// Mknod records the request and returns an error. Most callers should +// wrap this call in a function that returns a more useful result. +func (r *Mknods) Mknod(req *fuse.MknodRequest, intr fs.Intr) (fs.Node, fuse.Error) { + tmp := *req + r.rec.RecordRequest(&tmp) + return nil, fuse.EIO +} + +// RecordedMknod returns information about the Mknod request. +// If no request was seen, returns a zero value. +func (r *Mknods) RecordedMknod() fuse.MknodRequest { + val := r.rec.Recorded() + if val == nil { + return fuse.MknodRequest{} + } + return *(val.(*fuse.MknodRequest)) +} + +// Opens records a Open request and its fields. +type Opens struct { + rec RequestRecorder +} + +var _ = fs.NodeOpener(&Opens{}) + +// Open records the request and returns an error. Most callers should +// wrap this call in a function that returns a more useful result. +func (r *Opens) Open(req *fuse.OpenRequest, resp *fuse.OpenResponse, intr fs.Intr) (fs.Handle, fuse.Error) { + tmp := *req + r.rec.RecordRequest(&tmp) + return nil, fuse.EIO +} + +// RecordedOpen returns information about the Open request. +// If no request was seen, returns a zero value. +func (r *Opens) RecordedOpen() fuse.OpenRequest { + val := r.rec.Recorded() + if val == nil { + return fuse.OpenRequest{} + } + return *(val.(*fuse.OpenRequest)) +} + +// Getxattrs records a Getxattr request and its fields. +type Getxattrs struct { + rec RequestRecorder +} + +var _ = fs.NodeGetxattrer(&Getxattrs{}) + +// Getxattr records the request and returns an error. Most callers should +// wrap this call in a function that returns a more useful result. +func (r *Getxattrs) Getxattr(req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse, intr fs.Intr) fuse.Error { + tmp := *req + r.rec.RecordRequest(&tmp) + return fuse.ENODATA +} + +// RecordedGetxattr returns information about the Getxattr request. +// If no request was seen, returns a zero value. +func (r *Getxattrs) RecordedGetxattr() fuse.GetxattrRequest { + val := r.rec.Recorded() + if val == nil { + return fuse.GetxattrRequest{} + } + return *(val.(*fuse.GetxattrRequest)) +} + +// Listxattrs records a Listxattr request and its fields. +type Listxattrs struct { + rec RequestRecorder +} + +var _ = fs.NodeListxattrer(&Listxattrs{}) + +// Listxattr records the request and returns an error. Most callers should +// wrap this call in a function that returns a more useful result. +func (r *Listxattrs) Listxattr(req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse, intr fs.Intr) fuse.Error { + tmp := *req + r.rec.RecordRequest(&tmp) + return fuse.ENODATA +} + +// RecordedListxattr returns information about the Listxattr request. +// If no request was seen, returns a zero value. +func (r *Listxattrs) RecordedListxattr() fuse.ListxattrRequest { + val := r.rec.Recorded() + if val == nil { + return fuse.ListxattrRequest{} + } + return *(val.(*fuse.ListxattrRequest)) +} + +// Setxattrs records a Setxattr request and its fields. +type Setxattrs struct { + rec RequestRecorder +} + +var _ = fs.NodeSetxattrer(&Setxattrs{}) + +// Setxattr records the request and returns an error. Most callers should +// wrap this call in a function that returns a more useful result. +func (r *Setxattrs) Setxattr(req *fuse.SetxattrRequest, intr fs.Intr) fuse.Error { + tmp := *req + r.rec.RecordRequest(&tmp) + return nil +} + +// RecordedSetxattr returns information about the Setxattr request. +// If no request was seen, returns a zero value. +func (r *Setxattrs) RecordedSetxattr() fuse.SetxattrRequest { + val := r.rec.Recorded() + if val == nil { + return fuse.SetxattrRequest{} + } + return *(val.(*fuse.SetxattrRequest)) +} + +// Removexattrs records a Removexattr request and its fields. +type Removexattrs struct { + rec RequestRecorder +} + +var _ = fs.NodeRemovexattrer(&Removexattrs{}) + +// Removexattr records the request and returns an error. Most callers should +// wrap this call in a function that returns a more useful result. +func (r *Removexattrs) Removexattr(req *fuse.RemovexattrRequest, intr fs.Intr) fuse.Error { + tmp := *req + r.rec.RecordRequest(&tmp) + return nil +} + +// RecordedRemovexattr returns information about the Removexattr request. +// If no request was seen, returns a zero value. +func (r *Removexattrs) RecordedRemovexattr() fuse.RemovexattrRequest { + val := r.rec.Recorded() + if val == nil { + return fuse.RemovexattrRequest{} + } + return *(val.(*fuse.RemovexattrRequest)) +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/record/wait.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/record/wait.go new file mode 100644 index 00000000..040a91ae --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/record/wait.go @@ -0,0 +1,54 @@ +package record + +import ( + "sync" + "time" + + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs" +) + +type nothing struct{} + +// ReleaseWaiter notes whether a FUSE Release call has been seen. +// +// Releases are not guaranteed to happen synchronously with any client +// call, so they must be waited for. +type ReleaseWaiter struct { + once sync.Once + seen chan nothing +} + +var _ = fs.HandleReleaser(&ReleaseWaiter{}) + +func (r *ReleaseWaiter) init() { + r.once.Do(func() { + r.seen = make(chan nothing, 1) + }) +} + +func (r *ReleaseWaiter) Release(req *fuse.ReleaseRequest, intr fs.Intr) fuse.Error { + r.init() + close(r.seen) + return nil +} + +// WaitForRelease waits for Release to be called. +// +// With zero duration, wait forever. Otherwise, timeout early +// in a more controller way than `-test.timeout`. +// +// Returns whether a Release was seen. Always true if dur==0. +func (r *ReleaseWaiter) WaitForRelease(dur time.Duration) bool { + r.init() + var timeout <-chan time.Time + if dur > 0 { + timeout = time.After(dur) + } + select { + case <-r.seen: + return true + case <-timeout: + return false + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/testfs.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/testfs.go new file mode 100644 index 00000000..6081cfe2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/fstestutil/testfs.go @@ -0,0 +1,29 @@ +package fstestutil + +import ( + "os" + + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs" +) + +// SimpleFS is a trivial FS that just implements the Root method. +type SimpleFS struct { + Node fs.Node +} + +var _ = fs.FS(SimpleFS{}) + +func (f SimpleFS) Root() (fs.Node, fuse.Error) { + return f.Node, nil +} + +// File can be embedded in a struct to make it look like a file. +type File struct{} + +func (f File) Attr() fuse.Attr { return fuse.Attr{Mode: 0666} } + +// Dir can be embedded in a struct to make it look like a directory. +type Dir struct{} + +func (f Dir) Attr() fuse.Attr { return fuse.Attr{Mode: os.ModeDir | 0777} } diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/serve.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/serve.go new file mode 100644 index 00000000..b30455fc --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/serve.go @@ -0,0 +1,1317 @@ +// FUSE service loop, for servers that wish to use it. + +package fs + +import ( + "encoding/binary" + "fmt" + "hash/fnv" + "io" + "reflect" + "strings" + "sync" + "time" +) + +import ( + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fuseutil" +) + +const ( + attrValidTime = 1 * time.Minute + entryValidTime = 1 * time.Minute +) + +// TODO: FINISH DOCS + +// An Intr is a channel that signals that a request has been interrupted. +// Being able to receive from the channel means the request has been +// interrupted. +type Intr chan struct{} + +func (Intr) String() string { return "fuse.Intr" } + +// An FS is the interface required of a file system. +// +// Other FUSE requests can be handled by implementing methods from the +// FS* interfaces, for example FSIniter. +type FS interface { + // Root is called to obtain the Node for the file system root. + Root() (Node, fuse.Error) +} + +type FSIniter interface { + // Init is called to initialize the FUSE connection. + // It can inspect the request and adjust the response as desired. + // Init must return promptly. + Init(req *fuse.InitRequest, resp *fuse.InitResponse, intr Intr) fuse.Error +} + +type FSStatfser interface { + // Statfs is called to obtain file system metadata. + // It should write that data to resp. + Statfs(req *fuse.StatfsRequest, resp *fuse.StatfsResponse, intr Intr) fuse.Error +} + +type FSDestroyer interface { + // Destroy is called when the file system is shutting down. + // + // Linux only sends this request for block device backed (fuseblk) + // filesystems, to allow them to flush writes to disk before the + // unmount completes. + // + // On normal FUSE filesystems, use Forget of the root Node to + // do actions at unmount time. + Destroy() +} + +type FSInodeGenerator interface { + // GenerateInode is called to pick a dynamic inode number when it + // would otherwise be 0. + // + // Not all filesystems bother tracking inodes, but FUSE requires + // the inode to be set, and fewer duplicates in general makes UNIX + // tools work better. + // + // Operations where the nodes may return 0 inodes include Getattr, + // Setattr and ReadDir. + // + // If FS does not implement FSInodeGenerator, GenerateDynamicInode + // is used. + // + // Implementing this is useful to e.g. constrain the range of + // inode values used for dynamic inodes. + GenerateInode(parentInode uint64, name string) uint64 +} + +// A Node is the interface required of a file or directory. +// See the documentation for type FS for general information +// pertaining to all methods. +// +// Other FUSE requests can be handled by implementing methods from the +// Node* interfaces, for example NodeOpener. +type Node interface { + Attr() fuse.Attr +} + +type NodeGetattrer interface { + // Getattr obtains the standard metadata for the receiver. + // It should store that metadata in resp. + // + // If this method is not implemented, the attributes will be + // generated based on Attr(), with zero values filled in. + Getattr(req *fuse.GetattrRequest, resp *fuse.GetattrResponse, intr Intr) fuse.Error +} + +type NodeSetattrer interface { + // Setattr sets the standard metadata for the receiver. + Setattr(req *fuse.SetattrRequest, resp *fuse.SetattrResponse, intr Intr) fuse.Error +} + +type NodeSymlinker interface { + // Symlink creates a new symbolic link in the receiver, which must be a directory. + // + // TODO is the above true about directories? + Symlink(req *fuse.SymlinkRequest, intr Intr) (Node, fuse.Error) +} + +// This optional request will be called only for symbolic link nodes. +type NodeReadlinker interface { + // Readlink reads a symbolic link. + Readlink(req *fuse.ReadlinkRequest, intr Intr) (string, fuse.Error) +} + +type NodeLinker interface { + // Link creates a new directory entry in the receiver based on an + // existing Node. Receiver must be a directory. + Link(req *fuse.LinkRequest, old Node, intr Intr) (Node, fuse.Error) +} + +type NodeRemover interface { + // Remove removes the entry with the given name from + // the receiver, which must be a directory. The entry to be removed + // may correspond to a file (unlink) or to a directory (rmdir). + Remove(req *fuse.RemoveRequest, intr Intr) fuse.Error +} + +type NodeAccesser interface { + // Access checks whether the calling context has permission for + // the given operations on the receiver. If so, Access should + // return nil. If not, Access should return EPERM. + // + // Note that this call affects the result of the access(2) system + // call but not the open(2) system call. If Access is not + // implemented, the Node behaves as if it always returns nil + // (permission granted), relying on checks in Open instead. + Access(req *fuse.AccessRequest, intr Intr) fuse.Error +} + +type NodeStringLookuper interface { + // Lookup looks up a specific entry in the receiver, + // which must be a directory. Lookup should return a Node + // corresponding to the entry. If the name does not exist in + // the directory, Lookup should return nil, err. + // + // Lookup need not to handle the names "." and "..". + Lookup(name string, intr Intr) (Node, fuse.Error) +} + +type NodeRequestLookuper interface { + // Lookup looks up a specific entry in the receiver. + // See NodeStringLookuper for more. + Lookup(req *fuse.LookupRequest, resp *fuse.LookupResponse, intr Intr) (Node, fuse.Error) +} + +type NodeMkdirer interface { + Mkdir(req *fuse.MkdirRequest, intr Intr) (Node, fuse.Error) +} + +type NodeOpener interface { + // Open opens the receiver. + // XXX note about access. XXX OpenFlags. + // XXX note that the Node may be a file or directory. + Open(req *fuse.OpenRequest, resp *fuse.OpenResponse, intr Intr) (Handle, fuse.Error) +} + +type NodeCreater interface { + // Create creates a new directory entry in the receiver, which + // must be a directory. + Create(req *fuse.CreateRequest, resp *fuse.CreateResponse, intr Intr) (Node, Handle, fuse.Error) +} + +type NodeForgetter interface { + Forget() +} + +type NodeRenamer interface { + Rename(req *fuse.RenameRequest, newDir Node, intr Intr) fuse.Error +} + +type NodeMknoder interface { + Mknod(req *fuse.MknodRequest, intr Intr) (Node, fuse.Error) +} + +// TODO this should be on Handle not Node +type NodeFsyncer interface { + Fsync(req *fuse.FsyncRequest, intr Intr) fuse.Error +} + +type NodeGetxattrer interface { + // Getxattr gets an extended attribute by the given name from the + // node. + // + // If there is no xattr by that name, returns fuse.ENODATA. This + // will be translated to the platform-specific correct error code + // by the framework. + Getxattr(req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse, intr Intr) fuse.Error +} + +type NodeListxattrer interface { + // Listxattr lists the extended attributes recorded for the node. + Listxattr(req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse, intr Intr) fuse.Error +} + +type NodeSetxattrer interface { + // Setxattr sets an extended attribute with the given name and + // value for the node. + Setxattr(req *fuse.SetxattrRequest, intr Intr) fuse.Error +} + +type NodeRemovexattrer interface { + // Removexattr removes an extended attribute for the name. + // + // If there is no xattr by that name, returns fuse.ENODATA. This + // will be translated to the platform-specific correct error code + // by the framework. + Removexattr(req *fuse.RemovexattrRequest, intr Intr) fuse.Error +} + +var startTime = time.Now() + +func nodeAttr(n Node) (attr fuse.Attr) { + attr = n.Attr() + if attr.Nlink == 0 { + attr.Nlink = 1 + } + if attr.Atime.IsZero() { + attr.Atime = startTime + } + if attr.Mtime.IsZero() { + attr.Mtime = startTime + } + if attr.Ctime.IsZero() { + attr.Ctime = startTime + } + if attr.Crtime.IsZero() { + attr.Crtime = startTime + } + return +} + +// A Handle is the interface required of an opened file or directory. +// See the documentation for type FS for general information +// pertaining to all methods. +// +// Other FUSE requests can be handled by implementing methods from the +// Node* interfaces. The most common to implement are +// HandleReader, HandleReadDirer, and HandleWriter. +// +// TODO implement methods: Getlk, Setlk, Setlkw +type Handle interface { +} + +type HandleFlusher interface { + // Flush is called each time the file or directory is closed. + // Because there can be multiple file descriptors referring to a + // single opened file, Flush can be called multiple times. + Flush(req *fuse.FlushRequest, intr Intr) fuse.Error +} + +type HandleReadAller interface { + ReadAll(intr Intr) ([]byte, fuse.Error) +} + +type HandleReadDirer interface { + ReadDir(intrt Intr) ([]fuse.Dirent, fuse.Error) +} + +type HandleReader interface { + Read(req *fuse.ReadRequest, resp *fuse.ReadResponse, intr Intr) fuse.Error +} + +type HandleWriter interface { + Write(req *fuse.WriteRequest, resp *fuse.WriteResponse, intr Intr) fuse.Error +} + +type HandleReleaser interface { + Release(req *fuse.ReleaseRequest, intr Intr) fuse.Error +} + +type Server struct { + FS FS + + // Function to send debug log messages to. If nil, use fuse.Debug. + // Note that changing this or fuse.Debug may not affect existing + // calls to Serve. + // + // See fuse.Debug for the rules that log functions must follow. + Debug func(msg interface{}) +} + +// Serve serves the FUSE connection by making calls to the methods +// of fs and the Nodes and Handles it makes available. It returns only +// when the connection has been closed or an unexpected error occurs. +func (s *Server) Serve(c *fuse.Conn) error { + sc := serveConn{ + fs: s.FS, + debug: s.Debug, + req: map[fuse.RequestID]*serveRequest{}, + dynamicInode: GenerateDynamicInode, + } + if sc.debug == nil { + sc.debug = fuse.Debug + } + if dyn, ok := sc.fs.(FSInodeGenerator); ok { + sc.dynamicInode = dyn.GenerateInode + } + + root, err := sc.fs.Root() + if err != nil { + return fmt.Errorf("cannot obtain root node: %v", err) + } + sc.node = append(sc.node, nil, &serveNode{inode: 1, node: root, refs: 1}) + sc.handle = append(sc.handle, nil) + + for { + req, err := c.ReadRequest() + if err != nil { + if err == io.EOF { + break + } + return err + } + + go sc.serve(req) + } + return nil +} + +// Serve serves a FUSE connection with the default settings. See +// Server.Serve. +func Serve(c *fuse.Conn, fs FS) error { + server := Server{ + FS: fs, + } + return server.Serve(c) +} + +type nothing struct{} + +type serveConn struct { + meta sync.Mutex + fs FS + req map[fuse.RequestID]*serveRequest + node []*serveNode + handle []*serveHandle + freeNode []fuse.NodeID + freeHandle []fuse.HandleID + nodeGen uint64 + debug func(msg interface{}) + dynamicInode func(parent uint64, name string) uint64 +} + +type serveRequest struct { + Request fuse.Request + Intr Intr +} + +type serveNode struct { + inode uint64 + node Node + refs uint64 +} + +func (sn *serveNode) attr() (attr fuse.Attr) { + attr = nodeAttr(sn.node) + if attr.Inode == 0 { + attr.Inode = sn.inode + } + return +} + +type serveHandle struct { + handle Handle + readData []byte + nodeID fuse.NodeID +} + +// NodeRef can be embedded in a Node to recognize the same Node being +// returned from multiple Lookup, Create etc calls. +// +// Without this, each Node will get a new NodeID, causing spurious +// cache invalidations, extra lookups and aliasing anomalies. This may +// not matter for a simple, read-only filesystem. +type NodeRef struct { + id fuse.NodeID + generation uint64 +} + +// nodeRef is only ever accessed while holding serveConn.meta +func (n *NodeRef) nodeRef() *NodeRef { + return n +} + +type nodeRef interface { + nodeRef() *NodeRef +} + +func (c *serveConn) saveNode(inode uint64, node Node) (id fuse.NodeID, gen uint64) { + c.meta.Lock() + defer c.meta.Unlock() + + var ref *NodeRef + if nodeRef, ok := node.(nodeRef); ok { + ref = nodeRef.nodeRef() + + if ref.id != 0 { + // dropNode guarantees that NodeRef is zeroed at the same + // time as the NodeID is removed from serveConn.node, as + // guarded by c.meta; this means sn cannot be nil here + sn := c.node[ref.id] + sn.refs++ + return ref.id, ref.generation + } + } + + sn := &serveNode{inode: inode, node: node, refs: 1} + if n := len(c.freeNode); n > 0 { + id = c.freeNode[n-1] + c.freeNode = c.freeNode[:n-1] + c.node[id] = sn + c.nodeGen++ + } else { + id = fuse.NodeID(len(c.node)) + c.node = append(c.node, sn) + } + gen = c.nodeGen + if ref != nil { + ref.id = id + ref.generation = gen + } + return +} + +func (c *serveConn) saveHandle(handle Handle, nodeID fuse.NodeID) (id fuse.HandleID) { + c.meta.Lock() + shandle := &serveHandle{handle: handle, nodeID: nodeID} + if n := len(c.freeHandle); n > 0 { + id = c.freeHandle[n-1] + c.freeHandle = c.freeHandle[:n-1] + c.handle[id] = shandle + } else { + id = fuse.HandleID(len(c.handle)) + c.handle = append(c.handle, shandle) + } + c.meta.Unlock() + return +} + +type nodeRefcountDropBug struct { + N uint64 + Refs uint64 + Node fuse.NodeID +} + +func (n *nodeRefcountDropBug) String() string { + return fmt.Sprintf("bug: trying to drop %d of %d references to %v", n.N, n.Refs, n.Node) +} + +func (c *serveConn) dropNode(id fuse.NodeID, n uint64) (forget bool) { + c.meta.Lock() + defer c.meta.Unlock() + snode := c.node[id] + + if snode == nil { + // this should only happen if refcounts kernel<->us disagree + // *and* two ForgetRequests for the same node race each other; + // this indicates a bug somewhere + c.debug(nodeRefcountDropBug{N: n, Node: id}) + + // we may end up triggering Forget twice, but that's better + // than not even once, and that's the best we can do + return true + } + + if n > snode.refs { + c.debug(nodeRefcountDropBug{N: n, Refs: snode.refs, Node: id}) + n = snode.refs + } + + snode.refs -= n + if snode.refs == 0 { + c.node[id] = nil + if nodeRef, ok := snode.node.(nodeRef); ok { + ref := nodeRef.nodeRef() + *ref = NodeRef{} + } + c.freeNode = append(c.freeNode, id) + return true + } + return false +} + +func (c *serveConn) dropHandle(id fuse.HandleID) { + c.meta.Lock() + c.handle[id] = nil + c.freeHandle = append(c.freeHandle, id) + c.meta.Unlock() +} + +type missingHandle struct { + Handle fuse.HandleID + MaxHandle fuse.HandleID +} + +func (m missingHandle) String() string { + return fmt.Sprint("missing handle", m.Handle, m.MaxHandle) +} + +// Returns nil for invalid handles. +func (c *serveConn) getHandle(id fuse.HandleID) (shandle *serveHandle) { + c.meta.Lock() + defer c.meta.Unlock() + if id < fuse.HandleID(len(c.handle)) { + shandle = c.handle[uint(id)] + } + if shandle == nil { + c.debug(missingHandle{ + Handle: id, + MaxHandle: fuse.HandleID(len(c.handle)), + }) + } + return +} + +type request struct { + Op string + Request *fuse.Header + In interface{} `json:",omitempty"` +} + +func (r request) String() string { + return fmt.Sprintf("<- %s", r.In) +} + +type logResponseHeader struct { + ID fuse.RequestID +} + +func (m logResponseHeader) String() string { + return fmt.Sprintf("ID=%#x", m.ID) +} + +type response struct { + Op string + Request logResponseHeader + Out interface{} `json:",omitempty"` + // Errno contains the errno value as a string, for example "EPERM". + Errno string `json:",omitempty"` + // Error may contain a free form error message. + Error string `json:",omitempty"` +} + +func (r response) errstr() string { + s := r.Errno + if r.Error != "" { + // prefix the errno constant to the long form message + s = s + ": " + r.Error + } + return s +} + +func (r response) String() string { + switch { + case r.Errno != "" && r.Out != nil: + return fmt.Sprintf("-> %s error=%s %s", r.Request, r.errstr(), r.Out) + case r.Errno != "": + return fmt.Sprintf("-> %s error=%s", r.Request, r.errstr()) + case r.Out != nil: + // make sure (seemingly) empty values are readable + switch r.Out.(type) { + case string: + return fmt.Sprintf("-> %s %q", r.Request, r.Out) + case []byte: + return fmt.Sprintf("-> %s [% x]", r.Request, r.Out) + default: + return fmt.Sprintf("-> %s %s", r.Request, r.Out) + } + default: + return fmt.Sprintf("-> %s", r.Request) + } +} + +type logMissingNode struct { + MaxNode fuse.NodeID +} + +func opName(req fuse.Request) string { + t := reflect.Indirect(reflect.ValueOf(req)).Type() + s := t.Name() + s = strings.TrimSuffix(s, "Request") + return s +} + +type logLinkRequestOldNodeNotFound struct { + Request *fuse.Header + In *fuse.LinkRequest +} + +func (m *logLinkRequestOldNodeNotFound) String() string { + return fmt.Sprintf("In LinkRequest (request %#x), node %d not found", m.Request.Hdr().ID, m.In.OldNode) +} + +type renameNewDirNodeNotFound struct { + Request *fuse.Header + In *fuse.RenameRequest +} + +func (m *renameNewDirNodeNotFound) String() string { + return fmt.Sprintf("In RenameRequest (request %#x), node %d not found", m.Request.Hdr().ID, m.In.NewDir) +} + +func (c *serveConn) serve(r fuse.Request) { + intr := make(Intr) + req := &serveRequest{Request: r, Intr: intr} + + c.debug(request{ + Op: opName(r), + Request: r.Hdr(), + In: r, + }) + var node Node + var snode *serveNode + c.meta.Lock() + hdr := r.Hdr() + if id := hdr.Node; id != 0 { + if id < fuse.NodeID(len(c.node)) { + snode = c.node[uint(id)] + } + if snode == nil { + c.meta.Unlock() + c.debug(response{ + Op: opName(r), + Request: logResponseHeader{ID: hdr.ID}, + Error: fuse.ESTALE.ErrnoName(), + // this is the only place that sets both Error and + // Out; not sure if i want to do that; might get rid + // of len(c.node) things altogether + Out: logMissingNode{ + MaxNode: fuse.NodeID(len(c.node)), + }, + }) + r.RespondError(fuse.ESTALE) + return + } + node = snode.node + } + if c.req[hdr.ID] != nil { + // This happens with OSXFUSE. Assume it's okay and + // that we'll never see an interrupt for this one. + // Otherwise everything wedges. TODO: Report to OSXFUSE? + // + // TODO this might have been because of missing done() calls + intr = nil + } else { + c.req[hdr.ID] = req + } + c.meta.Unlock() + + // Call this before responding. + // After responding is too late: we might get another request + // with the same ID and be very confused. + done := func(resp interface{}) { + msg := response{ + Op: opName(r), + Request: logResponseHeader{ID: hdr.ID}, + } + if err, ok := resp.(error); ok { + msg.Error = err.Error() + if ferr, ok := err.(fuse.ErrorNumber); ok { + errno := ferr.Errno() + msg.Errno = errno.ErrnoName() + if errno == err { + // it's just a fuse.Errno with no extra detail; + // skip the textual message for log readability + msg.Error = "" + } + } else { + msg.Errno = fuse.DefaultErrno.ErrnoName() + } + } else { + msg.Out = resp + } + c.debug(msg) + + c.meta.Lock() + delete(c.req, hdr.ID) + c.meta.Unlock() + } + + switch r := r.(type) { + default: + // Note: To FUSE, ENOSYS means "this server never implements this request." + // It would be inappropriate to return ENOSYS for other operations in this + // switch that might only be unavailable in some contexts, not all. + done(fuse.ENOSYS) + r.RespondError(fuse.ENOSYS) + + // FS operations. + case *fuse.InitRequest: + s := &fuse.InitResponse{ + MaxWrite: 128 * 1024, + Flags: fuse.InitBigWrites, + } + if fs, ok := c.fs.(FSIniter); ok { + if err := fs.Init(r, s, intr); err != nil { + done(err) + r.RespondError(err) + break + } + } + done(s) + r.Respond(s) + + case *fuse.StatfsRequest: + s := &fuse.StatfsResponse{} + if fs, ok := c.fs.(FSStatfser); ok { + if err := fs.Statfs(r, s, intr); err != nil { + done(err) + r.RespondError(err) + break + } + } + done(s) + r.Respond(s) + + // Node operations. + case *fuse.GetattrRequest: + s := &fuse.GetattrResponse{} + if n, ok := node.(NodeGetattrer); ok { + if err := n.Getattr(r, s, intr); err != nil { + done(err) + r.RespondError(err) + break + } + } else { + s.AttrValid = attrValidTime + s.Attr = snode.attr() + } + done(s) + r.Respond(s) + + case *fuse.SetattrRequest: + s := &fuse.SetattrResponse{} + if n, ok := node.(NodeSetattrer); ok { + if err := n.Setattr(r, s, intr); err != nil { + done(err) + r.RespondError(err) + break + } + done(s) + r.Respond(s) + break + } + + if s.AttrValid == 0 { + s.AttrValid = attrValidTime + } + s.Attr = snode.attr() + done(s) + r.Respond(s) + + case *fuse.SymlinkRequest: + s := &fuse.SymlinkResponse{} + n, ok := node.(NodeSymlinker) + if !ok { + done(fuse.EIO) // XXX or EPERM like Mkdir? + r.RespondError(fuse.EIO) + break + } + n2, err := n.Symlink(r, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + c.saveLookup(&s.LookupResponse, snode, r.NewName, n2) + done(s) + r.Respond(s) + + case *fuse.ReadlinkRequest: + n, ok := node.(NodeReadlinker) + if !ok { + done(fuse.EIO) /// XXX or EPERM? + r.RespondError(fuse.EIO) + break + } + target, err := n.Readlink(r, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + done(target) + r.Respond(target) + + case *fuse.LinkRequest: + n, ok := node.(NodeLinker) + if !ok { + done(fuse.EIO) /// XXX or EPERM? + r.RespondError(fuse.EIO) + break + } + c.meta.Lock() + var oldNode *serveNode + if int(r.OldNode) < len(c.node) { + oldNode = c.node[r.OldNode] + } + c.meta.Unlock() + if oldNode == nil { + c.debug(logLinkRequestOldNodeNotFound{ + Request: r.Hdr(), + In: r, + }) + done(fuse.EIO) + r.RespondError(fuse.EIO) + break + } + n2, err := n.Link(r, oldNode.node, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + s := &fuse.LookupResponse{} + c.saveLookup(s, snode, r.NewName, n2) + done(s) + r.Respond(s) + + case *fuse.RemoveRequest: + n, ok := node.(NodeRemover) + if !ok { + done(fuse.EIO) /// XXX or EPERM? + r.RespondError(fuse.EIO) + break + } + err := n.Remove(r, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + done(nil) + r.Respond() + + case *fuse.AccessRequest: + if n, ok := node.(NodeAccesser); ok { + if err := n.Access(r, intr); err != nil { + done(err) + r.RespondError(err) + break + } + } + done(nil) + r.Respond() + + case *fuse.LookupRequest: + var n2 Node + var err fuse.Error + s := &fuse.LookupResponse{} + if n, ok := node.(NodeStringLookuper); ok { + n2, err = n.Lookup(r.Name, intr) + } else if n, ok := node.(NodeRequestLookuper); ok { + n2, err = n.Lookup(r, s, intr) + } else { + done(fuse.ENOENT) + r.RespondError(fuse.ENOENT) + break + } + if err != nil { + done(err) + r.RespondError(err) + break + } + c.saveLookup(s, snode, r.Name, n2) + done(s) + r.Respond(s) + + case *fuse.MkdirRequest: + s := &fuse.MkdirResponse{} + n, ok := node.(NodeMkdirer) + if !ok { + done(fuse.EPERM) + r.RespondError(fuse.EPERM) + break + } + n2, err := n.Mkdir(r, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + c.saveLookup(&s.LookupResponse, snode, r.Name, n2) + done(s) + r.Respond(s) + + case *fuse.OpenRequest: + s := &fuse.OpenResponse{} + var h2 Handle + if n, ok := node.(NodeOpener); ok { + hh, err := n.Open(r, s, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + h2 = hh + } else { + h2 = node + } + s.Handle = c.saveHandle(h2, hdr.Node) + done(s) + r.Respond(s) + + case *fuse.CreateRequest: + n, ok := node.(NodeCreater) + if !ok { + // If we send back ENOSYS, FUSE will try mknod+open. + done(fuse.EPERM) + r.RespondError(fuse.EPERM) + break + } + s := &fuse.CreateResponse{OpenResponse: fuse.OpenResponse{}} + n2, h2, err := n.Create(r, s, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + c.saveLookup(&s.LookupResponse, snode, r.Name, n2) + s.Handle = c.saveHandle(h2, hdr.Node) + done(s) + r.Respond(s) + + case *fuse.GetxattrRequest: + n, ok := node.(NodeGetxattrer) + if !ok { + done(fuse.ENOTSUP) + r.RespondError(fuse.ENOTSUP) + break + } + s := &fuse.GetxattrResponse{} + err := n.Getxattr(r, s, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + if r.Size != 0 && uint64(len(s.Xattr)) > uint64(r.Size) { + done(fuse.ERANGE) + r.RespondError(fuse.ERANGE) + break + } + done(s) + r.Respond(s) + + case *fuse.ListxattrRequest: + n, ok := node.(NodeListxattrer) + if !ok { + done(fuse.ENOTSUP) + r.RespondError(fuse.ENOTSUP) + break + } + s := &fuse.ListxattrResponse{} + err := n.Listxattr(r, s, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + if r.Size != 0 && uint64(len(s.Xattr)) > uint64(r.Size) { + done(fuse.ERANGE) + r.RespondError(fuse.ERANGE) + break + } + done(s) + r.Respond(s) + + case *fuse.SetxattrRequest: + n, ok := node.(NodeSetxattrer) + if !ok { + done(fuse.ENOTSUP) + r.RespondError(fuse.ENOTSUP) + break + } + err := n.Setxattr(r, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + done(nil) + r.Respond() + + case *fuse.RemovexattrRequest: + n, ok := node.(NodeRemovexattrer) + if !ok { + done(fuse.ENOTSUP) + r.RespondError(fuse.ENOTSUP) + break + } + err := n.Removexattr(r, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + done(nil) + r.Respond() + + case *fuse.ForgetRequest: + forget := c.dropNode(hdr.Node, r.N) + if forget { + n, ok := node.(NodeForgetter) + if ok { + n.Forget() + } + } + done(nil) + r.Respond() + + // Handle operations. + case *fuse.ReadRequest: + shandle := c.getHandle(r.Handle) + if shandle == nil { + done(fuse.ESTALE) + r.RespondError(fuse.ESTALE) + return + } + handle := shandle.handle + + s := &fuse.ReadResponse{Data: make([]byte, 0, r.Size)} + if r.Dir { + if h, ok := handle.(HandleReadDirer); ok { + if shandle.readData == nil { + dirs, err := h.ReadDir(intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + var data []byte + for _, dir := range dirs { + if dir.Inode == 0 { + dir.Inode = c.dynamicInode(snode.inode, dir.Name) + } + data = fuse.AppendDirent(data, dir) + } + shandle.readData = data + } + fuseutil.HandleRead(r, s, shandle.readData) + done(s) + r.Respond(s) + break + } + } else { + if h, ok := handle.(HandleReadAller); ok { + if shandle.readData == nil { + data, err := h.ReadAll(intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + if data == nil { + data = []byte{} + } + shandle.readData = data + } + fuseutil.HandleRead(r, s, shandle.readData) + done(s) + r.Respond(s) + break + } + h, ok := handle.(HandleReader) + if !ok { + fmt.Printf("NO READ FOR %T\n", handle) + done(fuse.EIO) + r.RespondError(fuse.EIO) + break + } + if err := h.Read(r, s, intr); err != nil { + done(err) + r.RespondError(err) + break + } + } + done(s) + r.Respond(s) + + case *fuse.WriteRequest: + shandle := c.getHandle(r.Handle) + if shandle == nil { + done(fuse.ESTALE) + r.RespondError(fuse.ESTALE) + return + } + + s := &fuse.WriteResponse{} + if h, ok := shandle.handle.(HandleWriter); ok { + if err := h.Write(r, s, intr); err != nil { + done(err) + r.RespondError(err) + break + } + done(s) + r.Respond(s) + break + } + done(fuse.EIO) + r.RespondError(fuse.EIO) + + case *fuse.FlushRequest: + shandle := c.getHandle(r.Handle) + if shandle == nil { + done(fuse.ESTALE) + r.RespondError(fuse.ESTALE) + return + } + handle := shandle.handle + + if h, ok := handle.(HandleFlusher); ok { + if err := h.Flush(r, intr); err != nil { + done(err) + r.RespondError(err) + break + } + } + done(nil) + r.Respond() + + case *fuse.ReleaseRequest: + shandle := c.getHandle(r.Handle) + if shandle == nil { + done(fuse.ESTALE) + r.RespondError(fuse.ESTALE) + return + } + handle := shandle.handle + + // No matter what, release the handle. + c.dropHandle(r.Handle) + + if h, ok := handle.(HandleReleaser); ok { + if err := h.Release(r, intr); err != nil { + done(err) + r.RespondError(err) + break + } + } + done(nil) + r.Respond() + + case *fuse.DestroyRequest: + if fs, ok := c.fs.(FSDestroyer); ok { + fs.Destroy() + } + done(nil) + r.Respond() + + case *fuse.RenameRequest: + c.meta.Lock() + var newDirNode *serveNode + if int(r.NewDir) < len(c.node) { + newDirNode = c.node[r.NewDir] + } + c.meta.Unlock() + if newDirNode == nil { + c.debug(renameNewDirNodeNotFound{ + Request: r.Hdr(), + In: r, + }) + done(fuse.EIO) + r.RespondError(fuse.EIO) + break + } + n, ok := node.(NodeRenamer) + if !ok { + done(fuse.EIO) // XXX or EPERM like Mkdir? + r.RespondError(fuse.EIO) + break + } + err := n.Rename(r, newDirNode.node, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + done(nil) + r.Respond() + + case *fuse.MknodRequest: + n, ok := node.(NodeMknoder) + if !ok { + done(fuse.EIO) + r.RespondError(fuse.EIO) + break + } + n2, err := n.Mknod(r, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + s := &fuse.LookupResponse{} + c.saveLookup(s, snode, r.Name, n2) + done(s) + r.Respond(s) + + case *fuse.FsyncRequest: + n, ok := node.(NodeFsyncer) + if !ok { + done(fuse.EIO) + r.RespondError(fuse.EIO) + break + } + err := n.Fsync(r, intr) + if err != nil { + done(err) + r.RespondError(err) + break + } + done(nil) + r.Respond() + + case *fuse.InterruptRequest: + c.meta.Lock() + ireq := c.req[r.IntrID] + if ireq != nil && ireq.Intr != nil { + close(ireq.Intr) + ireq.Intr = nil + } + c.meta.Unlock() + done(nil) + r.Respond() + + /* case *FsyncdirRequest: + done(ENOSYS) + r.RespondError(ENOSYS) + + case *GetlkRequest, *SetlkRequest, *SetlkwRequest: + done(ENOSYS) + r.RespondError(ENOSYS) + + case *BmapRequest: + done(ENOSYS) + r.RespondError(ENOSYS) + + case *SetvolnameRequest, *GetxtimesRequest, *ExchangeRequest: + done(ENOSYS) + r.RespondError(ENOSYS) + */ + } +} + +func (c *serveConn) saveLookup(s *fuse.LookupResponse, snode *serveNode, elem string, n2 Node) { + s.Attr = nodeAttr(n2) + if s.Attr.Inode == 0 { + s.Attr.Inode = c.dynamicInode(snode.inode, elem) + } + + s.Node, s.Generation = c.saveNode(s.Attr.Inode, n2) + if s.EntryValid == 0 { + s.EntryValid = entryValidTime + } + if s.AttrValid == 0 { + s.AttrValid = attrValidTime + } +} + +// DataHandle returns a read-only Handle that satisfies reads +// using the given data. +func DataHandle(data []byte) Handle { + return &dataHandle{data} +} + +type dataHandle struct { + data []byte +} + +func (d *dataHandle) ReadAll(intr Intr) ([]byte, fuse.Error) { + return d.data, nil +} + +// GenerateDynamicInode returns a dynamic inode. +// +// The parent inode and current entry name are used as the criteria +// for choosing a pseudorandom inode. This makes it likely the same +// entry will get the same inode on multiple runs. +func GenerateDynamicInode(parent uint64, name string) uint64 { + h := fnv.New64a() + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], parent) + _, _ = h.Write(buf[:]) + _, _ = h.Write([]byte(name)) + var inode uint64 + for { + inode = h.Sum64() + if inode != 0 { + break + } + // there's a tiny probability that result is zero; change the + // input a little and try again + _, _ = h.Write([]byte{'x'}) + } + return inode +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/serve_test.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/serve_test.go new file mode 100644 index 00000000..90f07b21 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/serve_test.go @@ -0,0 +1,1767 @@ +package fs_test + +import ( + "bytes" + "errors" + "flag" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "syscall" + "testing" + "time" + + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs" + "camlistore.org/third_party/bazil.org/fuse/fs/fstestutil" + "camlistore.org/third_party/bazil.org/fuse/fs/fstestutil/record" + "camlistore.org/third_party/bazil.org/fuse/fuseutil" + "camlistore.org/third_party/bazil.org/fuse/syscallx" +) + +// TO TEST: +// Lookup(*LookupRequest, *LookupResponse) +// Getattr(*GetattrRequest, *GetattrResponse) +// Attr with explicit inode +// Setattr(*SetattrRequest, *SetattrResponse) +// Access(*AccessRequest) +// Open(*OpenRequest, *OpenResponse) +// Write(*WriteRequest, *WriteResponse) +// Flush(*FlushRequest, *FlushResponse) + +func init() { + fstestutil.DebugByDefault() +} + +var childMode bool + +func init() { + flag.BoolVar(&childMode, "fuse.internal.childmode", false, "internal use only") +} + +// childCmd prepares a test function to be run in a subprocess, with +// childMode set to true. Caller must still call Run or Start. +// +// Re-using the test executable as the subprocess is useful because +// now test executables can e.g. be cross-compiled, transferred +// between hosts, and run in settings where the whole Go development +// environment is not installed. +func childCmd(testName string) (*exec.Cmd, error) { + // caller may set cwd, so we can't rely on relative paths + executable, err := filepath.Abs(os.Args[0]) + if err != nil { + return nil, err + } + testName = regexp.QuoteMeta(testName) + cmd := exec.Command(executable, "-test.run=^"+testName+"$", "-fuse.internal.childmode") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd, nil +} + +// childMapFS is an FS with one fixed child named "child". +type childMapFS map[string]fs.Node + +var _ = fs.FS(childMapFS{}) +var _ = fs.Node(childMapFS{}) +var _ = fs.NodeStringLookuper(childMapFS{}) + +func (f childMapFS) Attr() fuse.Attr { + return fuse.Attr{Inode: 1, Mode: os.ModeDir | 0777} +} + +func (f childMapFS) Root() (fs.Node, fuse.Error) { + return f, nil +} + +func (f childMapFS) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) { + child, ok := f[name] + if !ok { + return nil, fuse.ENOENT + } + return child, nil +} + +// symlink can be embedded in a struct to make it look like a symlink. +type symlink struct { + target string +} + +func (f symlink) Attr() fuse.Attr { return fuse.Attr{Mode: os.ModeSymlink | 0666} } + +// fifo can be embedded in a struct to make it look like a named pipe. +type fifo struct{} + +func (f fifo) Attr() fuse.Attr { return fuse.Attr{Mode: os.ModeNamedPipe | 0666} } + +type badRootFS struct{} + +func (badRootFS) Root() (fs.Node, fuse.Error) { + // pick a really distinct error, to identify it later + return nil, fuse.Errno(syscall.ENAMETOOLONG) +} + +func TestRootErr(t *testing.T) { + t.Parallel() + mnt, err := fstestutil.MountedT(t, badRootFS{}) + if err == nil { + // path for synchronous mounts (linux): started out fine, now + // wait for Serve to cycle through + err = <-mnt.Error + // without this, unmount will keep failing with EBUSY; nudge + // kernel into realizing InitResponse will not happen + mnt.Conn.Close() + mnt.Close() + } + + if err == nil { + t.Fatal("expected an error") + } + // TODO this should not be a textual comparison, Serve hides + // details + if err.Error() != "cannot obtain root node: file name too long" { + t.Errorf("Unexpected error: %v", err) + } +} + +type testStatFS struct{} + +func (f testStatFS) Root() (fs.Node, fuse.Error) { + return f, nil +} + +func (f testStatFS) Attr() fuse.Attr { + return fuse.Attr{Inode: 1, Mode: os.ModeDir | 0777} +} + +func (f testStatFS) Statfs(req *fuse.StatfsRequest, resp *fuse.StatfsResponse, int fs.Intr) fuse.Error { + resp.Blocks = 42 + resp.Files = 13 + return nil +} + +func TestStatfs(t *testing.T) { + t.Parallel() + mnt, err := fstestutil.MountedT(t, testStatFS{}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + { + var st syscall.Statfs_t + err = syscall.Statfs(mnt.Dir, &st) + if err != nil { + t.Errorf("Statfs failed: %v", err) + } + t.Logf("Statfs got: %#v", st) + if g, e := st.Blocks, uint64(42); g != e { + t.Errorf("got Blocks = %d; want %d", g, e) + } + if g, e := st.Files, uint64(13); g != e { + t.Errorf("got Files = %d; want %d", g, e) + } + } + + { + var st syscall.Statfs_t + f, err := os.Open(mnt.Dir) + if err != nil { + t.Errorf("Open for fstatfs failed: %v", err) + } + defer f.Close() + err = syscall.Fstatfs(int(f.Fd()), &st) + if err != nil { + t.Errorf("Fstatfs failed: %v", err) + } + t.Logf("Fstatfs got: %#v", st) + if g, e := st.Blocks, uint64(42); g != e { + t.Errorf("got Blocks = %d; want %d", g, e) + } + if g, e := st.Files, uint64(13); g != e { + t.Errorf("got Files = %d; want %d", g, e) + } + } + +} + +// Test Stat of root. + +type root struct{} + +func (f root) Root() (fs.Node, fuse.Error) { + return f, nil +} + +func (root) Attr() fuse.Attr { + return fuse.Attr{Inode: 1, Mode: os.ModeDir | 0555} +} + +func TestStatRoot(t *testing.T) { + t.Parallel() + mnt, err := fstestutil.MountedT(t, root{}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + fi, err := os.Stat(mnt.Dir) + if err != nil { + t.Fatalf("root getattr failed with %v", err) + } + mode := fi.Mode() + if (mode & os.ModeType) != os.ModeDir { + t.Errorf("root is not a directory: %#v", fi) + } + if mode.Perm() != 0555 { + t.Errorf("root has weird access mode: %v", mode.Perm()) + } + switch stat := fi.Sys().(type) { + case *syscall.Stat_t: + if stat.Ino != 1 { + t.Errorf("root has wrong inode: %v", stat.Ino) + } + if stat.Nlink != 1 { + t.Errorf("root has wrong link count: %v", stat.Nlink) + } + if stat.Uid != 0 { + t.Errorf("root has wrong uid: %d", stat.Uid) + } + if stat.Gid != 0 { + t.Errorf("root has wrong gid: %d", stat.Gid) + } + } +} + +// Test Read calling ReadAll. + +type readAll struct { + fstestutil.File +} + +const hi = "hello, world" + +func (readAll) Attr() fuse.Attr { + return fuse.Attr{ + Mode: 0666, + Size: uint64(len(hi)), + } +} + +func (readAll) ReadAll(intr fs.Intr) ([]byte, fuse.Error) { + return []byte(hi), nil +} + +func testReadAll(t *testing.T, path string) { + data, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("readAll: %v", err) + } + if string(data) != hi { + t.Errorf("readAll = %q, want %q", data, hi) + } +} + +func TestReadAll(t *testing.T) { + t.Parallel() + mnt, err := fstestutil.MountedT(t, childMapFS{"child": readAll{}}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + testReadAll(t, mnt.Dir+"/child") +} + +// Test Read. + +type readWithHandleRead struct { + fstestutil.File +} + +func (readWithHandleRead) Attr() fuse.Attr { + return fuse.Attr{ + Mode: 0666, + Size: uint64(len(hi)), + } +} + +func (readWithHandleRead) Read(req *fuse.ReadRequest, resp *fuse.ReadResponse, intr fs.Intr) fuse.Error { + fuseutil.HandleRead(req, resp, []byte(hi)) + return nil +} + +func TestReadAllWithHandleRead(t *testing.T) { + t.Parallel() + mnt, err := fstestutil.MountedT(t, childMapFS{"child": readWithHandleRead{}}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + testReadAll(t, mnt.Dir+"/child") +} + +// Test Release. + +type release struct { + fstestutil.File + record.ReleaseWaiter +} + +func TestRelease(t *testing.T) { + t.Parallel() + r := &release{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": r}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + f, err := os.Open(mnt.Dir + "/child") + if err != nil { + t.Fatal(err) + } + f.Close() + if !r.WaitForRelease(1 * time.Second) { + t.Error("Close did not Release in time") + } +} + +// Test Write calling basic Write, with an fsync thrown in too. + +type write struct { + fstestutil.File + record.Writes + record.Fsyncs +} + +func TestWrite(t *testing.T) { + t.Parallel() + w := &write{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": w}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + f, err := os.Create(mnt.Dir + "/child") + if err != nil { + t.Fatalf("Create: %v", err) + } + defer f.Close() + n, err := f.Write([]byte(hi)) + if err != nil { + t.Fatalf("Write: %v", err) + } + if n != len(hi) { + t.Fatalf("short write; n=%d; hi=%d", n, len(hi)) + } + + err = syscall.Fsync(int(f.Fd())) + if err != nil { + t.Fatalf("Fsync = %v", err) + } + if w.RecordedFsync() == (fuse.FsyncRequest{}) { + t.Errorf("never received expected fsync call") + } + + err = f.Close() + if err != nil { + t.Fatalf("Close: %v", err) + } + + if got := string(w.RecordedWriteData()); got != hi { + t.Errorf("write = %q, want %q", got, hi) + } +} + +// Test Write of a larger buffer. + +type writeLarge struct { + fstestutil.File + record.Writes +} + +func TestWriteLarge(t *testing.T) { + t.Parallel() + w := &write{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": w}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + f, err := os.Create(mnt.Dir + "/child") + if err != nil { + t.Fatalf("Create: %v", err) + } + defer f.Close() + const one = "xyzzyfoo" + large := bytes.Repeat([]byte(one), 8192) + n, err := f.Write(large) + if err != nil { + t.Fatalf("Write: %v", err) + } + if g, e := n, len(large); g != e { + t.Fatalf("short write: %d != %d", g, e) + } + + err = f.Close() + if err != nil { + t.Fatalf("Close: %v", err) + } + + got := w.RecordedWriteData() + if g, e := len(got), len(large); g != e { + t.Errorf("write wrong length: %d != %d", g, e) + } + if g := strings.Replace(string(got), one, "", -1); g != "" { + t.Errorf("write wrong data: expected repeats of %q, also got %q", one, g) + } +} + +// Test Write calling Setattr+Write+Flush. + +type writeTruncateFlush struct { + fstestutil.File + record.Writes + record.Setattrs + record.Flushes +} + +func TestWriteTruncateFlush(t *testing.T) { + t.Parallel() + w := &writeTruncateFlush{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": w}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + err = ioutil.WriteFile(mnt.Dir+"/child", []byte(hi), 0666) + if err != nil { + t.Fatalf("WriteFile: %v", err) + } + if w.RecordedSetattr() == (fuse.SetattrRequest{}) { + t.Errorf("writeTruncateFlush expected Setattr") + } + if !w.RecordedFlush() { + t.Errorf("writeTruncateFlush expected Setattr") + } + if got := string(w.RecordedWriteData()); got != hi { + t.Errorf("writeTruncateFlush = %q, want %q", got, hi) + } +} + +// Test Mkdir. + +type mkdir1 struct { + fstestutil.Dir + record.Mkdirs +} + +func (f *mkdir1) Mkdir(req *fuse.MkdirRequest, intr fs.Intr) (fs.Node, fuse.Error) { + f.Mkdirs.Mkdir(req, intr) + return &mkdir1{}, nil +} + +func TestMkdir(t *testing.T) { + t.Parallel() + f := &mkdir1{} + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + // uniform umask needed to make os.Mkdir's mode into something + // reproducible + defer syscall.Umask(syscall.Umask(0022)) + err = os.Mkdir(mnt.Dir+"/foo", 0771) + if err != nil { + t.Fatalf("mkdir: %v", err) + } + want := fuse.MkdirRequest{Name: "foo", Mode: os.ModeDir | 0751} + if g, e := f.RecordedMkdir(), want; g != e { + t.Errorf("mkdir saw %+v, want %+v", g, e) + } +} + +// Test Create (and fsync) + +type create1file struct { + fstestutil.File + record.Fsyncs +} + +type create1 struct { + fstestutil.Dir + f create1file +} + +func (f *create1) Create(req *fuse.CreateRequest, resp *fuse.CreateResponse, intr fs.Intr) (fs.Node, fs.Handle, fuse.Error) { + if req.Name != "foo" { + log.Printf("ERROR create1.Create unexpected name: %q\n", req.Name) + return nil, nil, fuse.EPERM + } + flags := req.Flags + + // OS X does not pass O_TRUNC here, Linux does; as this is a + // Create, that's acceptable + flags &^= fuse.OpenTruncate + + if runtime.GOOS == "linux" { + // Linux <3.7 accidentally leaks O_CLOEXEC through to FUSE; + // avoid spurious test failures + flags &^= fuse.OpenFlags(syscall.O_CLOEXEC) + } + + if g, e := flags, fuse.OpenReadWrite|fuse.OpenCreate; g != e { + log.Printf("ERROR create1.Create unexpected flags: %v != %v\n", g, e) + return nil, nil, fuse.EPERM + } + if g, e := req.Mode, os.FileMode(0644); g != e { + log.Printf("ERROR create1.Create unexpected mode: %v != %v\n", g, e) + return nil, nil, fuse.EPERM + } + return &f.f, &f.f, nil +} + +func TestCreate(t *testing.T) { + t.Parallel() + f := &create1{} + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + // uniform umask needed to make os.Create's 0666 into something + // reproducible + defer syscall.Umask(syscall.Umask(0022)) + ff, err := os.Create(mnt.Dir + "/foo") + if err != nil { + t.Fatalf("create1 WriteFile: %v", err) + } + defer ff.Close() + + err = syscall.Fsync(int(ff.Fd())) + if err != nil { + t.Fatalf("Fsync = %v", err) + } + + if f.f.RecordedFsync() == (fuse.FsyncRequest{}) { + t.Errorf("never received expected fsync call") + } + + ff.Close() +} + +// Test Create + Write + Remove + +type create3file struct { + fstestutil.File + record.Writes +} + +type create3 struct { + fstestutil.Dir + f create3file + fooCreated record.MarkRecorder + fooRemoved record.MarkRecorder +} + +func (f *create3) Create(req *fuse.CreateRequest, resp *fuse.CreateResponse, intr fs.Intr) (fs.Node, fs.Handle, fuse.Error) { + if req.Name != "foo" { + log.Printf("ERROR create3.Create unexpected name: %q\n", req.Name) + return nil, nil, fuse.EPERM + } + f.fooCreated.Mark() + return &f.f, &f.f, nil +} + +func (f *create3) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) { + if f.fooCreated.Recorded() && !f.fooRemoved.Recorded() && name == "foo" { + return &f.f, nil + } + return nil, fuse.ENOENT +} + +func (f *create3) Remove(r *fuse.RemoveRequest, intr fs.Intr) fuse.Error { + if f.fooCreated.Recorded() && !f.fooRemoved.Recorded() && + r.Name == "foo" && !r.Dir { + f.fooRemoved.Mark() + return nil + } + return fuse.ENOENT +} + +func TestCreateWriteRemove(t *testing.T) { + t.Parallel() + f := &create3{} + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + err = ioutil.WriteFile(mnt.Dir+"/foo", []byte(hi), 0666) + if err != nil { + t.Fatalf("create3 WriteFile: %v", err) + } + if got := string(f.f.RecordedWriteData()); got != hi { + t.Fatalf("create3 write = %q, want %q", got, hi) + } + + err = os.Remove(mnt.Dir + "/foo") + if err != nil { + t.Fatalf("Remove: %v", err) + } + err = os.Remove(mnt.Dir + "/foo") + if err == nil { + t.Fatalf("second Remove = nil; want some error") + } +} + +// Test symlink + readlink + +// is a Node that is a symlink to target +type symlink1link struct { + symlink + target string +} + +func (f symlink1link) Readlink(*fuse.ReadlinkRequest, fs.Intr) (string, fuse.Error) { + return f.target, nil +} + +type symlink1 struct { + fstestutil.Dir + record.Symlinks +} + +func (f *symlink1) Symlink(req *fuse.SymlinkRequest, intr fs.Intr) (fs.Node, fuse.Error) { + f.Symlinks.Symlink(req, intr) + return symlink1link{target: req.Target}, nil +} + +func TestSymlink(t *testing.T) { + t.Parallel() + f := &symlink1{} + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + const target = "/some-target" + + err = os.Symlink(target, mnt.Dir+"/symlink.file") + if err != nil { + t.Fatalf("os.Symlink: %v", err) + } + + want := fuse.SymlinkRequest{NewName: "symlink.file", Target: target} + if g, e := f.RecordedSymlink(), want; g != e { + t.Errorf("symlink saw %+v, want %+v", g, e) + } + + gotName, err := os.Readlink(mnt.Dir + "/symlink.file") + if err != nil { + t.Fatalf("os.Readlink: %v", err) + } + if gotName != target { + t.Errorf("os.Readlink = %q; want %q", gotName, target) + } +} + +// Test link + +type link1 struct { + fstestutil.Dir + record.Links +} + +func (f *link1) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) { + if name == "old" { + return fstestutil.File{}, nil + } + return nil, fuse.ENOENT +} + +func (f *link1) Link(r *fuse.LinkRequest, old fs.Node, intr fs.Intr) (fs.Node, fuse.Error) { + f.Links.Link(r, old, intr) + return fstestutil.File{}, nil +} + +func TestLink(t *testing.T) { + t.Parallel() + f := &link1{} + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + err = os.Link(mnt.Dir+"/old", mnt.Dir+"/new") + if err != nil { + t.Fatalf("Link: %v", err) + } + + got := f.RecordedLink() + want := fuse.LinkRequest{ + NewName: "new", + // unpredictable + OldNode: got.OldNode, + } + if g, e := got, want; g != e { + t.Fatalf("link saw %+v, want %+v", g, e) + } +} + +// Test Rename + +type rename1 struct { + fstestutil.Dir + renamed record.Counter +} + +func (f *rename1) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) { + if name == "old" { + return fstestutil.File{}, nil + } + return nil, fuse.ENOENT +} + +func (f *rename1) Rename(r *fuse.RenameRequest, newDir fs.Node, intr fs.Intr) fuse.Error { + if r.OldName == "old" && r.NewName == "new" && newDir == f { + f.renamed.Inc() + return nil + } + return fuse.EIO +} + +func TestRename(t *testing.T) { + t.Parallel() + f := &rename1{} + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + err = os.Rename(mnt.Dir+"/old", mnt.Dir+"/new") + if err != nil { + t.Fatalf("Rename: %v", err) + } + if g, e := f.renamed.Count(), uint32(1); g != e { + t.Fatalf("expected rename didn't happen: %d != %d", g, e) + } + err = os.Rename(mnt.Dir+"/old2", mnt.Dir+"/new2") + if err == nil { + t.Fatal("expected error on second Rename; got nil") + } +} + +// Test mknod + +type mknod1 struct { + fstestutil.Dir + record.Mknods +} + +func (f *mknod1) Mknod(r *fuse.MknodRequest, intr fs.Intr) (fs.Node, fuse.Error) { + f.Mknods.Mknod(r, intr) + return fifo{}, nil +} + +func TestMknod(t *testing.T) { + t.Parallel() + if os.Getuid() != 0 { + t.Skip("skipping unless root") + } + + f := &mknod1{} + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + defer syscall.Umask(syscall.Umask(0)) + err = syscall.Mknod(mnt.Dir+"/node", syscall.S_IFIFO|0666, 123) + if err != nil { + t.Fatalf("Mknod: %v", err) + } + + want := fuse.MknodRequest{ + Name: "node", + Mode: os.FileMode(os.ModeNamedPipe | 0666), + Rdev: uint32(123), + } + if runtime.GOOS == "linux" { + // Linux fuse doesn't echo back the rdev if the node + // isn't a device (we're using a FIFO here, as that + // bit is portable.) + want.Rdev = 0 + } + if g, e := f.RecordedMknod(), want; g != e { + t.Fatalf("mknod saw %+v, want %+v", g, e) + } +} + +// Test Read served with DataHandle. + +type dataHandleTest struct { + fstestutil.File +} + +func (dataHandleTest) Attr() fuse.Attr { + return fuse.Attr{ + Mode: 0666, + Size: uint64(len(hi)), + } +} + +func (dataHandleTest) Open(*fuse.OpenRequest, *fuse.OpenResponse, fs.Intr) (fs.Handle, fuse.Error) { + return fs.DataHandle([]byte(hi)), nil +} + +func TestDataHandle(t *testing.T) { + t.Parallel() + f := &dataHandleTest{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + data, err := ioutil.ReadFile(mnt.Dir + "/child") + if err != nil { + t.Errorf("readAll: %v", err) + return + } + if string(data) != hi { + t.Errorf("readAll = %q, want %q", data, hi) + } +} + +// Test interrupt + +type interrupt struct { + fstestutil.File + + // strobes to signal we have a read hanging + hanging chan struct{} +} + +func (interrupt) Attr() fuse.Attr { + return fuse.Attr{ + Mode: 0666, + Size: 1, + } +} + +func (it *interrupt) Read(req *fuse.ReadRequest, resp *fuse.ReadResponse, intr fs.Intr) fuse.Error { + select { + case it.hanging <- struct{}{}: + default: + } + <-intr + return fuse.EINTR +} + +func TestInterrupt(t *testing.T) { + t.Parallel() + f := &interrupt{} + f.hanging = make(chan struct{}, 1) + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + // start a subprocess that can hang until signaled + cmd := exec.Command("cat", mnt.Dir+"/child") + + err = cmd.Start() + if err != nil { + t.Errorf("interrupt: cannot start cat: %v", err) + return + } + + // try to clean up if child is still alive when returning + defer cmd.Process.Kill() + + // wait till we're sure it's hanging in read + <-f.hanging + + err = cmd.Process.Signal(os.Interrupt) + if err != nil { + t.Errorf("interrupt: cannot interrupt cat: %v", err) + return + } + + p, err := cmd.Process.Wait() + if err != nil { + t.Errorf("interrupt: cat bork: %v", err) + return + } + switch ws := p.Sys().(type) { + case syscall.WaitStatus: + if ws.CoreDump() { + t.Errorf("interrupt: didn't expect cat to dump core: %v", ws) + } + + if ws.Exited() { + t.Errorf("interrupt: didn't expect cat to exit normally: %v", ws) + } + + if !ws.Signaled() { + t.Errorf("interrupt: expected cat to get a signal: %v", ws) + } else { + if ws.Signal() != os.Interrupt { + t.Errorf("interrupt: cat got wrong signal: %v", ws) + } + } + default: + t.Logf("interrupt: this platform has no test coverage") + } +} + +// Test truncate + +type truncate struct { + fstestutil.File + record.Setattrs +} + +func testTruncate(t *testing.T, toSize int64) { + t.Parallel() + f := &truncate{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + err = os.Truncate(mnt.Dir+"/child", toSize) + if err != nil { + t.Fatalf("Truncate: %v", err) + } + gotr := f.RecordedSetattr() + if gotr == (fuse.SetattrRequest{}) { + t.Fatalf("no recorded SetattrRequest") + } + if g, e := gotr.Size, uint64(toSize); g != e { + t.Errorf("got Size = %q; want %q", g, e) + } + if g, e := gotr.Valid&^fuse.SetattrLockOwner, fuse.SetattrSize; g != e { + t.Errorf("got Valid = %q; want %q", g, e) + } + t.Logf("Got request: %#v", gotr) +} + +func TestTruncate42(t *testing.T) { + testTruncate(t, 42) +} + +func TestTruncate0(t *testing.T) { + testTruncate(t, 0) +} + +// Test ftruncate + +type ftruncate struct { + fstestutil.File + record.Setattrs +} + +func testFtruncate(t *testing.T, toSize int64) { + t.Parallel() + f := &ftruncate{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + { + fil, err := os.OpenFile(mnt.Dir+"/child", os.O_WRONLY, 0666) + if err != nil { + t.Error(err) + return + } + defer fil.Close() + + err = fil.Truncate(toSize) + if err != nil { + t.Fatalf("Ftruncate: %v", err) + } + } + gotr := f.RecordedSetattr() + if gotr == (fuse.SetattrRequest{}) { + t.Fatalf("no recorded SetattrRequest") + } + if g, e := gotr.Size, uint64(toSize); g != e { + t.Errorf("got Size = %q; want %q", g, e) + } + if g, e := gotr.Valid&^fuse.SetattrLockOwner, fuse.SetattrHandle|fuse.SetattrSize; g != e { + t.Errorf("got Valid = %q; want %q", g, e) + } + t.Logf("Got request: %#v", gotr) +} + +func TestFtruncate42(t *testing.T) { + testFtruncate(t, 42) +} + +func TestFtruncate0(t *testing.T) { + testFtruncate(t, 0) +} + +// Test opening existing file truncates + +type truncateWithOpen struct { + fstestutil.File + record.Setattrs +} + +func TestTruncateWithOpen(t *testing.T) { + t.Parallel() + f := &truncateWithOpen{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + fil, err := os.OpenFile(mnt.Dir+"/child", os.O_WRONLY|os.O_TRUNC, 0666) + if err != nil { + t.Error(err) + return + } + fil.Close() + + gotr := f.RecordedSetattr() + if gotr == (fuse.SetattrRequest{}) { + t.Fatalf("no recorded SetattrRequest") + } + if g, e := gotr.Size, uint64(0); g != e { + t.Errorf("got Size = %q; want %q", g, e) + } + // osxfuse sets SetattrHandle here, linux does not + if g, e := gotr.Valid&^(fuse.SetattrLockOwner|fuse.SetattrHandle), fuse.SetattrSize; g != e { + t.Errorf("got Valid = %q; want %q", g, e) + } + t.Logf("Got request: %#v", gotr) +} + +// Test readdir + +type readdir struct { + fstestutil.Dir +} + +func (d *readdir) ReadDir(intr fs.Intr) ([]fuse.Dirent, fuse.Error) { + return []fuse.Dirent{ + {Name: "one", Inode: 11, Type: fuse.DT_Dir}, + {Name: "three", Inode: 13}, + {Name: "two", Inode: 12, Type: fuse.DT_File}, + }, nil +} + +func TestReadDir(t *testing.T) { + t.Parallel() + f := &readdir{} + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + fil, err := os.Open(mnt.Dir) + if err != nil { + t.Error(err) + return + } + defer fil.Close() + + // go Readdir is just Readdirnames + Lstat, there's no point in + // testing that here; we have no consumption API for the real + // dirent data + names, err := fil.Readdirnames(100) + if err != nil { + t.Error(err) + return + } + + t.Logf("Got readdir: %q", names) + + if len(names) != 3 || + names[0] != "one" || + names[1] != "three" || + names[2] != "two" { + t.Errorf(`expected 3 entries of "one", "three", "two", got: %q`, names) + return + } +} + +// Test Chmod. + +type chmod struct { + fstestutil.File + record.Setattrs +} + +func (f *chmod) Setattr(req *fuse.SetattrRequest, resp *fuse.SetattrResponse, intr fs.Intr) fuse.Error { + if !req.Valid.Mode() { + log.Printf("setattr not a chmod: %v", req.Valid) + return fuse.EIO + } + f.Setattrs.Setattr(req, resp, intr) + return nil +} + +func TestChmod(t *testing.T) { + t.Parallel() + f := &chmod{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + err = os.Chmod(mnt.Dir+"/child", 0764) + if err != nil { + t.Errorf("chmod: %v", err) + return + } + got := f.RecordedSetattr() + if g, e := got.Mode, os.FileMode(0764); g != e { + t.Errorf("wrong mode: %v != %v", g, e) + } +} + +// Test open + +type open struct { + fstestutil.File + record.Opens +} + +func (f *open) Open(req *fuse.OpenRequest, resp *fuse.OpenResponse, intr fs.Intr) (fs.Handle, fuse.Error) { + f.Opens.Open(req, resp, intr) + // pick a really distinct error, to identify it later + return nil, fuse.Errno(syscall.ENAMETOOLONG) + +} + +func TestOpen(t *testing.T) { + t.Parallel() + f := &open{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + // node: mode only matters with O_CREATE + fil, err := os.OpenFile(mnt.Dir+"/child", os.O_WRONLY|os.O_APPEND, 0) + if err == nil { + t.Error("Open err == nil, expected ENAMETOOLONG") + fil.Close() + return + } + + switch err2 := err.(type) { + case *os.PathError: + if err2.Err == syscall.ENAMETOOLONG { + break + } + t.Errorf("unexpected inner error: %#v", err2) + default: + t.Errorf("unexpected error: %v", err) + } + + want := fuse.OpenRequest{Dir: false, Flags: fuse.OpenWriteOnly | fuse.OpenAppend} + if runtime.GOOS == "darwin" { + // osxfuse does not let O_APPEND through at all + // + // https://code.google.com/p/macfuse/issues/detail?id=233 + // https://code.google.com/p/macfuse/issues/detail?id=132 + // https://code.google.com/p/macfuse/issues/detail?id=133 + want.Flags &^= fuse.OpenAppend + } + got := f.RecordedOpen() + + if runtime.GOOS == "linux" { + // Linux <3.7 accidentally leaks O_CLOEXEC through to FUSE; + // avoid spurious test failures + got.Flags &^= fuse.OpenFlags(syscall.O_CLOEXEC) + } + + if g, e := got, want; g != e { + t.Errorf("open saw %v, want %v", g, e) + return + } +} + +// Test Fsync on a dir + +type fsyncDir struct { + fstestutil.Dir + record.Fsyncs +} + +func TestFsyncDir(t *testing.T) { + t.Parallel() + f := &fsyncDir{} + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + fil, err := os.Open(mnt.Dir) + if err != nil { + t.Errorf("fsyncDir open: %v", err) + return + } + defer fil.Close() + err = fil.Sync() + if err != nil { + t.Errorf("fsyncDir sync: %v", err) + return + } + + got := f.RecordedFsync() + want := fuse.FsyncRequest{ + Flags: 0, + Dir: true, + // unpredictable + Handle: got.Handle, + } + if runtime.GOOS == "darwin" { + // TODO document the meaning of these flags, figure out why + // they differ + want.Flags = 1 + } + if g, e := got, want; g != e { + t.Fatalf("fsyncDir saw %+v, want %+v", g, e) + } +} + +// Test Getxattr + +type getxattr struct { + fstestutil.File + record.Getxattrs +} + +func (f *getxattr) Getxattr(req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse, intr fs.Intr) fuse.Error { + f.Getxattrs.Getxattr(req, resp, intr) + resp.Xattr = []byte("hello, world") + return nil +} + +func TestGetxattr(t *testing.T) { + t.Parallel() + f := &getxattr{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + buf := make([]byte, 8192) + n, err := syscallx.Getxattr(mnt.Dir+"/child", "not-there", buf) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + buf = buf[:n] + if g, e := string(buf), "hello, world"; g != e { + t.Errorf("wrong getxattr content: %#v != %#v", g, e) + } + seen := f.RecordedGetxattr() + if g, e := seen.Name, "not-there"; g != e { + t.Errorf("wrong getxattr name: %#v != %#v", g, e) + } +} + +// Test Getxattr that has no space to return value + +type getxattrTooSmall struct { + fstestutil.File +} + +func (f *getxattrTooSmall) Getxattr(req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse, intr fs.Intr) fuse.Error { + resp.Xattr = []byte("hello, world") + return nil +} + +func TestGetxattrTooSmall(t *testing.T) { + t.Parallel() + f := &getxattrTooSmall{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + buf := make([]byte, 3) + _, err = syscallx.Getxattr(mnt.Dir+"/child", "whatever", buf) + if err == nil { + t.Error("Getxattr = nil; want some error") + } + if err != syscall.ERANGE { + t.Errorf("unexpected error: %v", err) + return + } +} + +// Test Getxattr used to probe result size + +type getxattrSize struct { + fstestutil.File +} + +func (f *getxattrSize) Getxattr(req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse, intr fs.Intr) fuse.Error { + resp.Xattr = []byte("hello, world") + return nil +} + +func TestGetxattrSize(t *testing.T) { + t.Parallel() + f := &getxattrSize{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + n, err := syscallx.Getxattr(mnt.Dir+"/child", "whatever", nil) + if err != nil { + t.Errorf("Getxattr unexpected error: %v", err) + return + } + if g, e := n, len("hello, world"); g != e { + t.Errorf("Getxattr incorrect size: %d != %d", g, e) + } +} + +// Test Listxattr + +type listxattr struct { + fstestutil.File + record.Listxattrs +} + +func (f *listxattr) Listxattr(req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse, intr fs.Intr) fuse.Error { + f.Listxattrs.Listxattr(req, resp, intr) + resp.Append("one", "two") + return nil +} + +func TestListxattr(t *testing.T) { + t.Parallel() + f := &listxattr{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + buf := make([]byte, 8192) + n, err := syscallx.Listxattr(mnt.Dir+"/child", buf) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + buf = buf[:n] + if g, e := string(buf), "one\x00two\x00"; g != e { + t.Errorf("wrong listxattr content: %#v != %#v", g, e) + } + + want := fuse.ListxattrRequest{ + Size: 8192, + } + if g, e := f.RecordedListxattr(), want; g != e { + t.Fatalf("listxattr saw %+v, want %+v", g, e) + } +} + +// Test Listxattr that has no space to return value + +type listxattrTooSmall struct { + fstestutil.File +} + +func (f *listxattrTooSmall) Listxattr(req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse, intr fs.Intr) fuse.Error { + resp.Xattr = []byte("one\x00two\x00") + return nil +} + +func TestListxattrTooSmall(t *testing.T) { + t.Parallel() + f := &listxattrTooSmall{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + buf := make([]byte, 3) + _, err = syscallx.Listxattr(mnt.Dir+"/child", buf) + if err == nil { + t.Error("Listxattr = nil; want some error") + } + if err != syscall.ERANGE { + t.Errorf("unexpected error: %v", err) + return + } +} + +// Test Listxattr used to probe result size + +type listxattrSize struct { + fstestutil.File +} + +func (f *listxattrSize) Listxattr(req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse, intr fs.Intr) fuse.Error { + resp.Xattr = []byte("one\x00two\x00") + return nil +} + +func TestListxattrSize(t *testing.T) { + t.Parallel() + f := &listxattrSize{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + n, err := syscallx.Listxattr(mnt.Dir+"/child", nil) + if err != nil { + t.Errorf("Listxattr unexpected error: %v", err) + return + } + if g, e := n, len("one\x00two\x00"); g != e { + t.Errorf("Getxattr incorrect size: %d != %d", g, e) + } +} + +// Test Setxattr + +type setxattr struct { + fstestutil.File + record.Setxattrs +} + +func testSetxattr(t *testing.T, size int) { + const linux_XATTR_NAME_MAX = 64 * 1024 + if size > linux_XATTR_NAME_MAX && runtime.GOOS == "linux" { + t.Skip("large xattrs are not supported by linux") + } + + t.Parallel() + f := &setxattr{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + const g = "hello, world" + greeting := strings.Repeat(g, size/len(g)+1)[:size] + err = syscallx.Setxattr(mnt.Dir+"/child", "greeting", []byte(greeting), 0) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + // fuse.SetxattrRequest contains a byte slice and thus cannot be + // directly compared + got := f.RecordedSetxattr() + + if g, e := got.Name, "greeting"; g != e { + t.Errorf("Setxattr incorrect name: %q != %q", g, e) + } + + if g, e := got.Flags, uint32(0); g != e { + t.Errorf("Setxattr incorrect flags: %d != %d", g, e) + } + + if g, e := string(got.Xattr), greeting; g != e { + t.Errorf("Setxattr incorrect data: %q != %q", g, e) + } +} + +func TestSetxattr(t *testing.T) { + testSetxattr(t, 20) +} + +func TestSetxattr64kB(t *testing.T) { + testSetxattr(t, 64*1024) +} + +func TestSetxattr16MB(t *testing.T) { + testSetxattr(t, 16*1024*1024) +} + +// Test Removexattr + +type removexattr struct { + fstestutil.File + record.Removexattrs +} + +func TestRemovexattr(t *testing.T) { + t.Parallel() + f := &removexattr{} + mnt, err := fstestutil.MountedT(t, childMapFS{"child": f}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + err = syscallx.Removexattr(mnt.Dir+"/child", "greeting") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + want := fuse.RemovexattrRequest{Name: "greeting"} + if g, e := f.RecordedRemovexattr(), want; g != e { + t.Errorf("removexattr saw %v, want %v", g, e) + } +} + +// Test default error. + +type defaultErrno struct { + fstestutil.Dir +} + +func (f defaultErrno) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) { + return nil, errors.New("bork") +} + +func TestDefaultErrno(t *testing.T) { + t.Parallel() + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{defaultErrno{}}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + _, err = os.Stat(mnt.Dir + "/trigger") + if err == nil { + t.Fatalf("expected error") + } + + switch err2 := err.(type) { + case *os.PathError: + if err2.Err == syscall.EIO { + break + } + t.Errorf("unexpected inner error: Err=%v %#v", err2.Err, err2) + default: + t.Errorf("unexpected error: %v", err) + } +} + +// Test custom error. + +type customErrNode struct { + fstestutil.Dir +} + +type myCustomError struct { + fuse.ErrorNumber +} + +var _ = fuse.ErrorNumber(myCustomError{}) + +func (myCustomError) Error() string { + return "bork" +} + +func (f customErrNode) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) { + return nil, myCustomError{ + ErrorNumber: fuse.Errno(syscall.ENAMETOOLONG), + } +} + +func TestCustomErrno(t *testing.T) { + t.Parallel() + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{customErrNode{}}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + _, err = os.Stat(mnt.Dir + "/trigger") + if err == nil { + t.Fatalf("expected error") + } + + switch err2 := err.(type) { + case *os.PathError: + if err2.Err == syscall.ENAMETOOLONG { + break + } + t.Errorf("unexpected inner error: %#v", err2) + default: + t.Errorf("unexpected error: %v", err) + } +} + +// Test Mmap writing + +type inMemoryFile struct { + data []byte +} + +func (f *inMemoryFile) Attr() fuse.Attr { + return fuse.Attr{ + Mode: 0666, + Size: uint64(len(f.data)), + } +} + +func (f *inMemoryFile) Read(req *fuse.ReadRequest, resp *fuse.ReadResponse, intr fs.Intr) fuse.Error { + fuseutil.HandleRead(req, resp, f.data) + return nil +} + +func (f *inMemoryFile) Write(req *fuse.WriteRequest, resp *fuse.WriteResponse, intr fs.Intr) fuse.Error { + resp.Size = copy(f.data[req.Offset:], req.Data) + return nil +} + +type mmap struct { + inMemoryFile + // We don't actually care about whether the fsync happened or not; + // this just lets us force the page cache to send the writes to + // FUSE, so we can reliably verify they came through. + record.Fsyncs +} + +func TestMmap(t *testing.T) { + const size = 16 * 4096 + writes := map[int]byte{ + 10: 'a', + 4096: 'b', + 4097: 'c', + size - 4096: 'd', + size - 1: 'z', + } + + // Run the mmap-using parts of the test in a subprocess, to avoid + // an intentional page fault hanging the whole process (because it + // would need to be served by the same process, and there might + // not be a thread free to do that). Merely bumping GOMAXPROCS is + // not enough to prevent the hangs reliably. + if childMode { + f, err := os.Create("child") + if err != nil { + t.Fatalf("Create: %v", err) + } + defer f.Close() + + data, err := syscall.Mmap(int(f.Fd()), 0, size, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) + if err != nil { + t.Fatalf("Mmap: %v", err) + } + + for i, b := range writes { + data[i] = b + } + + if err := syscallx.Msync(data, syscall.MS_SYNC); err != nil { + t.Fatalf("Msync: %v", err) + } + + if err := syscall.Munmap(data); err != nil { + t.Fatalf("Munmap: %v", err) + } + + if err := f.Sync(); err != nil { + t.Fatalf("Fsync = %v", err) + } + + err = f.Close() + if err != nil { + t.Fatalf("Close: %v", err) + } + + return + } + + w := &mmap{} + w.data = make([]byte, size) + mnt, err := fstestutil.MountedT(t, childMapFS{"child": w}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + child, err := childCmd("TestMmap") + if err != nil { + t.Fatal(err) + } + child.Dir = mnt.Dir + if err := child.Run(); err != nil { + t.Fatal(err) + } + + got := w.data + if g, e := len(got), size; g != e { + t.Fatalf("bad write length: %d != %d", g, e) + } + for i, g := range got { + // default '\x00' for writes[i] is good here + if e := writes[i]; g != e { + t.Errorf("wrong byte at offset %d: %q != %q", i, g, e) + } + } +} + +// Test direct Read. + +type directRead struct { + fstestutil.File +} + +// explicitly not defining Attr and setting Size + +func (f directRead) Open(req *fuse.OpenRequest, resp *fuse.OpenResponse, intr fs.Intr) (fs.Handle, fuse.Error) { + // do not allow the kernel to use page cache + resp.Flags |= fuse.OpenDirectIO + return f, nil +} + +func (directRead) Read(req *fuse.ReadRequest, resp *fuse.ReadResponse, intr fs.Intr) fuse.Error { + fuseutil.HandleRead(req, resp, []byte(hi)) + return nil +} + +func TestDirectRead(t *testing.T) { + t.Parallel() + mnt, err := fstestutil.MountedT(t, childMapFS{"child": directRead{}}) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + testReadAll(t, mnt.Dir+"/child") +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/tree.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/tree.go new file mode 100644 index 00000000..5a12071e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fs/tree.go @@ -0,0 +1,96 @@ +// FUSE directory tree, for servers that wish to use it with the service loop. + +package fs + +import ( + "os" + pathpkg "path" + "strings" +) + +import ( + "camlistore.org/third_party/bazil.org/fuse" +) + +// A Tree implements a basic read-only directory tree for FUSE. +// The Nodes contained in it may still be writable. +type Tree struct { + tree +} + +func (t *Tree) Root() (Node, fuse.Error) { + return &t.tree, nil +} + +// Add adds the path to the tree, resolving to the given node. +// If path or a prefix of path has already been added to the tree, +// Add panics. +// +// Add is only safe to call before starting to serve requests. +func (t *Tree) Add(path string, node Node) { + path = pathpkg.Clean("/" + path)[1:] + elems := strings.Split(path, "/") + dir := Node(&t.tree) + for i, elem := range elems { + dt, ok := dir.(*tree) + if !ok { + panic("fuse: Tree.Add for " + strings.Join(elems[:i], "/") + " and " + path) + } + n := dt.lookup(elem) + if n != nil { + if i+1 == len(elems) { + panic("fuse: Tree.Add for " + path + " conflicts with " + elem) + } + dir = n + } else { + if i+1 == len(elems) { + dt.add(elem, node) + } else { + dir = &tree{} + dt.add(elem, dir) + } + } + } +} + +type treeDir struct { + name string + node Node +} + +type tree struct { + dir []treeDir +} + +func (t *tree) lookup(name string) Node { + for _, d := range t.dir { + if d.name == name { + return d.node + } + } + return nil +} + +func (t *tree) add(name string, n Node) { + t.dir = append(t.dir, treeDir{name, n}) +} + +func (t *tree) Attr() fuse.Attr { + return fuse.Attr{Mode: os.ModeDir | 0555} +} + +func (t *tree) Lookup(name string, intr Intr) (Node, fuse.Error) { + n := t.lookup(name) + if n != nil { + return n, nil + } + return nil, fuse.ENOENT +} + +func (t *tree) ReadDir(intr Intr) ([]fuse.Dirent, fuse.Error) { + var out []fuse.Dirent + for _, d := range t.dir { + out = append(out, fuse.Dirent{Name: d.name}) + } + return out, nil +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse.go new file mode 100644 index 00000000..c6b81793 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse.go @@ -0,0 +1,2043 @@ +// See the file LICENSE for copyright and licensing information. +// Adapted from Plan 9 from User Space's src/cmd/9pfuse/fuse.c, +// which carries this notice: +// +// The files in this directory are subject to the following license. +// +// The author of this software is Russ Cox. +// +// Copyright (c) 2006 Russ Cox +// +// Permission to use, copy, modify, and distribute this software for any +// purpose without fee is hereby granted, provided that this entire notice +// is included in all copies of any software which is or includes a copy +// or modification of this software and in all copies of the supporting +// documentation for such software. +// +// THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED +// WARRANTY. IN PARTICULAR, THE AUTHOR MAKES NO REPRESENTATION OR WARRANTY +// OF ANY KIND CONCERNING THE MERCHANTABILITY OF THIS SOFTWARE OR ITS +// FITNESS FOR ANY PARTICULAR PURPOSE. + +// Package fuse enables writing FUSE file systems on Linux, OS X, and FreeBSD. +// +// On OS X, it requires OSXFUSE (http://osxfuse.github.com/). +// +// There are two approaches to writing a FUSE file system. The first is to speak +// the low-level message protocol, reading from a Conn using ReadRequest and +// writing using the various Respond methods. This approach is closest to +// the actual interaction with the kernel and can be the simplest one in contexts +// such as protocol translators. +// +// Servers of synthesized file systems tend to share common +// bookkeeping abstracted away by the second approach, which is to +// call fs.Serve to serve the FUSE protocol using an implementation of +// the service methods in the interfaces FS* (file system), Node* (file +// or directory), and Handle* (opened file or directory). +// There are a daunting number of such methods that can be written, +// but few are required. +// The specific methods are described in the documentation for those interfaces. +// +// The hellofs subdirectory contains a simple illustration of the fs.Serve approach. +// +// Service Methods +// +// The required and optional methods for the FS, Node, and Handle interfaces +// have the general form +// +// Op(req *OpRequest, resp *OpResponse, intr Intr) Error +// +// where Op is the name of a FUSE operation. Op reads request parameters +// from req and writes results to resp. An operation whose only result is +// the error result omits the resp parameter. Multiple goroutines may call +// service methods simultaneously; the methods being called are responsible +// for appropriate synchronization. +// +// Interrupted Operations +// +// In some file systems, some operations +// may take an undetermined amount of time. For example, a Read waiting for +// a network message or a matching Write might wait indefinitely. If the request +// is cancelled and no longer needed, the package will close intr, a chan struct{}. +// Blocking operations should select on a receive from intr and attempt to +// abort the operation early if the receive succeeds (meaning the channel is closed). +// To indicate that the operation failed because it was aborted, return fuse.EINTR. +// +// If an operation does not block for an indefinite amount of time, the intr parameter +// can be ignored. +// +// Authentication +// +// All requests types embed a Header, meaning that the method can inspect +// req.Pid, req.Uid, and req.Gid as necessary to implement permission checking. +// Alternately, XXX. +// +// Mount Options +// +// Behavior and metadata of the mounted file system can be changed by +// passing MountOption values to Mount. +// +package fuse + +// BUG(rsc): The mount code for FreeBSD has not been written yet. + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "sync" + "syscall" + "time" + "unsafe" +) + +// A Conn represents a connection to a mounted FUSE file system. +type Conn struct { + // Ready is closed when the mount is complete or has failed. + Ready <-chan struct{} + + // MountError stores any error from the mount process. Only valid + // after Ready is closed. + MountError error + + // File handle for kernel communication. Only safe to access if + // rio or wio is held. + dev *os.File + buf []byte + wio sync.Mutex + rio sync.RWMutex +} + +// Mount mounts a new FUSE connection on the named directory +// and returns a connection for reading and writing FUSE messages. +// +// After a successful return, caller must call Close to free +// resources. +// +// Even on successful return, the new mount is not guaranteed to be +// visible until after Conn.Ready is closed. See Conn.MountError for +// possible errors. Incoming requests on Conn must be served to make +// progress. +func Mount(dir string, options ...MountOption) (*Conn, error) { + conf := MountConfig{ + options: make(map[string]string), + } + for _, option := range options { + if err := option(&conf); err != nil { + return nil, err + } + } + + ready := make(chan struct{}, 1) + c := &Conn{ + Ready: ready, + } + f, err := mount(dir, &conf, ready, &c.MountError) + if err != nil { + return nil, err + } + c.dev = f + return c, nil +} + +// A Request represents a single FUSE request received from the kernel. +// Use a type switch to determine the specific kind. +// A request of unrecognized type will have concrete type *Header. +type Request interface { + // Hdr returns the Header associated with this request. + Hdr() *Header + + // RespondError responds to the request with the given error. + RespondError(Error) + + String() string +} + +// A RequestID identifies an active FUSE request. +type RequestID uint64 + +// A NodeID is a number identifying a directory or file. +// It must be unique among IDs returned in LookupResponses +// that have not yet been forgotten by ForgetRequests. +type NodeID uint64 + +// A HandleID is a number identifying an open directory or file. +// It only needs to be unique while the directory or file is open. +type HandleID uint64 + +// The RootID identifies the root directory of a FUSE file system. +const RootID NodeID = rootID + +// A Header describes the basic information sent in every request. +type Header struct { + Conn *Conn `json:"-"` // connection this request was received on + ID RequestID // unique ID for request + Node NodeID // file or directory the request is about + Uid uint32 // user ID of process making request + Gid uint32 // group ID of process making request + Pid uint32 // process ID of process making request + + // for returning to reqPool + msg *message +} + +func (h *Header) String() string { + return fmt.Sprintf("ID=%#x Node=%#x Uid=%d Gid=%d Pid=%d", h.ID, h.Node, h.Uid, h.Gid, h.Pid) +} + +func (h *Header) Hdr() *Header { + return h +} + +func (h *Header) noResponse() { + putMessage(h.msg) +} + +func (h *Header) respond(out *outHeader, n uintptr) { + h.Conn.respond(out, n) + putMessage(h.msg) +} + +func (h *Header) respondData(out *outHeader, n uintptr, data []byte) { + h.Conn.respondData(out, n, data) + putMessage(h.msg) +} + +// An Error is a FUSE error. +// +// Errors messages will be visible in the debug log as part of the +// response. +// +// The FUSE interface can only communicate POSIX errno error numbers +// to file system clients, the message is not visible to file system +// clients. The returned error can implement ErrorNumber to control +// the errno returned. Without ErrorNumber, a generic errno (EIO) is +// returned. +type Error error + +// An ErrorNumber is an error with a specific error number. +// +// Operations may return an error value that implements ErrorNumber to +// control what specific error number (errno) to return. +type ErrorNumber interface { + // Errno returns the the error number (errno) for this error. + Errno() Errno +} + +const ( + // ENOSYS indicates that the call is not supported. + ENOSYS = Errno(syscall.ENOSYS) + + // ESTALE is used by Serve to respond to violations of the FUSE protocol. + ESTALE = Errno(syscall.ESTALE) + + ENOENT = Errno(syscall.ENOENT) + EIO = Errno(syscall.EIO) + EPERM = Errno(syscall.EPERM) + + // EINTR indicates request was interrupted by an InterruptRequest. + // See also fs.Intr. + EINTR = Errno(syscall.EINTR) + + ENODATA = Errno(syscall.ENODATA) + ERANGE = Errno(syscall.ERANGE) + ENOTSUP = Errno(syscall.ENOTSUP) + EEXIST = Errno(syscall.EEXIST) +) + +// DefaultErrno is the errno used when error returned does not +// implement ErrorNumber. +const DefaultErrno = EIO + +var errnoNames = map[Errno]string{ + ENOSYS: "ENOSYS", + ESTALE: "ESTALE", + ENOENT: "ENOENT", + EIO: "EIO", + EPERM: "EPERM", + EINTR: "EINTR", + ENODATA: "ENODATA", + EEXIST: "EEXIST", +} + +// Errno implements Error and ErrorNumber using a syscall.Errno. +type Errno syscall.Errno + +var _ = ErrorNumber(Errno(0)) +var _ = Error(Errno(0)) +var _ = error(Errno(0)) + +func (e Errno) Errno() Errno { + return e +} + +func (e Errno) String() string { + return syscall.Errno(e).Error() +} + +func (e Errno) Error() string { + return syscall.Errno(e).Error() +} + +// ErrnoName returns the short non-numeric identifier for this errno. +// For example, "EIO". +func (e Errno) ErrnoName() string { + s := errnoNames[e] + if s == "" { + s = fmt.Sprint(e.Errno()) + } + return s +} + +func (e Errno) MarshalText() ([]byte, error) { + s := e.ErrnoName() + return []byte(s), nil +} + +func (h *Header) RespondError(err Error) { + errno := DefaultErrno + if ferr, ok := err.(ErrorNumber); ok { + errno = ferr.Errno() + } + // FUSE uses negative errors! + // TODO: File bug report against OSXFUSE: positive error causes kernel panic. + out := &outHeader{Error: -int32(errno), Unique: uint64(h.ID)} + h.respond(out, unsafe.Sizeof(*out)) +} + +// Maximum file write size we are prepared to receive from the kernel. +const maxWrite = 16 * 1024 * 1024 + +// All requests read from the kernel, without data, are shorter than +// this. +var maxRequestSize = syscall.Getpagesize() +var bufSize = maxRequestSize + maxWrite + +// reqPool is a pool of messages. +// +// Lifetime of a logical message is from getMessage to putMessage. +// getMessage is called by ReadRequest. putMessage is called by +// Conn.ReadRequest, Request.Respond, or Request.RespondError. +// +// Messages in the pool are guaranteed to have conn and off zeroed, +// buf allocated and len==bufSize, and hdr set. +var reqPool = sync.Pool{ + New: allocMessage, +} + +func allocMessage() interface{} { + m := &message{buf: make([]byte, bufSize)} + m.hdr = (*inHeader)(unsafe.Pointer(&m.buf[0])) + return m +} + +func getMessage(c *Conn) *message { + m := reqPool.Get().(*message) + m.conn = c + return m +} + +func putMessage(m *message) { + m.buf = m.buf[:bufSize] + m.conn = nil + m.off = 0 + reqPool.Put(m) +} + +// a message represents the bytes of a single FUSE message +type message struct { + conn *Conn + buf []byte // all bytes + hdr *inHeader // header + off int // offset for reading additional fields +} + +func (m *message) len() uintptr { + return uintptr(len(m.buf) - m.off) +} + +func (m *message) data() unsafe.Pointer { + var p unsafe.Pointer + if m.off < len(m.buf) { + p = unsafe.Pointer(&m.buf[m.off]) + } + return p +} + +func (m *message) bytes() []byte { + return m.buf[m.off:] +} + +func (m *message) Header() Header { + h := m.hdr + return Header{ + Conn: m.conn, + ID: RequestID(h.Unique), + Node: NodeID(h.Nodeid), + Uid: h.Uid, + Gid: h.Gid, + Pid: h.Pid, + + msg: m, + } +} + +// fileMode returns a Go os.FileMode from a Unix mode. +func fileMode(unixMode uint32) os.FileMode { + mode := os.FileMode(unixMode & 0777) + switch unixMode & syscall.S_IFMT { + case syscall.S_IFREG: + // nothing + case syscall.S_IFDIR: + mode |= os.ModeDir + case syscall.S_IFCHR: + mode |= os.ModeCharDevice | os.ModeDevice + case syscall.S_IFBLK: + mode |= os.ModeDevice + case syscall.S_IFIFO: + mode |= os.ModeNamedPipe + case syscall.S_IFLNK: + mode |= os.ModeSymlink + case syscall.S_IFSOCK: + mode |= os.ModeSocket + default: + // no idea + mode |= os.ModeDevice + } + if unixMode&syscall.S_ISUID != 0 { + mode |= os.ModeSetuid + } + if unixMode&syscall.S_ISGID != 0 { + mode |= os.ModeSetgid + } + return mode +} + +type noOpcode struct { + Opcode uint32 +} + +func (m noOpcode) String() string { + return fmt.Sprintf("No opcode %v", m.Opcode) +} + +type malformedMessage struct { +} + +func (malformedMessage) String() string { + return "malformed message" +} + +// Close closes the FUSE connection. +func (c *Conn) Close() error { + c.wio.Lock() + defer c.wio.Unlock() + c.rio.Lock() + defer c.rio.Unlock() + return c.dev.Close() +} + +// caller must hold wio or rio +func (c *Conn) fd() int { + return int(c.dev.Fd()) +} + +// ReadRequest returns the next FUSE request from the kernel. +// +// Caller must call either Request.Respond or Request.RespondError in +// a reasonable time. Caller must not retain Request after that call. +func (c *Conn) ReadRequest() (Request, error) { + m := getMessage(c) +loop: + c.rio.RLock() + n, err := syscall.Read(c.fd(), m.buf) + c.rio.RUnlock() + if err == syscall.EINTR { + // OSXFUSE sends EINTR to userspace when a request interrupt + // completed before it got sent to userspace? + goto loop + } + if err != nil && err != syscall.ENODEV { + putMessage(m) + return nil, err + } + if n <= 0 { + putMessage(m) + return nil, io.EOF + } + m.buf = m.buf[:n] + + if n < inHeaderSize { + putMessage(m) + return nil, errors.New("fuse: message too short") + } + + // FreeBSD FUSE sends a short length in the header + // for FUSE_INIT even though the actual read length is correct. + if n == inHeaderSize+initInSize && m.hdr.Opcode == opInit && m.hdr.Len < uint32(n) { + m.hdr.Len = uint32(n) + } + + // OSXFUSE sometimes sends the wrong m.hdr.Len in a FUSE_WRITE message. + if m.hdr.Len < uint32(n) && m.hdr.Len >= uint32(unsafe.Sizeof(writeIn{})) && m.hdr.Opcode == opWrite { + m.hdr.Len = uint32(n) + } + + if m.hdr.Len != uint32(n) { + // prepare error message before returning m to pool + err := fmt.Errorf("fuse: read %d opcode %d but expected %d", n, m.hdr.Opcode, m.hdr.Len) + putMessage(m) + return nil, err + } + + m.off = inHeaderSize + + // Convert to data structures. + // Do not trust kernel to hand us well-formed data. + var req Request + switch m.hdr.Opcode { + default: + Debug(noOpcode{Opcode: m.hdr.Opcode}) + goto unrecognized + + case opLookup: + buf := m.bytes() + n := len(buf) + if n == 0 || buf[n-1] != '\x00' { + goto corrupt + } + req = &LookupRequest{ + Header: m.Header(), + Name: string(buf[:n-1]), + } + + case opForget: + in := (*forgetIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + req = &ForgetRequest{ + Header: m.Header(), + N: in.Nlookup, + } + + case opGetattr: + req = &GetattrRequest{ + Header: m.Header(), + } + + case opSetattr: + in := (*setattrIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + req = &SetattrRequest{ + Header: m.Header(), + Valid: SetattrValid(in.Valid), + Handle: HandleID(in.Fh), + Size: in.Size, + Atime: time.Unix(int64(in.Atime), int64(in.AtimeNsec)), + Mtime: time.Unix(int64(in.Mtime), int64(in.MtimeNsec)), + Mode: fileMode(in.Mode), + Uid: in.Uid, + Gid: in.Gid, + Bkuptime: in.BkupTime(), + Chgtime: in.Chgtime(), + Flags: in.Flags(), + } + + case opReadlink: + if len(m.bytes()) > 0 { + goto corrupt + } + req = &ReadlinkRequest{ + Header: m.Header(), + } + + case opSymlink: + // m.bytes() is "newName\0target\0" + names := m.bytes() + if len(names) == 0 || names[len(names)-1] != 0 { + goto corrupt + } + i := bytes.IndexByte(names, '\x00') + if i < 0 { + goto corrupt + } + newName, target := names[0:i], names[i+1:len(names)-1] + req = &SymlinkRequest{ + Header: m.Header(), + NewName: string(newName), + Target: string(target), + } + + case opLink: + in := (*linkIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + newName := m.bytes()[unsafe.Sizeof(*in):] + if len(newName) < 2 || newName[len(newName)-1] != 0 { + goto corrupt + } + newName = newName[:len(newName)-1] + req = &LinkRequest{ + Header: m.Header(), + OldNode: NodeID(in.Oldnodeid), + NewName: string(newName), + } + + case opMknod: + in := (*mknodIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + name := m.bytes()[unsafe.Sizeof(*in):] + if len(name) < 2 || name[len(name)-1] != '\x00' { + goto corrupt + } + name = name[:len(name)-1] + req = &MknodRequest{ + Header: m.Header(), + Mode: fileMode(in.Mode), + Rdev: in.Rdev, + Name: string(name), + } + + case opMkdir: + in := (*mkdirIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + name := m.bytes()[unsafe.Sizeof(*in):] + i := bytes.IndexByte(name, '\x00') + if i < 0 { + goto corrupt + } + req = &MkdirRequest{ + Header: m.Header(), + Name: string(name[:i]), + // observed on Linux: mkdirIn.Mode & syscall.S_IFMT == 0, + // and this causes fileMode to go into it's "no idea" + // code branch; enforce type to directory + Mode: fileMode((in.Mode &^ syscall.S_IFMT) | syscall.S_IFDIR), + } + + case opUnlink, opRmdir: + buf := m.bytes() + n := len(buf) + if n == 0 || buf[n-1] != '\x00' { + goto corrupt + } + req = &RemoveRequest{ + Header: m.Header(), + Name: string(buf[:n-1]), + Dir: m.hdr.Opcode == opRmdir, + } + + case opRename: + in := (*renameIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + newDirNodeID := NodeID(in.Newdir) + oldNew := m.bytes()[unsafe.Sizeof(*in):] + // oldNew should be "old\x00new\x00" + if len(oldNew) < 4 { + goto corrupt + } + if oldNew[len(oldNew)-1] != '\x00' { + goto corrupt + } + i := bytes.IndexByte(oldNew, '\x00') + if i < 0 { + goto corrupt + } + oldName, newName := string(oldNew[:i]), string(oldNew[i+1:len(oldNew)-1]) + req = &RenameRequest{ + Header: m.Header(), + NewDir: newDirNodeID, + OldName: oldName, + NewName: newName, + } + + case opOpendir, opOpen: + in := (*openIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + req = &OpenRequest{ + Header: m.Header(), + Dir: m.hdr.Opcode == opOpendir, + Flags: openFlags(in.Flags), + } + + case opRead, opReaddir: + in := (*readIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + req = &ReadRequest{ + Header: m.Header(), + Dir: m.hdr.Opcode == opReaddir, + Handle: HandleID(in.Fh), + Offset: int64(in.Offset), + Size: int(in.Size), + } + + case opWrite: + in := (*writeIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + r := &WriteRequest{ + Header: m.Header(), + Handle: HandleID(in.Fh), + Offset: int64(in.Offset), + Flags: WriteFlags(in.WriteFlags), + } + buf := m.bytes()[unsafe.Sizeof(*in):] + if uint32(len(buf)) < in.Size { + goto corrupt + } + r.Data = buf + req = r + + case opStatfs: + req = &StatfsRequest{ + Header: m.Header(), + } + + case opRelease, opReleasedir: + in := (*releaseIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + req = &ReleaseRequest{ + Header: m.Header(), + Dir: m.hdr.Opcode == opReleasedir, + Handle: HandleID(in.Fh), + Flags: openFlags(in.Flags), + ReleaseFlags: ReleaseFlags(in.ReleaseFlags), + LockOwner: in.LockOwner, + } + + case opFsync, opFsyncdir: + in := (*fsyncIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + req = &FsyncRequest{ + Dir: m.hdr.Opcode == opFsyncdir, + Header: m.Header(), + Handle: HandleID(in.Fh), + Flags: in.FsyncFlags, + } + + case opSetxattr: + in := (*setxattrIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + m.off += int(unsafe.Sizeof(*in)) + name := m.bytes() + i := bytes.IndexByte(name, '\x00') + if i < 0 { + goto corrupt + } + xattr := name[i+1:] + if uint32(len(xattr)) < in.Size { + goto corrupt + } + xattr = xattr[:in.Size] + req = &SetxattrRequest{ + Header: m.Header(), + Flags: in.Flags, + Position: in.position(), + Name: string(name[:i]), + Xattr: xattr, + } + + case opGetxattr: + in := (*getxattrIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + name := m.bytes()[unsafe.Sizeof(*in):] + i := bytes.IndexByte(name, '\x00') + if i < 0 { + goto corrupt + } + req = &GetxattrRequest{ + Header: m.Header(), + Name: string(name[:i]), + Size: in.Size, + Position: in.position(), + } + + case opListxattr: + in := (*getxattrIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + req = &ListxattrRequest{ + Header: m.Header(), + Size: in.Size, + Position: in.position(), + } + + case opRemovexattr: + buf := m.bytes() + n := len(buf) + if n == 0 || buf[n-1] != '\x00' { + goto corrupt + } + req = &RemovexattrRequest{ + Header: m.Header(), + Name: string(buf[:n-1]), + } + + case opFlush: + in := (*flushIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + req = &FlushRequest{ + Header: m.Header(), + Handle: HandleID(in.Fh), + Flags: in.FlushFlags, + LockOwner: in.LockOwner, + } + + case opInit: + in := (*initIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + req = &InitRequest{ + Header: m.Header(), + Major: in.Major, + Minor: in.Minor, + MaxReadahead: in.MaxReadahead, + Flags: InitFlags(in.Flags), + } + + case opGetlk: + panic("opGetlk") + case opSetlk: + panic("opSetlk") + case opSetlkw: + panic("opSetlkw") + + case opAccess: + in := (*accessIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + req = &AccessRequest{ + Header: m.Header(), + Mask: in.Mask, + } + + case opCreate: + in := (*createIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + name := m.bytes()[unsafe.Sizeof(*in):] + i := bytes.IndexByte(name, '\x00') + if i < 0 { + goto corrupt + } + req = &CreateRequest{ + Header: m.Header(), + Flags: openFlags(in.Flags), + Mode: fileMode(in.Mode), + Name: string(name[:i]), + } + + case opInterrupt: + in := (*interruptIn)(m.data()) + if m.len() < unsafe.Sizeof(*in) { + goto corrupt + } + req = &InterruptRequest{ + Header: m.Header(), + IntrID: RequestID(in.Unique), + } + + case opBmap: + panic("opBmap") + + case opDestroy: + req = &DestroyRequest{ + Header: m.Header(), + } + + // OS X + case opSetvolname: + panic("opSetvolname") + case opGetxtimes: + panic("opGetxtimes") + case opExchange: + panic("opExchange") + } + + return req, nil + +corrupt: + Debug(malformedMessage{}) + putMessage(m) + return nil, fmt.Errorf("fuse: malformed message") + +unrecognized: + // Unrecognized message. + // Assume higher-level code will send a "no idea what you mean" error. + h := m.Header() + return &h, nil +} + +type bugShortKernelWrite struct { + Written int64 + Length int64 + Error string + Stack string +} + +func (b bugShortKernelWrite) String() string { + return fmt.Sprintf("short kernel write: written=%d/%d error=%q stack=\n%s", b.Written, b.Length, b.Error, b.Stack) +} + +// safe to call even with nil error +func errorString(err error) string { + if err == nil { + return "" + } + return err.Error() +} + +func (c *Conn) respond(out *outHeader, n uintptr) { + c.wio.Lock() + defer c.wio.Unlock() + out.Len = uint32(n) + msg := (*[1 << 30]byte)(unsafe.Pointer(out))[:n] + nn, err := syscall.Write(c.fd(), msg) + if nn != len(msg) || err != nil { + Debug(bugShortKernelWrite{ + Written: int64(nn), + Length: int64(len(msg)), + Error: errorString(err), + Stack: stack(), + }) + } +} + +func (c *Conn) respondData(out *outHeader, n uintptr, data []byte) { + c.wio.Lock() + defer c.wio.Unlock() + // TODO: use writev + out.Len = uint32(n + uintptr(len(data))) + msg := make([]byte, out.Len) + copy(msg, (*[1 << 30]byte)(unsafe.Pointer(out))[:n]) + copy(msg[n:], data) + syscall.Write(c.fd(), msg) +} + +// An InitRequest is the first request sent on a FUSE file system. +type InitRequest struct { + Header `json:"-"` + Major uint32 + Minor uint32 + // Maximum readahead in bytes that the kernel plans to use. + MaxReadahead uint32 + Flags InitFlags +} + +var _ = Request(&InitRequest{}) + +func (r *InitRequest) String() string { + return fmt.Sprintf("Init [%s] %d.%d ra=%d fl=%v", &r.Header, r.Major, r.Minor, r.MaxReadahead, r.Flags) +} + +// An InitResponse is the response to an InitRequest. +type InitResponse struct { + // Maximum readahead in bytes that the kernel can use. Ignored if + // greater than InitRequest.MaxReadahead. + MaxReadahead uint32 + Flags InitFlags + // Maximum size of a single write operation. + // Linux enforces a minimum of 4 KiB. + MaxWrite uint32 +} + +func (r *InitResponse) String() string { + return fmt.Sprintf("Init %+v", *r) +} + +// Respond replies to the request with the given response. +func (r *InitRequest) Respond(resp *InitResponse) { + out := &initOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + Major: kernelVersion, + Minor: kernelMinorVersion, + MaxReadahead: resp.MaxReadahead, + Flags: uint32(resp.Flags), + MaxWrite: resp.MaxWrite, + } + // MaxWrite larger than our receive buffer would just lead to + // errors on large writes. + if out.MaxWrite > maxWrite { + out.MaxWrite = maxWrite + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +// A StatfsRequest requests information about the mounted file system. +type StatfsRequest struct { + Header `json:"-"` +} + +var _ = Request(&StatfsRequest{}) + +func (r *StatfsRequest) String() string { + return fmt.Sprintf("Statfs [%s]\n", &r.Header) +} + +// Respond replies to the request with the given response. +func (r *StatfsRequest) Respond(resp *StatfsResponse) { + out := &statfsOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + St: kstatfs{ + Blocks: resp.Blocks, + Bfree: resp.Bfree, + Bavail: resp.Bavail, + Files: resp.Files, + Bsize: resp.Bsize, + Namelen: resp.Namelen, + Frsize: resp.Frsize, + }, + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +// A StatfsResponse is the response to a StatfsRequest. +type StatfsResponse struct { + Blocks uint64 // Total data blocks in file system. + Bfree uint64 // Free blocks in file system. + Bavail uint64 // Free blocks in file system if you're not root. + Files uint64 // Total files in file system. + Ffree uint64 // Free files in file system. + Bsize uint32 // Block size + Namelen uint32 // Maximum file name length? + Frsize uint32 // Fragment size, smallest addressable data size in the file system. +} + +func (r *StatfsResponse) String() string { + return fmt.Sprintf("Statfs %+v", *r) +} + +// An AccessRequest asks whether the file can be accessed +// for the purpose specified by the mask. +type AccessRequest struct { + Header `json:"-"` + Mask uint32 +} + +var _ = Request(&AccessRequest{}) + +func (r *AccessRequest) String() string { + return fmt.Sprintf("Access [%s] mask=%#x", &r.Header, r.Mask) +} + +// Respond replies to the request indicating that access is allowed. +// To deny access, use RespondError. +func (r *AccessRequest) Respond() { + out := &outHeader{Unique: uint64(r.ID)} + r.respond(out, unsafe.Sizeof(*out)) +} + +// An Attr is the metadata for a single file or directory. +type Attr struct { + Inode uint64 // inode number + Size uint64 // size in bytes + Blocks uint64 // size in blocks + Atime time.Time // time of last access + Mtime time.Time // time of last modification + Ctime time.Time // time of last inode change + Crtime time.Time // time of creation (OS X only) + Mode os.FileMode // file mode + Nlink uint32 // number of links + Uid uint32 // owner uid + Gid uint32 // group gid + Rdev uint32 // device numbers + Flags uint32 // chflags(2) flags (OS X only) +} + +func unix(t time.Time) (sec uint64, nsec uint32) { + nano := t.UnixNano() + sec = uint64(nano / 1e9) + nsec = uint32(nano % 1e9) + return +} + +func (a *Attr) attr() (out attr) { + out.Ino = a.Inode + out.Size = a.Size + out.Blocks = a.Blocks + out.Atime, out.AtimeNsec = unix(a.Atime) + out.Mtime, out.MtimeNsec = unix(a.Mtime) + out.Ctime, out.CtimeNsec = unix(a.Ctime) + out.SetCrtime(unix(a.Crtime)) + out.Mode = uint32(a.Mode) & 0777 + switch { + default: + out.Mode |= syscall.S_IFREG + case a.Mode&os.ModeDir != 0: + out.Mode |= syscall.S_IFDIR + case a.Mode&os.ModeDevice != 0: + if a.Mode&os.ModeCharDevice != 0 { + out.Mode |= syscall.S_IFCHR + } else { + out.Mode |= syscall.S_IFBLK + } + case a.Mode&os.ModeNamedPipe != 0: + out.Mode |= syscall.S_IFIFO + case a.Mode&os.ModeSymlink != 0: + out.Mode |= syscall.S_IFLNK + case a.Mode&os.ModeSocket != 0: + out.Mode |= syscall.S_IFSOCK + } + if a.Mode&os.ModeSetuid != 0 { + out.Mode |= syscall.S_ISUID + } + if a.Mode&os.ModeSetgid != 0 { + out.Mode |= syscall.S_ISGID + } + out.Nlink = a.Nlink + if out.Nlink < 1 { + out.Nlink = 1 + } + out.Uid = a.Uid + out.Gid = a.Gid + out.Rdev = a.Rdev + out.SetFlags(a.Flags) + + return +} + +// A GetattrRequest asks for the metadata for the file denoted by r.Node. +type GetattrRequest struct { + Header `json:"-"` +} + +var _ = Request(&GetattrRequest{}) + +func (r *GetattrRequest) String() string { + return fmt.Sprintf("Getattr [%s]", &r.Header) +} + +// Respond replies to the request with the given response. +func (r *GetattrRequest) Respond(resp *GetattrResponse) { + out := &attrOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + AttrValid: uint64(resp.AttrValid / time.Second), + AttrValidNsec: uint32(resp.AttrValid % time.Second / time.Nanosecond), + Attr: resp.Attr.attr(), + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +// A GetattrResponse is the response to a GetattrRequest. +type GetattrResponse struct { + AttrValid time.Duration // how long Attr can be cached + Attr Attr // file attributes +} + +func (r *GetattrResponse) String() string { + return fmt.Sprintf("Getattr %+v", *r) +} + +// A GetxattrRequest asks for the extended attributes associated with r.Node. +type GetxattrRequest struct { + Header `json:"-"` + + // Maximum size to return. + Size uint32 + + // Name of the attribute requested. + Name string + + // Offset within extended attributes. + // + // Only valid for OS X, and then only with the resource fork + // attribute. + Position uint32 +} + +var _ = Request(&GetxattrRequest{}) + +func (r *GetxattrRequest) String() string { + return fmt.Sprintf("Getxattr [%s] %q %d @%d", &r.Header, r.Name, r.Size, r.Position) +} + +// Respond replies to the request with the given response. +func (r *GetxattrRequest) Respond(resp *GetxattrResponse) { + if r.Size == 0 { + out := &getxattrOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + Size: uint32(len(resp.Xattr)), + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) + } else { + out := &outHeader{Unique: uint64(r.ID)} + r.respondData(out, unsafe.Sizeof(*out), resp.Xattr) + } +} + +func (r *GetxattrRequest) RespondError(err Error) { + err = translateGetxattrError(err) + r.Header.RespondError(err) +} + +// A GetxattrResponse is the response to a GetxattrRequest. +type GetxattrResponse struct { + Xattr []byte +} + +func (r *GetxattrResponse) String() string { + return fmt.Sprintf("Getxattr %x", r.Xattr) +} + +// A ListxattrRequest asks to list the extended attributes associated with r.Node. +type ListxattrRequest struct { + Header `json:"-"` + Size uint32 // maximum size to return + Position uint32 // offset within attribute list +} + +var _ = Request(&ListxattrRequest{}) + +func (r *ListxattrRequest) String() string { + return fmt.Sprintf("Listxattr [%s] %d @%d", &r.Header, r.Size, r.Position) +} + +// Respond replies to the request with the given response. +func (r *ListxattrRequest) Respond(resp *ListxattrResponse) { + if r.Size == 0 { + out := &getxattrOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + Size: uint32(len(resp.Xattr)), + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) + } else { + out := &outHeader{Unique: uint64(r.ID)} + r.respondData(out, unsafe.Sizeof(*out), resp.Xattr) + } +} + +// A ListxattrResponse is the response to a ListxattrRequest. +type ListxattrResponse struct { + Xattr []byte +} + +func (r *ListxattrResponse) String() string { + return fmt.Sprintf("Listxattr %x", r.Xattr) +} + +// Append adds an extended attribute name to the response. +func (r *ListxattrResponse) Append(names ...string) { + for _, name := range names { + r.Xattr = append(r.Xattr, name...) + r.Xattr = append(r.Xattr, '\x00') + } +} + +// A RemovexattrRequest asks to remove an extended attribute associated with r.Node. +type RemovexattrRequest struct { + Header `json:"-"` + Name string // name of extended attribute +} + +var _ = Request(&RemovexattrRequest{}) + +func (r *RemovexattrRequest) String() string { + return fmt.Sprintf("Removexattr [%s] %q", &r.Header, r.Name) +} + +// Respond replies to the request, indicating that the attribute was removed. +func (r *RemovexattrRequest) Respond() { + out := &outHeader{Unique: uint64(r.ID)} + r.respond(out, unsafe.Sizeof(*out)) +} + +func (r *RemovexattrRequest) RespondError(err Error) { + err = translateGetxattrError(err) + r.Header.RespondError(err) +} + +// A SetxattrRequest asks to set an extended attribute associated with a file. +type SetxattrRequest struct { + Header `json:"-"` + + // Flags can make the request fail if attribute does/not already + // exist. Unfortunately, the constants are platform-specific and + // not exposed by Go1.2. Look for XATTR_CREATE, XATTR_REPLACE. + // + // TODO improve this later + // + // TODO XATTR_CREATE and exist -> EEXIST + // + // TODO XATTR_REPLACE and not exist -> ENODATA + Flags uint32 + + // Offset within extended attributes. + // + // Only valid for OS X, and then only with the resource fork + // attribute. + Position uint32 + + Name string + Xattr []byte +} + +var _ = Request(&SetxattrRequest{}) + +func trunc(b []byte, max int) ([]byte, string) { + if len(b) > max { + return b[:max], "..." + } + return b, "" +} + +func (r *SetxattrRequest) String() string { + xattr, tail := trunc(r.Xattr, 16) + return fmt.Sprintf("Setxattr [%s] %q %x%s fl=%v @%#x", &r.Header, r.Name, xattr, tail, r.Flags, r.Position) +} + +// Respond replies to the request, indicating that the extended attribute was set. +func (r *SetxattrRequest) Respond() { + out := &outHeader{Unique: uint64(r.ID)} + r.respond(out, unsafe.Sizeof(*out)) +} + +func (r *SetxattrRequest) RespondError(err Error) { + err = translateGetxattrError(err) + r.Header.RespondError(err) +} + +// A LookupRequest asks to look up the given name in the directory named by r.Node. +type LookupRequest struct { + Header `json:"-"` + Name string +} + +var _ = Request(&LookupRequest{}) + +func (r *LookupRequest) String() string { + return fmt.Sprintf("Lookup [%s] %q", &r.Header, r.Name) +} + +// Respond replies to the request with the given response. +func (r *LookupRequest) Respond(resp *LookupResponse) { + out := &entryOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + Nodeid: uint64(resp.Node), + Generation: resp.Generation, + EntryValid: uint64(resp.EntryValid / time.Second), + EntryValidNsec: uint32(resp.EntryValid % time.Second / time.Nanosecond), + AttrValid: uint64(resp.AttrValid / time.Second), + AttrValidNsec: uint32(resp.AttrValid % time.Second / time.Nanosecond), + Attr: resp.Attr.attr(), + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +// A LookupResponse is the response to a LookupRequest. +type LookupResponse struct { + Node NodeID + Generation uint64 + EntryValid time.Duration + AttrValid time.Duration + Attr Attr +} + +func (r *LookupResponse) String() string { + return fmt.Sprintf("Lookup %+v", *r) +} + +// An OpenRequest asks to open a file or directory +type OpenRequest struct { + Header `json:"-"` + Dir bool // is this Opendir? + Flags OpenFlags +} + +var _ = Request(&OpenRequest{}) + +func (r *OpenRequest) String() string { + return fmt.Sprintf("Open [%s] dir=%v fl=%v", &r.Header, r.Dir, r.Flags) +} + +// Respond replies to the request with the given response. +func (r *OpenRequest) Respond(resp *OpenResponse) { + out := &openOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + Fh: uint64(resp.Handle), + OpenFlags: uint32(resp.Flags), + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +// A OpenResponse is the response to a OpenRequest. +type OpenResponse struct { + Handle HandleID + Flags OpenResponseFlags +} + +func (r *OpenResponse) String() string { + return fmt.Sprintf("Open %+v", *r) +} + +// A CreateRequest asks to create and open a file (not a directory). +type CreateRequest struct { + Header `json:"-"` + Name string + Flags OpenFlags + Mode os.FileMode +} + +var _ = Request(&CreateRequest{}) + +func (r *CreateRequest) String() string { + return fmt.Sprintf("Create [%s] %q fl=%v mode=%v", &r.Header, r.Name, r.Flags, r.Mode) +} + +// Respond replies to the request with the given response. +func (r *CreateRequest) Respond(resp *CreateResponse) { + out := &createOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + + Nodeid: uint64(resp.Node), + Generation: resp.Generation, + EntryValid: uint64(resp.EntryValid / time.Second), + EntryValidNsec: uint32(resp.EntryValid % time.Second / time.Nanosecond), + AttrValid: uint64(resp.AttrValid / time.Second), + AttrValidNsec: uint32(resp.AttrValid % time.Second / time.Nanosecond), + Attr: resp.Attr.attr(), + + Fh: uint64(resp.Handle), + OpenFlags: uint32(resp.Flags), + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +// A CreateResponse is the response to a CreateRequest. +// It describes the created node and opened handle. +type CreateResponse struct { + LookupResponse + OpenResponse +} + +func (r *CreateResponse) String() string { + return fmt.Sprintf("Create %+v", *r) +} + +// A MkdirRequest asks to create (but not open) a directory. +type MkdirRequest struct { + Header `json:"-"` + Name string + Mode os.FileMode +} + +var _ = Request(&MkdirRequest{}) + +func (r *MkdirRequest) String() string { + return fmt.Sprintf("Mkdir [%s] %q mode=%v", &r.Header, r.Name, r.Mode) +} + +// Respond replies to the request with the given response. +func (r *MkdirRequest) Respond(resp *MkdirResponse) { + out := &entryOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + Nodeid: uint64(resp.Node), + Generation: resp.Generation, + EntryValid: uint64(resp.EntryValid / time.Second), + EntryValidNsec: uint32(resp.EntryValid % time.Second / time.Nanosecond), + AttrValid: uint64(resp.AttrValid / time.Second), + AttrValidNsec: uint32(resp.AttrValid % time.Second / time.Nanosecond), + Attr: resp.Attr.attr(), + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +// A MkdirResponse is the response to a MkdirRequest. +type MkdirResponse struct { + LookupResponse +} + +func (r *MkdirResponse) String() string { + return fmt.Sprintf("Mkdir %+v", *r) +} + +// A ReadRequest asks to read from an open file. +type ReadRequest struct { + Header `json:"-"` + Dir bool // is this Readdir? + Handle HandleID + Offset int64 + Size int +} + +var _ = Request(&ReadRequest{}) + +func (r *ReadRequest) String() string { + return fmt.Sprintf("Read [%s] %#x %d @%#x dir=%v", &r.Header, r.Handle, r.Size, r.Offset, r.Dir) +} + +// Respond replies to the request with the given response. +func (r *ReadRequest) Respond(resp *ReadResponse) { + out := &outHeader{Unique: uint64(r.ID)} + r.respondData(out, unsafe.Sizeof(*out), resp.Data) +} + +// A ReadResponse is the response to a ReadRequest. +type ReadResponse struct { + Data []byte +} + +func (r *ReadResponse) String() string { + return fmt.Sprintf("Read %d", len(r.Data)) +} + +type jsonReadResponse struct { + Len uint64 +} + +func (r *ReadResponse) MarshalJSON() ([]byte, error) { + j := jsonReadResponse{ + Len: uint64(len(r.Data)), + } + return json.Marshal(j) +} + +// A ReleaseRequest asks to release (close) an open file handle. +type ReleaseRequest struct { + Header `json:"-"` + Dir bool // is this Releasedir? + Handle HandleID + Flags OpenFlags // flags from OpenRequest + ReleaseFlags ReleaseFlags + LockOwner uint32 +} + +var _ = Request(&ReleaseRequest{}) + +func (r *ReleaseRequest) String() string { + return fmt.Sprintf("Release [%s] %#x fl=%v rfl=%v owner=%#x", &r.Header, r.Handle, r.Flags, r.ReleaseFlags, r.LockOwner) +} + +// Respond replies to the request, indicating that the handle has been released. +func (r *ReleaseRequest) Respond() { + out := &outHeader{Unique: uint64(r.ID)} + r.respond(out, unsafe.Sizeof(*out)) +} + +// A DestroyRequest is sent by the kernel when unmounting the file system. +// No more requests will be received after this one, but it should still be +// responded to. +type DestroyRequest struct { + Header `json:"-"` +} + +var _ = Request(&DestroyRequest{}) + +func (r *DestroyRequest) String() string { + return fmt.Sprintf("Destroy [%s]", &r.Header) +} + +// Respond replies to the request. +func (r *DestroyRequest) Respond() { + out := &outHeader{Unique: uint64(r.ID)} + r.respond(out, unsafe.Sizeof(*out)) +} + +// A ForgetRequest is sent by the kernel when forgetting about r.Node +// as returned by r.N lookup requests. +type ForgetRequest struct { + Header `json:"-"` + N uint64 +} + +var _ = Request(&ForgetRequest{}) + +func (r *ForgetRequest) String() string { + return fmt.Sprintf("Forget [%s] %d", &r.Header, r.N) +} + +// Respond replies to the request, indicating that the forgetfulness has been recorded. +func (r *ForgetRequest) Respond() { + // Don't reply to forget messages. + r.noResponse() +} + +// A Dirent represents a single directory entry. +type Dirent struct { + // Inode this entry names. + Inode uint64 + + // Type of the entry, for example DT_File. + // + // Setting this is optional. The zero value (DT_Unknown) means + // callers will just need to do a Getattr when the type is + // needed. Providing a type can speed up operations + // significantly. + Type DirentType + + // Name of the entry + Name string +} + +// Type of an entry in a directory listing. +type DirentType uint32 + +const ( + // These don't quite match os.FileMode; especially there's an + // explicit unknown, instead of zero value meaning file. They + // are also not quite syscall.DT_*; nothing says the FUSE + // protocol follows those, and even if they were, we don't + // want each fs to fiddle with syscall. + + // The shift by 12 is hardcoded in the FUSE userspace + // low-level C library, so it's safe here. + + DT_Unknown DirentType = 0 + DT_Socket DirentType = syscall.S_IFSOCK >> 12 + DT_Link DirentType = syscall.S_IFLNK >> 12 + DT_File DirentType = syscall.S_IFREG >> 12 + DT_Block DirentType = syscall.S_IFBLK >> 12 + DT_Dir DirentType = syscall.S_IFDIR >> 12 + DT_Char DirentType = syscall.S_IFCHR >> 12 + DT_FIFO DirentType = syscall.S_IFIFO >> 12 +) + +func (t DirentType) String() string { + switch t { + case DT_Unknown: + return "unknown" + case DT_Socket: + return "socket" + case DT_Link: + return "link" + case DT_File: + return "file" + case DT_Block: + return "block" + case DT_Dir: + return "dir" + case DT_Char: + return "char" + case DT_FIFO: + return "fifo" + } + return "invalid" +} + +// AppendDirent appends the encoded form of a directory entry to data +// and returns the resulting slice. +func AppendDirent(data []byte, dir Dirent) []byte { + de := dirent{ + Ino: dir.Inode, + Namelen: uint32(len(dir.Name)), + Type: uint32(dir.Type), + } + de.Off = uint64(len(data) + direntSize + (len(dir.Name)+7)&^7) + data = append(data, (*[direntSize]byte)(unsafe.Pointer(&de))[:]...) + data = append(data, dir.Name...) + n := direntSize + uintptr(len(dir.Name)) + if n%8 != 0 { + var pad [8]byte + data = append(data, pad[:8-n%8]...) + } + return data +} + +// A WriteRequest asks to write to an open file. +type WriteRequest struct { + Header + Handle HandleID + Offset int64 + Data []byte + Flags WriteFlags +} + +var _ = Request(&WriteRequest{}) + +func (r *WriteRequest) String() string { + return fmt.Sprintf("Write [%s] %#x %d @%d fl=%v", &r.Header, r.Handle, len(r.Data), r.Offset, r.Flags) +} + +type jsonWriteRequest struct { + Handle HandleID + Offset int64 + Len uint64 + Flags WriteFlags +} + +func (r *WriteRequest) MarshalJSON() ([]byte, error) { + j := jsonWriteRequest{ + Handle: r.Handle, + Offset: r.Offset, + Len: uint64(len(r.Data)), + Flags: r.Flags, + } + return json.Marshal(j) +} + +// Respond replies to the request with the given response. +func (r *WriteRequest) Respond(resp *WriteResponse) { + out := &writeOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + Size: uint32(resp.Size), + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +// A WriteResponse replies to a write indicating how many bytes were written. +type WriteResponse struct { + Size int +} + +func (r *WriteResponse) String() string { + return fmt.Sprintf("Write %+v", *r) +} + +// A SetattrRequest asks to change one or more attributes associated with a file, +// as indicated by Valid. +type SetattrRequest struct { + Header `json:"-"` + Valid SetattrValid + Handle HandleID + Size uint64 + Atime time.Time + Mtime time.Time + Mode os.FileMode + Uid uint32 + Gid uint32 + + // OS X only + Bkuptime time.Time + Chgtime time.Time + Crtime time.Time + Flags uint32 // see chflags(2) +} + +var _ = Request(&SetattrRequest{}) + +func (r *SetattrRequest) String() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "Setattr [%s]", &r.Header) + if r.Valid.Mode() { + fmt.Fprintf(&buf, " mode=%v", r.Mode) + } + if r.Valid.Uid() { + fmt.Fprintf(&buf, " uid=%d", r.Uid) + } + if r.Valid.Gid() { + fmt.Fprintf(&buf, " gid=%d", r.Gid) + } + if r.Valid.Size() { + fmt.Fprintf(&buf, " size=%d", r.Size) + } + if r.Valid.Atime() { + fmt.Fprintf(&buf, " atime=%v", r.Atime) + } + if r.Valid.AtimeNow() { + fmt.Fprintf(&buf, " atime=now") + } + if r.Valid.Mtime() { + fmt.Fprintf(&buf, " mtime=%v", r.Mtime) + } + if r.Valid.MtimeNow() { + fmt.Fprintf(&buf, " mtime=now") + } + if r.Valid.Handle() { + fmt.Fprintf(&buf, " handle=%#x", r.Handle) + } else { + fmt.Fprintf(&buf, " handle=INVALID-%#x", r.Handle) + } + if r.Valid.LockOwner() { + fmt.Fprintf(&buf, " lockowner") + } + if r.Valid.Crtime() { + fmt.Fprintf(&buf, " crtime=%v", r.Crtime) + } + if r.Valid.Chgtime() { + fmt.Fprintf(&buf, " chgtime=%v", r.Chgtime) + } + if r.Valid.Bkuptime() { + fmt.Fprintf(&buf, " bkuptime=%v", r.Bkuptime) + } + if r.Valid.Flags() { + fmt.Fprintf(&buf, " flags=%#x", r.Flags) + } + return buf.String() +} + +// Respond replies to the request with the given response, +// giving the updated attributes. +func (r *SetattrRequest) Respond(resp *SetattrResponse) { + out := &attrOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + AttrValid: uint64(resp.AttrValid / time.Second), + AttrValidNsec: uint32(resp.AttrValid % time.Second / time.Nanosecond), + Attr: resp.Attr.attr(), + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +// A SetattrResponse is the response to a SetattrRequest. +type SetattrResponse struct { + AttrValid time.Duration // how long Attr can be cached + Attr Attr // file attributes +} + +func (r *SetattrResponse) String() string { + return fmt.Sprintf("Setattr %+v", *r) +} + +// A FlushRequest asks for the current state of an open file to be flushed +// to storage, as when a file descriptor is being closed. A single opened Handle +// may receive multiple FlushRequests over its lifetime. +type FlushRequest struct { + Header `json:"-"` + Handle HandleID + Flags uint32 + LockOwner uint64 +} + +var _ = Request(&FlushRequest{}) + +func (r *FlushRequest) String() string { + return fmt.Sprintf("Flush [%s] %#x fl=%#x lk=%#x", &r.Header, r.Handle, r.Flags, r.LockOwner) +} + +// Respond replies to the request, indicating that the flush succeeded. +func (r *FlushRequest) Respond() { + out := &outHeader{Unique: uint64(r.ID)} + r.respond(out, unsafe.Sizeof(*out)) +} + +// A RemoveRequest asks to remove a file or directory from the +// directory r.Node. +type RemoveRequest struct { + Header `json:"-"` + Name string // name of the entry to remove + Dir bool // is this rmdir? +} + +var _ = Request(&RemoveRequest{}) + +func (r *RemoveRequest) String() string { + return fmt.Sprintf("Remove [%s] %q dir=%v", &r.Header, r.Name, r.Dir) +} + +// Respond replies to the request, indicating that the file was removed. +func (r *RemoveRequest) Respond() { + out := &outHeader{Unique: uint64(r.ID)} + r.respond(out, unsafe.Sizeof(*out)) +} + +// A SymlinkRequest is a request to create a symlink making NewName point to Target. +type SymlinkRequest struct { + Header `json:"-"` + NewName, Target string +} + +var _ = Request(&SymlinkRequest{}) + +func (r *SymlinkRequest) String() string { + return fmt.Sprintf("Symlink [%s] from %q to target %q", &r.Header, r.NewName, r.Target) +} + +// Respond replies to the request, indicating that the symlink was created. +func (r *SymlinkRequest) Respond(resp *SymlinkResponse) { + out := &entryOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + Nodeid: uint64(resp.Node), + Generation: resp.Generation, + EntryValid: uint64(resp.EntryValid / time.Second), + EntryValidNsec: uint32(resp.EntryValid % time.Second / time.Nanosecond), + AttrValid: uint64(resp.AttrValid / time.Second), + AttrValidNsec: uint32(resp.AttrValid % time.Second / time.Nanosecond), + Attr: resp.Attr.attr(), + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +// A SymlinkResponse is the response to a SymlinkRequest. +type SymlinkResponse struct { + LookupResponse +} + +// A ReadlinkRequest is a request to read a symlink's target. +type ReadlinkRequest struct { + Header `json:"-"` +} + +var _ = Request(&ReadlinkRequest{}) + +func (r *ReadlinkRequest) String() string { + return fmt.Sprintf("Readlink [%s]", &r.Header) +} + +func (r *ReadlinkRequest) Respond(target string) { + out := &outHeader{Unique: uint64(r.ID)} + r.respondData(out, unsafe.Sizeof(*out), []byte(target)) +} + +// A LinkRequest is a request to create a hard link. +type LinkRequest struct { + Header `json:"-"` + OldNode NodeID + NewName string +} + +var _ = Request(&LinkRequest{}) + +func (r *LinkRequest) String() string { + return fmt.Sprintf("Link [%s] node %d to %q", &r.Header, r.OldNode, r.NewName) +} + +func (r *LinkRequest) Respond(resp *LookupResponse) { + out := &entryOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + Nodeid: uint64(resp.Node), + Generation: resp.Generation, + EntryValid: uint64(resp.EntryValid / time.Second), + EntryValidNsec: uint32(resp.EntryValid % time.Second / time.Nanosecond), + AttrValid: uint64(resp.AttrValid / time.Second), + AttrValidNsec: uint32(resp.AttrValid % time.Second / time.Nanosecond), + Attr: resp.Attr.attr(), + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +// A RenameRequest is a request to rename a file. +type RenameRequest struct { + Header `json:"-"` + NewDir NodeID + OldName, NewName string +} + +var _ = Request(&RenameRequest{}) + +func (r *RenameRequest) String() string { + return fmt.Sprintf("Rename [%s] from %q to dirnode %d %q", &r.Header, r.OldName, r.NewDir, r.NewName) +} + +func (r *RenameRequest) Respond() { + out := &outHeader{Unique: uint64(r.ID)} + r.respond(out, unsafe.Sizeof(*out)) +} + +type MknodRequest struct { + Header `json:"-"` + Name string + Mode os.FileMode + Rdev uint32 +} + +var _ = Request(&MknodRequest{}) + +func (r *MknodRequest) String() string { + return fmt.Sprintf("Mknod [%s] Name %q mode %v rdev %d", &r.Header, r.Name, r.Mode, r.Rdev) +} + +func (r *MknodRequest) Respond(resp *LookupResponse) { + out := &entryOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + Nodeid: uint64(resp.Node), + Generation: resp.Generation, + EntryValid: uint64(resp.EntryValid / time.Second), + EntryValidNsec: uint32(resp.EntryValid % time.Second / time.Nanosecond), + AttrValid: uint64(resp.AttrValid / time.Second), + AttrValidNsec: uint32(resp.AttrValid % time.Second / time.Nanosecond), + Attr: resp.Attr.attr(), + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +type FsyncRequest struct { + Header `json:"-"` + Handle HandleID + // TODO bit 1 is datasync, not well documented upstream + Flags uint32 + Dir bool +} + +var _ = Request(&FsyncRequest{}) + +func (r *FsyncRequest) String() string { + return fmt.Sprintf("Fsync [%s] Handle %v Flags %v", &r.Header, r.Handle, r.Flags) +} + +func (r *FsyncRequest) Respond() { + out := &outHeader{Unique: uint64(r.ID)} + r.respond(out, unsafe.Sizeof(*out)) +} + +// An InterruptRequest is a request to interrupt another pending request. The +// response to that request should return an error status of EINTR. +type InterruptRequest struct { + Header `json:"-"` + IntrID RequestID // ID of the request to be interrupt. +} + +var _ = Request(&InterruptRequest{}) + +func (r *InterruptRequest) Respond() { + // nothing to do here + r.noResponse() +} + +func (r *InterruptRequest) String() string { + return fmt.Sprintf("Interrupt [%s] ID %v", &r.Header, r.IntrID) +} + +/*{ + +// A XXXRequest xxx. +type XXXRequest struct { + Header `json:"-"` + xxx +} + +var _ = Request(&XXXRequest{}) + +func (r *XXXRequest) String() string { + return fmt.Sprintf("XXX [%s] xxx", &r.Header) +} + +// Respond replies to the request with the given response. +func (r *XXXRequest) Respond(resp *XXXResponse) { + out := &xxxOut{ + outHeader: outHeader{Unique: uint64(r.ID)}, + xxx, + } + r.respond(&out.outHeader, unsafe.Sizeof(*out)) +} + +// A XXXResponse is the response to a XXXRequest. +type XXXResponse struct { + xxx +} + +func (r *XXXResponse) String() string { + return fmt.Sprintf("XXX %+v", *r) +} + + } +*/ diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel.go new file mode 100644 index 00000000..5fba53db --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel.go @@ -0,0 +1,639 @@ +// See the file LICENSE for copyright and licensing information. + +// Derived from FUSE's fuse_kernel.h +/* + This file defines the kernel interface of FUSE + Copyright (C) 2001-2007 Miklos Szeredi + + + This -- and only this -- header file may also be distributed under + the terms of the BSD Licence as follows: + + Copyright (C) 2001-2007 Miklos Szeredi. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. +*/ + +package fuse + +import ( + "fmt" + "syscall" + "unsafe" +) + +// Version is the FUSE version implemented by the package. +const Version = "7.8" + +const ( + kernelVersion = 7 + kernelMinorVersion = 8 + rootID = 1 +) + +type kstatfs struct { + Blocks uint64 + Bfree uint64 + Bavail uint64 + Files uint64 + Ffree uint64 + Bsize uint32 + Namelen uint32 + Frsize uint32 + Padding uint32 + Spare [6]uint32 +} + +type fileLock struct { + Start uint64 + End uint64 + Type uint32 + Pid uint32 +} + +// The SetattrValid are bit flags describing which fields in the SetattrRequest +// are included in the change. +type SetattrValid uint32 + +const ( + SetattrMode SetattrValid = 1 << 0 + SetattrUid SetattrValid = 1 << 1 + SetattrGid SetattrValid = 1 << 2 + SetattrSize SetattrValid = 1 << 3 + SetattrAtime SetattrValid = 1 << 4 + SetattrMtime SetattrValid = 1 << 5 + SetattrHandle SetattrValid = 1 << 6 + + // Linux only(?) + SetattrAtimeNow SetattrValid = 1 << 7 + SetattrMtimeNow SetattrValid = 1 << 8 + SetattrLockOwner SetattrValid = 1 << 9 // http://www.mail-archive.com/git-commits-head@vger.kernel.org/msg27852.html + + // OS X only + SetattrCrtime SetattrValid = 1 << 28 + SetattrChgtime SetattrValid = 1 << 29 + SetattrBkuptime SetattrValid = 1 << 30 + SetattrFlags SetattrValid = 1 << 31 +) + +func (fl SetattrValid) Mode() bool { return fl&SetattrMode != 0 } +func (fl SetattrValid) Uid() bool { return fl&SetattrUid != 0 } +func (fl SetattrValid) Gid() bool { return fl&SetattrGid != 0 } +func (fl SetattrValid) Size() bool { return fl&SetattrSize != 0 } +func (fl SetattrValid) Atime() bool { return fl&SetattrAtime != 0 } +func (fl SetattrValid) Mtime() bool { return fl&SetattrMtime != 0 } +func (fl SetattrValid) Handle() bool { return fl&SetattrHandle != 0 } +func (fl SetattrValid) AtimeNow() bool { return fl&SetattrAtimeNow != 0 } +func (fl SetattrValid) MtimeNow() bool { return fl&SetattrMtimeNow != 0 } +func (fl SetattrValid) LockOwner() bool { return fl&SetattrLockOwner != 0 } +func (fl SetattrValid) Crtime() bool { return fl&SetattrCrtime != 0 } +func (fl SetattrValid) Chgtime() bool { return fl&SetattrChgtime != 0 } +func (fl SetattrValid) Bkuptime() bool { return fl&SetattrBkuptime != 0 } +func (fl SetattrValid) Flags() bool { return fl&SetattrFlags != 0 } + +func (fl SetattrValid) String() string { + return flagString(uint32(fl), setattrValidNames) +} + +var setattrValidNames = []flagName{ + {uint32(SetattrMode), "SetattrMode"}, + {uint32(SetattrUid), "SetattrUid"}, + {uint32(SetattrGid), "SetattrGid"}, + {uint32(SetattrSize), "SetattrSize"}, + {uint32(SetattrAtime), "SetattrAtime"}, + {uint32(SetattrMtime), "SetattrMtime"}, + {uint32(SetattrHandle), "SetattrHandle"}, + {uint32(SetattrAtimeNow), "SetattrAtimeNow"}, + {uint32(SetattrMtimeNow), "SetattrMtimeNow"}, + {uint32(SetattrLockOwner), "SetattrLockOwner"}, + {uint32(SetattrCrtime), "SetattrCrtime"}, + {uint32(SetattrChgtime), "SetattrChgtime"}, + {uint32(SetattrBkuptime), "SetattrBkuptime"}, + {uint32(SetattrFlags), "SetattrFlags"}, +} + +// Flags that can be seen in OpenRequest.Flags. +const ( + // Access modes. These are not 1-bit flags, but alternatives where + // only one can be chosen. See the IsReadOnly etc convenience + // methods. + OpenReadOnly OpenFlags = syscall.O_RDONLY + OpenWriteOnly OpenFlags = syscall.O_WRONLY + OpenReadWrite OpenFlags = syscall.O_RDWR + + OpenAppend OpenFlags = syscall.O_APPEND + OpenCreate OpenFlags = syscall.O_CREAT + OpenExclusive OpenFlags = syscall.O_EXCL + OpenSync OpenFlags = syscall.O_SYNC + OpenTruncate OpenFlags = syscall.O_TRUNC +) + +// OpenAccessModeMask is a bitmask that separates the access mode +// from the other flags in OpenFlags. +const OpenAccessModeMask OpenFlags = syscall.O_ACCMODE + +// OpenFlags are the O_FOO flags passed to open/create/etc calls. For +// example, os.O_WRONLY | os.O_APPEND. +type OpenFlags uint32 + +func (fl OpenFlags) String() string { + // O_RDONLY, O_RWONLY, O_RDWR are not flags + s := accModeName(fl & OpenAccessModeMask) + flags := uint32(fl &^ OpenAccessModeMask) + if flags != 0 { + s = s + "+" + flagString(flags, openFlagNames) + } + return s +} + +// Return true if OpenReadOnly is set. +func (fl OpenFlags) IsReadOnly() bool { + return fl&OpenAccessModeMask == OpenReadOnly +} + +// Return true if OpenWriteOnly is set. +func (fl OpenFlags) IsWriteOnly() bool { + return fl&OpenAccessModeMask == OpenWriteOnly +} + +// Return true if OpenReadWrite is set. +func (fl OpenFlags) IsReadWrite() bool { + return fl&OpenAccessModeMask == OpenReadWrite +} + +func accModeName(flags OpenFlags) string { + switch flags { + case OpenReadOnly: + return "OpenReadOnly" + case OpenWriteOnly: + return "OpenWriteOnly" + case OpenReadWrite: + return "OpenReadWrite" + default: + return "" + } +} + +var openFlagNames = []flagName{ + {uint32(OpenCreate), "OpenCreate"}, + {uint32(OpenExclusive), "OpenExclusive"}, + {uint32(OpenTruncate), "OpenTruncate"}, + {uint32(OpenAppend), "OpenAppend"}, + {uint32(OpenSync), "OpenSync"}, +} + +// The OpenResponseFlags are returned in the OpenResponse. +type OpenResponseFlags uint32 + +const ( + OpenDirectIO OpenResponseFlags = 1 << 0 // bypass page cache for this open file + OpenKeepCache OpenResponseFlags = 1 << 1 // don't invalidate the data cache on open + OpenNonSeekable OpenResponseFlags = 1 << 2 // (Linux?) + + OpenPurgeAttr OpenResponseFlags = 1 << 30 // OS X + OpenPurgeUBC OpenResponseFlags = 1 << 31 // OS X +) + +func (fl OpenResponseFlags) String() string { + return flagString(uint32(fl), openResponseFlagNames) +} + +var openResponseFlagNames = []flagName{ + {uint32(OpenDirectIO), "OpenDirectIO"}, + {uint32(OpenKeepCache), "OpenKeepCache"}, + {uint32(OpenPurgeAttr), "OpenPurgeAttr"}, + {uint32(OpenPurgeUBC), "OpenPurgeUBC"}, +} + +// The InitFlags are used in the Init exchange. +type InitFlags uint32 + +const ( + InitAsyncRead InitFlags = 1 << 0 + InitPosixLocks InitFlags = 1 << 1 + InitFileOps InitFlags = 1 << 2 + InitAtomicTrunc InitFlags = 1 << 3 + InitExportSupport InitFlags = 1 << 4 + InitBigWrites InitFlags = 1 << 5 + InitDontMask InitFlags = 1 << 6 + InitSpliceWrite InitFlags = 1 << 7 + InitSpliceMove InitFlags = 1 << 8 + InitSpliceRead InitFlags = 1 << 9 + InitFlockLocks InitFlags = 1 << 10 + InitHasIoctlDir InitFlags = 1 << 11 + InitAutoInvalData InitFlags = 1 << 12 + InitDoReaddirplus InitFlags = 1 << 13 + InitReaddirplusAuto InitFlags = 1 << 14 + InitAsyncDIO InitFlags = 1 << 15 + InitWritebackCache InitFlags = 1 << 16 + InitNoOpenSupport InitFlags = 1 << 17 + + InitCaseSensitive InitFlags = 1 << 29 // OS X only + InitVolRename InitFlags = 1 << 30 // OS X only + InitXtimes InitFlags = 1 << 31 // OS X only +) + +type flagName struct { + bit uint32 + name string +} + +var initFlagNames = []flagName{ + {uint32(InitAsyncRead), "InitAsyncRead"}, + {uint32(InitPosixLocks), "InitPosixLocks"}, + {uint32(InitFileOps), "InitFileOps"}, + {uint32(InitAtomicTrunc), "InitAtomicTrunc"}, + {uint32(InitExportSupport), "InitExportSupport"}, + {uint32(InitBigWrites), "InitBigWrites"}, + {uint32(InitDontMask), "InitDontMask"}, + {uint32(InitSpliceWrite), "InitSpliceWrite"}, + {uint32(InitSpliceMove), "InitSpliceMove"}, + {uint32(InitSpliceRead), "InitSpliceRead"}, + {uint32(InitFlockLocks), "InitFlockLocks"}, + {uint32(InitHasIoctlDir), "InitHasIoctlDir"}, + {uint32(InitAutoInvalData), "InitAutoInvalData"}, + {uint32(InitDoReaddirplus), "InitDoReaddirplus"}, + {uint32(InitReaddirplusAuto), "InitReaddirplusAuto"}, + {uint32(InitAsyncDIO), "InitAsyncDIO"}, + {uint32(InitWritebackCache), "InitWritebackCache"}, + {uint32(InitNoOpenSupport), "InitNoOpenSupport"}, + + {uint32(InitCaseSensitive), "InitCaseSensitive"}, + {uint32(InitVolRename), "InitVolRename"}, + {uint32(InitXtimes), "InitXtimes"}, +} + +func (fl InitFlags) String() string { + return flagString(uint32(fl), initFlagNames) +} + +func flagString(f uint32, names []flagName) string { + var s string + + if f == 0 { + return "0" + } + + for _, n := range names { + if f&n.bit != 0 { + s += "+" + n.name + f &^= n.bit + } + } + if f != 0 { + s += fmt.Sprintf("%+#x", f) + } + return s[1:] +} + +// The ReleaseFlags are used in the Release exchange. +type ReleaseFlags uint32 + +const ( + ReleaseFlush ReleaseFlags = 1 << 0 +) + +func (fl ReleaseFlags) String() string { + return flagString(uint32(fl), releaseFlagNames) +} + +var releaseFlagNames = []flagName{ + {uint32(ReleaseFlush), "ReleaseFlush"}, +} + +// Opcodes +const ( + opLookup = 1 + opForget = 2 // no reply + opGetattr = 3 + opSetattr = 4 + opReadlink = 5 + opSymlink = 6 + opMknod = 8 + opMkdir = 9 + opUnlink = 10 + opRmdir = 11 + opRename = 12 + opLink = 13 + opOpen = 14 + opRead = 15 + opWrite = 16 + opStatfs = 17 + opRelease = 18 + opFsync = 20 + opSetxattr = 21 + opGetxattr = 22 + opListxattr = 23 + opRemovexattr = 24 + opFlush = 25 + opInit = 26 + opOpendir = 27 + opReaddir = 28 + opReleasedir = 29 + opFsyncdir = 30 + opGetlk = 31 + opSetlk = 32 + opSetlkw = 33 + opAccess = 34 + opCreate = 35 + opInterrupt = 36 + opBmap = 37 + opDestroy = 38 + opIoctl = 39 // Linux? + opPoll = 40 // Linux? + + // OS X + opSetvolname = 61 + opGetxtimes = 62 + opExchange = 63 +) + +type entryOut struct { + outHeader + Nodeid uint64 // Inode ID + Generation uint64 // Inode generation + EntryValid uint64 // Cache timeout for the name + AttrValid uint64 // Cache timeout for the attributes + EntryValidNsec uint32 + AttrValidNsec uint32 + Attr attr +} + +type forgetIn struct { + Nlookup uint64 +} + +type attrOut struct { + outHeader + AttrValid uint64 // Cache timeout for the attributes + AttrValidNsec uint32 + Dummy uint32 + Attr attr +} + +// OS X +type getxtimesOut struct { + outHeader + Bkuptime uint64 + Crtime uint64 + BkuptimeNsec uint32 + CrtimeNsec uint32 +} + +type mknodIn struct { + Mode uint32 + Rdev uint32 + // "filename\x00" follows. +} + +type mkdirIn struct { + Mode uint32 + Padding uint32 + // filename follows +} + +type renameIn struct { + Newdir uint64 + // "oldname\x00newname\x00" follows +} + +// OS X +type exchangeIn struct { + Olddir uint64 + Newdir uint64 + Options uint64 +} + +type linkIn struct { + Oldnodeid uint64 +} + +type setattrInCommon struct { + Valid uint32 + Padding uint32 + Fh uint64 + Size uint64 + LockOwner uint64 // unused on OS X? + Atime uint64 + Mtime uint64 + Unused2 uint64 + AtimeNsec uint32 + MtimeNsec uint32 + Unused3 uint32 + Mode uint32 + Unused4 uint32 + Uid uint32 + Gid uint32 + Unused5 uint32 +} + +type openIn struct { + Flags uint32 + Unused uint32 +} + +type openOut struct { + outHeader + Fh uint64 + OpenFlags uint32 + Padding uint32 +} + +type createIn struct { + Flags uint32 + Mode uint32 +} + +type createOut struct { + outHeader + + Nodeid uint64 // Inode ID + Generation uint64 // Inode generation + EntryValid uint64 // Cache timeout for the name + AttrValid uint64 // Cache timeout for the attributes + EntryValidNsec uint32 + AttrValidNsec uint32 + Attr attr + + Fh uint64 + OpenFlags uint32 + Padding uint32 +} + +type releaseIn struct { + Fh uint64 + Flags uint32 + ReleaseFlags uint32 + LockOwner uint32 +} + +type flushIn struct { + Fh uint64 + FlushFlags uint32 + Padding uint32 + LockOwner uint64 +} + +type readIn struct { + Fh uint64 + Offset uint64 + Size uint32 + Padding uint32 +} + +type writeIn struct { + Fh uint64 + Offset uint64 + Size uint32 + WriteFlags uint32 +} + +type writeOut struct { + outHeader + Size uint32 + Padding uint32 +} + +// The WriteFlags are passed in WriteRequest. +type WriteFlags uint32 + +func (fl WriteFlags) String() string { + return flagString(uint32(fl), writeFlagNames) +} + +var writeFlagNames = []flagName{} + +const compatStatfsSize = 48 + +type statfsOut struct { + outHeader + St kstatfs +} + +type fsyncIn struct { + Fh uint64 + FsyncFlags uint32 + Padding uint32 +} + +type setxattrInCommon struct { + Size uint32 + Flags uint32 +} + +func (setxattrInCommon) position() uint32 { + return 0 +} + +type getxattrInCommon struct { + Size uint32 + Padding uint32 +} + +func (getxattrInCommon) position() uint32 { + return 0 +} + +type getxattrOut struct { + outHeader + Size uint32 + Padding uint32 +} + +type lkIn struct { + Fh uint64 + Owner uint64 + Lk fileLock +} + +type lkOut struct { + outHeader + Lk fileLock +} + +type accessIn struct { + Mask uint32 + Padding uint32 +} + +type initIn struct { + Major uint32 + Minor uint32 + MaxReadahead uint32 + Flags uint32 +} + +const initInSize = int(unsafe.Sizeof(initIn{})) + +type initOut struct { + outHeader + Major uint32 + Minor uint32 + MaxReadahead uint32 + Flags uint32 + Unused uint32 + MaxWrite uint32 +} + +type interruptIn struct { + Unique uint64 +} + +type bmapIn struct { + Block uint64 + BlockSize uint32 + Padding uint32 +} + +type bmapOut struct { + outHeader + Block uint64 +} + +type inHeader struct { + Len uint32 + Opcode uint32 + Unique uint64 + Nodeid uint64 + Uid uint32 + Gid uint32 + Pid uint32 + Padding uint32 +} + +const inHeaderSize = int(unsafe.Sizeof(inHeader{})) + +type outHeader struct { + Len uint32 + Error int32 + Unique uint64 +} + +type dirent struct { + Ino uint64 + Off uint64 + Namelen uint32 + Type uint32 + Name [0]byte +} + +const direntSize = 8 + 8 + 4 + 4 diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_darwin.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_darwin.go new file mode 100644 index 00000000..4f9347d0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_darwin.go @@ -0,0 +1,86 @@ +package fuse + +import ( + "time" +) + +type attr struct { + Ino uint64 + Size uint64 + Blocks uint64 + Atime uint64 + Mtime uint64 + Ctime uint64 + Crtime_ uint64 // OS X only + AtimeNsec uint32 + MtimeNsec uint32 + CtimeNsec uint32 + CrtimeNsec uint32 // OS X only + Mode uint32 + Nlink uint32 + Uid uint32 + Gid uint32 + Rdev uint32 + Flags_ uint32 // OS X only; see chflags(2) +} + +func (a *attr) SetCrtime(s uint64, ns uint32) { + a.Crtime_, a.CrtimeNsec = s, ns +} + +func (a *attr) SetFlags(f uint32) { + a.Flags_ = f +} + +type setattrIn struct { + setattrInCommon + + // OS X only + Bkuptime_ uint64 + Chgtime_ uint64 + Crtime uint64 + BkuptimeNsec uint32 + ChgtimeNsec uint32 + CrtimeNsec uint32 + Flags_ uint32 // see chflags(2) +} + +func (in *setattrIn) BkupTime() time.Time { + return time.Unix(int64(in.Bkuptime_), int64(in.BkuptimeNsec)) +} + +func (in *setattrIn) Chgtime() time.Time { + return time.Unix(int64(in.Chgtime_), int64(in.ChgtimeNsec)) +} + +func (in *setattrIn) Flags() uint32 { + return in.Flags_ +} + +func openFlags(flags uint32) OpenFlags { + return OpenFlags(flags) +} + +type getxattrIn struct { + getxattrInCommon + + // OS X only + Position uint32 + Padding uint32 +} + +func (g *getxattrIn) position() uint32 { + return g.Position +} + +type setxattrIn struct { + setxattrInCommon + + // OS X only + Position uint32 + Padding uint32 +} + +func (s *setxattrIn) position() uint32 { + return s.Position +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_linux.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_linux.go new file mode 100644 index 00000000..6a752457 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_linux.go @@ -0,0 +1,70 @@ +package fuse + +import "time" + +type attr struct { + Ino uint64 + Size uint64 + Blocks uint64 + Atime uint64 + Mtime uint64 + Ctime uint64 + AtimeNsec uint32 + MtimeNsec uint32 + CtimeNsec uint32 + Mode uint32 + Nlink uint32 + Uid uint32 + Gid uint32 + Rdev uint32 + // Blksize uint32 // Only in protocol 7.9 + // padding_ uint32 // Only in protocol 7.9 +} + +func (a *attr) Crtime() time.Time { + return time.Time{} +} + +func (a *attr) SetCrtime(s uint64, ns uint32) { + // Ignored on Linux. +} + +func (a *attr) SetFlags(f uint32) { + // Ignored on Linux. +} + +type setattrIn struct { + setattrInCommon +} + +func (in *setattrIn) BkupTime() time.Time { + return time.Time{} +} + +func (in *setattrIn) Chgtime() time.Time { + return time.Time{} +} + +func (in *setattrIn) Flags() uint32 { + return 0 +} + +func openFlags(flags uint32) OpenFlags { + // on amd64, the 32-bit O_LARGEFILE flag is always seen; + // on i386, the flag probably depends on the app + // requesting, but in any case should be utterly + // uninteresting to us here; our kernel protocol messages + // are not directly related to the client app's kernel + // API/ABI + flags &^= 0x8000 + + return OpenFlags(flags) +} + +type getxattrIn struct { + getxattrInCommon +} + +type setxattrIn struct { + setxattrInCommon +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_std.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_std.go new file mode 100644 index 00000000..074cfd32 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_std.go @@ -0,0 +1 @@ +package fuse diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_test.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_test.go new file mode 100644 index 00000000..9b5727fd --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuse_kernel_test.go @@ -0,0 +1,31 @@ +package fuse_test + +import ( + "os" + "testing" + + "camlistore.org/third_party/bazil.org/fuse" +) + +func TestOpenFlagsAccmodeMask(t *testing.T) { + var f = fuse.OpenFlags(os.O_RDWR | os.O_SYNC) + if g, e := f&fuse.OpenAccessModeMask, fuse.OpenReadWrite; g != e { + t.Fatalf("OpenAccessModeMask behaves wrong: %v: %o != %o", f, g, e) + } + if f.IsReadOnly() { + t.Fatalf("IsReadOnly is wrong: %v", f) + } + if f.IsWriteOnly() { + t.Fatalf("IsWriteOnly is wrong: %v", f) + } + if !f.IsReadWrite() { + t.Fatalf("IsReadWrite is wrong: %v", f) + } +} + +func TestOpenFlagsString(t *testing.T) { + var f = fuse.OpenFlags(os.O_RDWR | os.O_SYNC | os.O_APPEND) + if g, e := f.String(), "OpenReadWrite+OpenAppend+OpenSync"; g != e { + t.Fatalf("OpenFlags.String: %q != %q", g, e) + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuseutil/fuseutil.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuseutil/fuseutil.go new file mode 100644 index 00000000..04c5a8ab --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/fuseutil/fuseutil.go @@ -0,0 +1,20 @@ +package fuseutil + +import ( + "camlistore.org/third_party/bazil.org/fuse" +) + +// HandleRead handles a read request assuming that data is the entire file content. +// It adjusts the amount returned in resp according to req.Offset and req.Size. +func HandleRead(req *fuse.ReadRequest, resp *fuse.ReadResponse, data []byte) { + if req.Offset >= int64(len(data)) { + data = nil + } else { + data = data[req.Offset:] + } + if len(data) > req.Size { + data = data[:req.Size] + } + n := copy(resp.Data[:req.Size], data) + resp.Data = resp.Data[:n] +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/hellofs/hello.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/hellofs/hello.go new file mode 100644 index 00000000..c6292241 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/hellofs/hello.go @@ -0,0 +1,95 @@ +// Hellofs implements a simple "hello world" file system. +package main + +import ( + "flag" + "fmt" + "log" + "os" + + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs" + _ "camlistore.org/third_party/bazil.org/fuse/fs/fstestutil" +) + +var Usage = func() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " %s MOUNTPOINT\n", os.Args[0]) + flag.PrintDefaults() +} + +func main() { + flag.Usage = Usage + flag.Parse() + + if flag.NArg() != 1 { + Usage() + os.Exit(2) + } + mountpoint := flag.Arg(0) + + c, err := fuse.Mount( + mountpoint, + fuse.FSName("helloworld"), + fuse.Subtype("hellofs"), + fuse.LocalVolume(), + fuse.VolumeName("Hello world!"), + ) + if err != nil { + log.Fatal(err) + } + defer c.Close() + + err = fs.Serve(c, FS{}) + if err != nil { + log.Fatal(err) + } + + // check if the mount process has an error to report + <-c.Ready + if err := c.MountError; err != nil { + log.Fatal(err) + } +} + +// FS implements the hello world file system. +type FS struct{} + +func (FS) Root() (fs.Node, fuse.Error) { + return Dir{}, nil +} + +// Dir implements both Node and Handle for the root directory. +type Dir struct{} + +func (Dir) Attr() fuse.Attr { + return fuse.Attr{Inode: 1, Mode: os.ModeDir | 0555} +} + +func (Dir) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) { + if name == "hello" { + return File{}, nil + } + return nil, fuse.ENOENT +} + +var dirDirs = []fuse.Dirent{ + {Inode: 2, Name: "hello", Type: fuse.DT_File}, +} + +func (Dir) ReadDir(intr fs.Intr) ([]fuse.Dirent, fuse.Error) { + return dirDirs, nil +} + +// File implements both Node and Handle for the hello file. +type File struct{} + +const greeting = "hello, world\n" + +func (File) Attr() fuse.Attr { + return fuse.Attr{Inode: 2, Mode: 0444, Size: uint64(len(greeting))} +} + +func (File) ReadAll(intr fs.Intr) ([]byte, fuse.Error) { + return []byte(greeting), nil +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/mount_darwin.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/mount_darwin.go new file mode 100644 index 00000000..6253ce82 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/mount_darwin.go @@ -0,0 +1,126 @@ +package fuse + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + "syscall" +) + +var errNoAvail = errors.New("no available fuse devices") + +var errNotLoaded = errors.New("osxfusefs is not loaded") + +func loadOSXFUSE() error { + cmd := exec.Command("/Library/Filesystems/osxfusefs.fs/Support/load_osxfusefs") + cmd.Dir = "/" + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + return err +} + +func openOSXFUSEDev() (*os.File, error) { + var f *os.File + var err error + for i := uint64(0); ; i++ { + path := "/dev/osxfuse" + strconv.FormatUint(i, 10) + f, err = os.OpenFile(path, os.O_RDWR, 0000) + if os.IsNotExist(err) { + if i == 0 { + // not even the first device was found -> fuse is not loaded + return nil, errNotLoaded + } + + // we've run out of kernel-provided devices + return nil, errNoAvail + } + + if err2, ok := err.(*os.PathError); ok && err2.Err == syscall.EBUSY { + // try the next one + continue + } + + if err != nil { + return nil, err + } + return f, nil + } +} + +func callMount(dir string, conf *MountConfig, f *os.File, ready chan<- struct{}, errp *error) error { + bin := "/Library/Filesystems/osxfusefs.fs/Support/mount_osxfusefs" + + for k, v := range conf.options { + if strings.Contains(k, ",") || strings.Contains(v, ",") { + // Silly limitation but the mount helper does not + // understand any escaping. See TestMountOptionCommaError. + return fmt.Errorf("mount options cannot contain commas on OS X: %q=%q", k, v) + } + } + cmd := exec.Command( + bin, + "-o", conf.getOptions(), + // Tell osxfuse-kext how large our buffer is. It must split + // writes larger than this into multiple writes. + // + // OSXFUSE seems to ignore InitResponse.MaxWrite, and uses + // this instead. + "-o", "iosize="+strconv.FormatUint(maxWrite, 10), + // refers to fd passed in cmd.ExtraFiles + "3", + dir, + ) + cmd.ExtraFiles = []*os.File{f} + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "MOUNT_FUSEFS_CALL_BY_LIB=") + // TODO this is used for fs typenames etc, let app influence it + cmd.Env = append(cmd.Env, "MOUNT_FUSEFS_DAEMON_PATH="+bin) + var buf bytes.Buffer + cmd.Stdout = &buf + cmd.Stderr = &buf + + err := cmd.Start() + if err != nil { + return err + } + go func() { + err = cmd.Wait() + if err != nil { + if buf.Len() > 0 { + output := buf.Bytes() + output = bytes.TrimRight(output, "\n") + msg := err.Error() + ": " + string(output) + err = errors.New(msg) + } + } + *errp = err + close(ready) + }() + return err +} + +func mount(dir string, conf *MountConfig, ready chan<- struct{}, errp *error) (*os.File, error) { + f, err := openOSXFUSEDev() + if err == errNotLoaded { + err = loadOSXFUSE() + if err != nil { + return nil, err + } + // try again + f, err = openOSXFUSEDev() + } + if err != nil { + return nil, err + } + err = callMount(dir, conf, f, ready, errp) + if err != nil { + f.Close() + return nil, err + } + return f, nil +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/mount_linux.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/mount_linux.go new file mode 100644 index 00000000..0748c0a5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/mount_linux.go @@ -0,0 +1,72 @@ +package fuse + +import ( + "fmt" + "net" + "os" + "os/exec" + "syscall" +) + +func mount(dir string, conf *MountConfig, ready chan<- struct{}, errp *error) (fusefd *os.File, err error) { + // linux mount is never delayed + close(ready) + + fds, err := syscall.Socketpair(syscall.AF_FILE, syscall.SOCK_STREAM, 0) + if err != nil { + return nil, fmt.Errorf("socketpair error: %v", err) + } + defer syscall.Close(fds[0]) + defer syscall.Close(fds[1]) + + cmd := exec.Command( + "fusermount", + "-o", conf.getOptions(), + "--", + dir, + ) + cmd.Env = append(os.Environ(), "_FUSE_COMMFD=3") + + writeFile := os.NewFile(uintptr(fds[0]), "fusermount-child-writes") + defer writeFile.Close() + cmd.ExtraFiles = []*os.File{writeFile} + + out, err := cmd.CombinedOutput() + if len(out) > 0 || err != nil { + return nil, fmt.Errorf("fusermount: %q, %v", out, err) + } + + readFile := os.NewFile(uintptr(fds[1]), "fusermount-parent-reads") + defer readFile.Close() + c, err := net.FileConn(readFile) + if err != nil { + return nil, fmt.Errorf("FileConn from fusermount socket: %v", err) + } + defer c.Close() + + uc, ok := c.(*net.UnixConn) + if !ok { + return nil, fmt.Errorf("unexpected FileConn type; expected UnixConn, got %T", c) + } + + buf := make([]byte, 32) // expect 1 byte + oob := make([]byte, 32) // expect 24 bytes + _, oobn, _, _, err := uc.ReadMsgUnix(buf, oob) + scms, err := syscall.ParseSocketControlMessage(oob[:oobn]) + if err != nil { + return nil, fmt.Errorf("ParseSocketControlMessage: %v", err) + } + if len(scms) != 1 { + return nil, fmt.Errorf("expected 1 SocketControlMessage; got scms = %#v", scms) + } + scm := scms[0] + gotFds, err := syscall.ParseUnixRights(&scm) + if err != nil { + return nil, fmt.Errorf("syscall.ParseUnixRights: %v", err) + } + if len(gotFds) != 1 { + return nil, fmt.Errorf("wanted 1 fd; got %#v", gotFds) + } + f := os.NewFile(uintptr(gotFds[0]), "/dev/fuse") + return f, nil +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options.go new file mode 100644 index 00000000..643a9492 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options.go @@ -0,0 +1,100 @@ +package fuse + +import ( + "errors" + "strings" +) + +// MountConfig holds the configuration for a mount operation. +// Use it by passing MountOption values to Mount. +type MountConfig struct { + options map[string]string +} + +func escapeComma(s string) string { + s = strings.Replace(s, `\`, `\\`, -1) + s = strings.Replace(s, `,`, `\,`, -1) + return s +} + +// getOptions makes a string of options suitable for passing to FUSE +// mount flag `-o`. Returns an empty string if no options were set. +// Any platform specific adjustments should happen before the call. +func (m *MountConfig) getOptions() string { + var opts []string + for k, v := range m.options { + k = escapeComma(k) + if v != "" { + k += "=" + escapeComma(v) + } + opts = append(opts, k) + } + return strings.Join(opts, ",") +} + +// MountOption is passed to Mount to change the behavior of the mount. +type MountOption func(*MountConfig) error + +// FSName sets the file system name (also called source) that is +// visible in the list of mounted file systems. +func FSName(name string) MountOption { + return func(conf *MountConfig) error { + conf.options["fsname"] = name + return nil + } +} + +// Subtype sets the subtype of the mount. The main type is always +// `fuse`. The type in a list of mounted file systems will look like +// `fuse.foo`. +// +// OS X ignores this option. +func Subtype(fstype string) MountOption { + return func(conf *MountConfig) error { + conf.options["subtype"] = fstype + return nil + } +} + +// LocalVolume sets the volume to be local (instead of network), +// changing the behavior of Finder, Spotlight, and such. +// +// OS X only. Others ignore this option. +func LocalVolume() MountOption { + return localVolume +} + +// VolumeName sets the volume name shown in Finder. +// +// OS X only. Others ignore this option. +func VolumeName(name string) MountOption { + return volumeName(name) +} + +var ErrCannotCombineAllowOtherAndAllowRoot = errors.New("cannot combine AllowOther and AllowRoot") + +// AllowOther allows other users to access the file system. +// +// Only one of AllowOther or AllowRoot can be used. +func AllowOther() MountOption { + return func(conf *MountConfig) error { + if _, ok := conf.options["allow_root"]; ok { + return ErrCannotCombineAllowOtherAndAllowRoot + } + conf.options["allow_other"] = "" + return nil + } +} + +// AllowRoot allows other users to access the file system. +// +// Only one of AllowOther or AllowRoot can be used. +func AllowRoot() MountOption { + return func(conf *MountConfig) error { + if _, ok := conf.options["allow_other"]; ok { + return ErrCannotCombineAllowOtherAndAllowRoot + } + conf.options["allow_root"] = "" + return nil + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_darwin.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_darwin.go new file mode 100644 index 00000000..15aedbcf --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_darwin.go @@ -0,0 +1,13 @@ +package fuse + +func localVolume(conf *MountConfig) error { + conf.options["local"] = "" + return nil +} + +func volumeName(name string) MountOption { + return func(conf *MountConfig) error { + conf.options["volname"] = name + return nil + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_darwin_test.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_darwin_test.go new file mode 100644 index 00000000..dd264667 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_darwin_test.go @@ -0,0 +1,27 @@ +package fuse_test + +import ( + "testing" + + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs/fstestutil" +) + +func TestMountOptionCommaError(t *testing.T) { + t.Parallel() + // this test is not tied to FSName, but needs just some option + // with string content + var name = "FuseTest,Marker" + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, + fuse.FSName(name), + ) + switch { + case err == nil: + mnt.Close() + t.Fatal("expected an error about commas") + case err.Error() == `mount options cannot contain commas on OS X: "fsname"="FuseTest,Marker"`: + // all good + default: + t.Fatalf("expected an error about commas, got: %v", err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_linux.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_linux.go new file mode 100644 index 00000000..69dd406b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_linux.go @@ -0,0 +1,13 @@ +package fuse + +func dummyOption(conf *MountConfig) error { + return nil +} + +func localVolume(conf *MountConfig) error { + return nil +} + +func volumeName(name string) MountOption { + return dummyOption +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_test.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_test.go new file mode 100644 index 00000000..aa212931 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/options_test.go @@ -0,0 +1,141 @@ +package fuse_test + +import ( + "runtime" + "testing" + + "camlistore.org/third_party/bazil.org/fuse" + "camlistore.org/third_party/bazil.org/fuse/fs/fstestutil" +) + +func init() { + fstestutil.DebugByDefault() +} + +func TestMountOptionFSName(t *testing.T) { + t.Parallel() + const name = "FuseTestMarker" + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, + fuse.FSName(name), + ) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + info, err := fstestutil.GetMountInfo(mnt.Dir) + if err != nil { + t.Fatal(err) + } + if g, e := info.FSName, name; g != e { + t.Errorf("wrong FSName: %q != %q", g, e) + } +} + +func testMountOptionFSNameEvil(t *testing.T, evil string) { + t.Parallel() + var name = "FuseTest" + evil + "Marker" + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, + fuse.FSName(name), + ) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + info, err := fstestutil.GetMountInfo(mnt.Dir) + if err != nil { + t.Fatal(err) + } + if g, e := info.FSName, name; g != e { + t.Errorf("wrong FSName: %q != %q", g, e) + } +} + +func TestMountOptionFSNameEvilComma(t *testing.T) { + if runtime.GOOS == "darwin" { + // see TestMountOptionCommaError for a test that enforces we + // at least give a nice error, instead of corrupting the mount + // options + t.Skip("TODO: OS X gets this wrong, commas in mount options cannot be escaped at all") + } + testMountOptionFSNameEvil(t, ",") +} + +func TestMountOptionFSNameEvilSpace(t *testing.T) { + testMountOptionFSNameEvil(t, " ") +} + +func TestMountOptionFSNameEvilTab(t *testing.T) { + testMountOptionFSNameEvil(t, "\t") +} + +func TestMountOptionFSNameEvilNewline(t *testing.T) { + testMountOptionFSNameEvil(t, "\n") +} + +func TestMountOptionFSNameEvilBackslash(t *testing.T) { + testMountOptionFSNameEvil(t, `\`) +} + +func TestMountOptionFSNameEvilBackslashDouble(t *testing.T) { + // catch double-unescaping, if it were to happen + testMountOptionFSNameEvil(t, `\\`) +} + +func TestMountOptionSubtype(t *testing.T) { + if runtime.GOOS == "darwin" { + t.Skip("OS X does not support Subtype") + } + t.Parallel() + const name = "FuseTestMarker" + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, + fuse.Subtype(name), + ) + if err != nil { + t.Fatal(err) + } + defer mnt.Close() + + info, err := fstestutil.GetMountInfo(mnt.Dir) + if err != nil { + t.Fatal(err) + } + if g, e := info.Type, "fuse."+name; g != e { + t.Errorf("wrong Subtype: %q != %q", g, e) + } +} + +// TODO test LocalVolume + +// TODO test AllowOther; hard because needs system-level authorization + +func TestMountOptionAllowOtherThenAllowRoot(t *testing.T) { + t.Parallel() + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, + fuse.AllowOther(), + fuse.AllowRoot(), + ) + if err == nil { + mnt.Close() + } + if g, e := err, fuse.ErrCannotCombineAllowOtherAndAllowRoot; g != e { + t.Fatalf("wrong error: %v != %v", g, e) + } +} + +// TODO test AllowRoot; hard because needs system-level authorization + +func TestMountOptionAllowRootThenAllowOther(t *testing.T) { + t.Parallel() + mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, + fuse.AllowRoot(), + fuse.AllowOther(), + ) + if err == nil { + mnt.Close() + } + if g, e := err, fuse.ErrCannotCombineAllowOtherAndAllowRoot; g != e { + t.Fatalf("wrong error: %v != %v", g, e) + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/doc.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/doc.go new file mode 100644 index 00000000..8ceee43b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/doc.go @@ -0,0 +1,13 @@ +// Package syscallx provides wrappers that make syscalls on various +// platforms more interoperable. +// +// The API intentionally omits the OS X-specific position and option +// arguments for extended attribute calls. +// +// Not having position means it might not be useful for accessing the +// resource fork. If that's needed by code inside fuse, a function +// with a different name may be added on the side. +// +// Options can be implemented with separate wrappers, in the style of +// Linux getxattr/lgetxattr/fgetxattr. +package syscallx diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/generate b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/generate new file mode 100755 index 00000000..476a282b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/generate @@ -0,0 +1,34 @@ +#!/bin/sh +set -e + +mksys="$(go env GOROOT)/src/pkg/syscall/mksyscall.pl" + +fix() { + sed 's,^package syscall$,&x\nimport "syscall",' \ + | gofmt -r='BytePtrFromString -> syscall.BytePtrFromString' \ + | gofmt -r='Syscall6 -> syscall.Syscall6' \ + | gofmt -r='Syscall -> syscall.Syscall' \ + | gofmt -r='SYS_GETXATTR -> syscall.SYS_GETXATTR' \ + | gofmt -r='SYS_LISTXATTR -> syscall.SYS_LISTXATTR' \ + | gofmt -r='SYS_SETXATTR -> syscall.SYS_SETXATTR' \ + | gofmt -r='SYS_REMOVEXATTR -> syscall.SYS_REMOVEXATTR' \ + | gofmt -r='SYS_MSYNC -> syscall.SYS_MSYNC' +} + +cd "$(dirname "$0")" + +$mksys xattr_darwin.go \ + | fix \ + >xattr_darwin_amd64.go + +$mksys -l32 xattr_darwin.go \ + | fix \ + >xattr_darwin_386.go + +$mksys msync.go \ + | fix \ + >msync_amd64.go + +$mksys -l32 msync.go \ + | fix \ + >msync_386.go diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/msync.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/msync.go new file mode 100644 index 00000000..30737e6d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/msync.go @@ -0,0 +1,9 @@ +package syscallx + +/* This is the source file for msync_*.go, to regenerate run + + ./generate + +*/ + +//sys Msync(b []byte, flags int) (err error) diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/msync_386.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/msync_386.go new file mode 100644 index 00000000..67259942 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/msync_386.go @@ -0,0 +1,24 @@ +// mksyscall.pl -l32 msync.go +// MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT + +package syscallx + +import "syscall" + +import "unsafe" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func Msync(b []byte, flags int) (err error) { + var _p0 unsafe.Pointer + if len(b) > 0 { + _p0 = unsafe.Pointer(&b[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + _, _, e1 := syscall.Syscall(syscall.SYS_MSYNC, uintptr(_p0), uintptr(len(b)), uintptr(flags)) + if e1 != 0 { + err = e1 + } + return +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/msync_amd64.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/msync_amd64.go new file mode 100644 index 00000000..0bbe1ab8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/msync_amd64.go @@ -0,0 +1,24 @@ +// mksyscall.pl msync.go +// MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT + +package syscallx + +import "syscall" + +import "unsafe" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func Msync(b []byte, flags int) (err error) { + var _p0 unsafe.Pointer + if len(b) > 0 { + _p0 = unsafe.Pointer(&b[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + _, _, e1 := syscall.Syscall(syscall.SYS_MSYNC, uintptr(_p0), uintptr(len(b)), uintptr(flags)) + if e1 != 0 { + err = e1 + } + return +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/syscallx.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/syscallx.go new file mode 100644 index 00000000..eb099129 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/syscallx.go @@ -0,0 +1,4 @@ +package syscallx + +// make us look more like package syscall, so mksyscall.pl output works +var _zero uintptr diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/syscallx_std.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/syscallx_std.go new file mode 100644 index 00000000..57353a53 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/syscallx_std.go @@ -0,0 +1,26 @@ +// +build !darwin + +package syscallx + +// This file just contains wrappers for platforms that already have +// the right stuff in stdlib. + +import ( + "syscall" +) + +func Getxattr(path string, attr string, dest []byte) (sz int, err error) { + return syscall.Getxattr(path, attr, dest) +} + +func Listxattr(path string, dest []byte) (sz int, err error) { + return syscall.Listxattr(path, dest) +} + +func Setxattr(path string, attr string, data []byte, flags int) (err error) { + return syscall.Setxattr(path, attr, data, flags) +} + +func Removexattr(path string, attr string) (err error) { + return syscall.Removexattr(path, attr) +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/xattr_darwin.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/xattr_darwin.go new file mode 100644 index 00000000..b00f9020 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/xattr_darwin.go @@ -0,0 +1,38 @@ +package syscallx + +/* This is the source file for syscallx_darwin_*.go, to regenerate run + + ./generate + +*/ + +// cannot use dest []byte here because OS X getxattr really wants a +// NULL to trigger size probing, size==0 is not enough +// +//sys getxattr(path string, attr string, dest *byte, size int, position uint32, options int) (sz int, err error) + +func Getxattr(path string, attr string, dest []byte) (sz int, err error) { + var destp *byte + if len(dest) > 0 { + destp = &dest[0] + } + return getxattr(path, attr, destp, len(dest), 0, 0) +} + +//sys listxattr(path string, dest []byte, options int) (sz int, err error) + +func Listxattr(path string, dest []byte) (sz int, err error) { + return listxattr(path, dest, 0) +} + +//sys setxattr(path string, attr string, data []byte, position uint32, flags int) (err error) + +func Setxattr(path string, attr string, data []byte, flags int) (err error) { + return setxattr(path, attr, data, 0, flags) +} + +//sys removexattr(path string, attr string, options int) (err error) + +func Removexattr(path string, attr string) (err error) { + return removexattr(path, attr, 0) +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/xattr_darwin_386.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/xattr_darwin_386.go new file mode 100644 index 00000000..ffc357ae --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/xattr_darwin_386.go @@ -0,0 +1,97 @@ +// mksyscall.pl -l32 xattr_darwin.go +// MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT + +package syscallx + +import "syscall" + +import "unsafe" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func getxattr(path string, attr string, dest *byte, size int, position uint32, options int) (sz int, err error) { + var _p0 *byte + _p0, err = syscall.BytePtrFromString(path) + if err != nil { + return + } + var _p1 *byte + _p1, err = syscall.BytePtrFromString(attr) + if err != nil { + return + } + r0, _, e1 := syscall.Syscall6(syscall.SYS_GETXATTR, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(unsafe.Pointer(dest)), uintptr(size), uintptr(position), uintptr(options)) + sz = int(r0) + if e1 != 0 { + err = e1 + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func listxattr(path string, dest []byte, options int) (sz int, err error) { + var _p0 *byte + _p0, err = syscall.BytePtrFromString(path) + if err != nil { + return + } + var _p1 unsafe.Pointer + if len(dest) > 0 { + _p1 = unsafe.Pointer(&dest[0]) + } else { + _p1 = unsafe.Pointer(&_zero) + } + r0, _, e1 := syscall.Syscall6(syscall.SYS_LISTXATTR, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(dest)), uintptr(options), 0, 0) + sz = int(r0) + if e1 != 0 { + err = e1 + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func setxattr(path string, attr string, data []byte, position uint32, flags int) (err error) { + var _p0 *byte + _p0, err = syscall.BytePtrFromString(path) + if err != nil { + return + } + var _p1 *byte + _p1, err = syscall.BytePtrFromString(attr) + if err != nil { + return + } + var _p2 unsafe.Pointer + if len(data) > 0 { + _p2 = unsafe.Pointer(&data[0]) + } else { + _p2 = unsafe.Pointer(&_zero) + } + _, _, e1 := syscall.Syscall6(syscall.SYS_SETXATTR, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(_p2), uintptr(len(data)), uintptr(position), uintptr(flags)) + if e1 != 0 { + err = e1 + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func removexattr(path string, attr string, options int) (err error) { + var _p0 *byte + _p0, err = syscall.BytePtrFromString(path) + if err != nil { + return + } + var _p1 *byte + _p1, err = syscall.BytePtrFromString(attr) + if err != nil { + return + } + _, _, e1 := syscall.Syscall(syscall.SYS_REMOVEXATTR, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(options)) + if e1 != 0 { + err = e1 + } + return +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/xattr_darwin_amd64.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/xattr_darwin_amd64.go new file mode 100644 index 00000000..864c4c1d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/syscallx/xattr_darwin_amd64.go @@ -0,0 +1,97 @@ +// mksyscall.pl xattr_darwin.go +// MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT + +package syscallx + +import "syscall" + +import "unsafe" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func getxattr(path string, attr string, dest *byte, size int, position uint32, options int) (sz int, err error) { + var _p0 *byte + _p0, err = syscall.BytePtrFromString(path) + if err != nil { + return + } + var _p1 *byte + _p1, err = syscall.BytePtrFromString(attr) + if err != nil { + return + } + r0, _, e1 := syscall.Syscall6(syscall.SYS_GETXATTR, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(unsafe.Pointer(dest)), uintptr(size), uintptr(position), uintptr(options)) + sz = int(r0) + if e1 != 0 { + err = e1 + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func listxattr(path string, dest []byte, options int) (sz int, err error) { + var _p0 *byte + _p0, err = syscall.BytePtrFromString(path) + if err != nil { + return + } + var _p1 unsafe.Pointer + if len(dest) > 0 { + _p1 = unsafe.Pointer(&dest[0]) + } else { + _p1 = unsafe.Pointer(&_zero) + } + r0, _, e1 := syscall.Syscall6(syscall.SYS_LISTXATTR, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(dest)), uintptr(options), 0, 0) + sz = int(r0) + if e1 != 0 { + err = e1 + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func setxattr(path string, attr string, data []byte, position uint32, flags int) (err error) { + var _p0 *byte + _p0, err = syscall.BytePtrFromString(path) + if err != nil { + return + } + var _p1 *byte + _p1, err = syscall.BytePtrFromString(attr) + if err != nil { + return + } + var _p2 unsafe.Pointer + if len(data) > 0 { + _p2 = unsafe.Pointer(&data[0]) + } else { + _p2 = unsafe.Pointer(&_zero) + } + _, _, e1 := syscall.Syscall6(syscall.SYS_SETXATTR, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(_p2), uintptr(len(data)), uintptr(position), uintptr(flags)) + if e1 != 0 { + err = e1 + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func removexattr(path string, attr string, options int) (err error) { + var _p0 *byte + _p0, err = syscall.BytePtrFromString(path) + if err != nil { + return + } + var _p1 *byte + _p1, err = syscall.BytePtrFromString(attr) + if err != nil { + return + } + _, _, e1 := syscall.Syscall(syscall.SYS_REMOVEXATTR, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(options)) + if e1 != 0 { + err = e1 + } + return +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/unmount.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/unmount.go new file mode 100644 index 00000000..ffe3f155 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/unmount.go @@ -0,0 +1,6 @@ +package fuse + +// Unmount tries to unmount the filesystem mounted at dir. +func Unmount(dir string) error { + return unmount(dir) +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/unmount_linux.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/unmount_linux.go new file mode 100644 index 00000000..088f0cfe --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/unmount_linux.go @@ -0,0 +1,21 @@ +package fuse + +import ( + "bytes" + "errors" + "os/exec" +) + +func unmount(dir string) error { + cmd := exec.Command("fusermount", "-u", dir) + output, err := cmd.CombinedOutput() + if err != nil { + if len(output) > 0 { + output = bytes.TrimRight(output, "\n") + msg := err.Error() + ": " + string(output) + err = errors.New(msg) + } + return err + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/unmount_std.go b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/unmount_std.go new file mode 100644 index 00000000..d6efe276 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/bazil.org/fuse/unmount_std.go @@ -0,0 +1,17 @@ +// +build !linux + +package fuse + +import ( + "os" + "syscall" +) + +func unmount(dir string) error { + err := syscall.Unmount(dir, 0) + if err != nil { + err = &os.PathError{Op: "unmount", Path: dir, Err: err} + return err + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/AUTHORS b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/AUTHORS new file mode 100644 index 00000000..c8ad75a8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/AUTHORS @@ -0,0 +1,17 @@ +# This is a list of contributors to the Closure Library. + +# Names should be added to this file like so: +# Name or Organization + +Google Inc. +Mohamed Mansour +Bjorn Tipling +SameGoal LLC +Guido Tapia +Andrew Mattie +Ilia Mirkin +Ivan Kozik +Rich Dougherty +Chad Killingsworth +Dan Pupius + diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/LICENSE b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/LICENSE new file mode 100644 index 00000000..d9a10c0d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/README b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/README new file mode 100644 index 00000000..7a61255c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/README @@ -0,0 +1,6 @@ +Closure Library is a powerful, low level JavaScript library designed +for building complex and scalable web applications. It is used by many +major Google web applications, such as Gmail and Google Docs. + +For more information about Closure Library, visit: +http://code.google.com/closure/library diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/aria.js b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/aria.js new file mode 100644 index 00000000..d9f04e33 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/aria.js @@ -0,0 +1,364 @@ +// Copyright 2007 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/** + * @fileoverview Utilities for adding, removing and setting ARIA roles and + * states as defined by W3C ARIA standard: http://www.w3.org/TR/wai-aria/ + * All modern browsers have some form of ARIA support, so no browser checks are + * performed when adding ARIA to components. + * + */ + +goog.provide('goog.a11y.aria'); + +goog.require('goog.a11y.aria.Role'); +goog.require('goog.a11y.aria.State'); +goog.require('goog.a11y.aria.datatables'); +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('goog.dom'); +goog.require('goog.dom.TagName'); +goog.require('goog.object'); + + +/** + * ARIA states/properties prefix. + * @private + */ +goog.a11y.aria.ARIA_PREFIX_ = 'aria-'; + + +/** + * ARIA role attribute. + * @private + */ +goog.a11y.aria.ROLE_ATTRIBUTE_ = 'role'; + + +/** + * A list of tag names for which we don't need to set ARIA role and states + * because they have well supported semantics for screen readers or because + * they don't contain content to be made accessible. + * @private + */ +goog.a11y.aria.TAGS_WITH_ASSUMED_ROLES_ = [ + goog.dom.TagName.A, + goog.dom.TagName.AREA, + goog.dom.TagName.BUTTON, + goog.dom.TagName.HEAD, + goog.dom.TagName.INPUT, + goog.dom.TagName.LINK, + goog.dom.TagName.MENU, + goog.dom.TagName.META, + goog.dom.TagName.OPTGROUP, + goog.dom.TagName.OPTION, + goog.dom.TagName.PROGRESS, + goog.dom.TagName.STYLE, + goog.dom.TagName.SELECT, + goog.dom.TagName.SOURCE, + goog.dom.TagName.TEXTAREA, + goog.dom.TagName.TITLE, + goog.dom.TagName.TRACK +]; + + +/** + * Sets the role of an element. If the roleName is + * empty string or null, the role for the element is removed. + * We encourage clients to call the goog.a11y.aria.removeRole + * method instead of setting null and empty string values. + * Special handling for this case is added to ensure + * backword compatibility with existing code. + * + * @param {!Element} element DOM node to set role of. + * @param {!goog.a11y.aria.Role|string} roleName role name(s). + */ +goog.a11y.aria.setRole = function(element, roleName) { + if (!roleName) { + // Setting the ARIA role to empty string is not allowed + // by the ARIA standard. + goog.a11y.aria.removeRole(element); + } else { + if (goog.asserts.ENABLE_ASSERTS) { + goog.asserts.assert(goog.object.containsValue( + goog.a11y.aria.Role, roleName), 'No such ARIA role ' + roleName); + } + element.setAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_, roleName); + } +}; + + +/** + * Gets role of an element. + * @param {!Element} element DOM element to get role of. + * @return {?goog.a11y.aria.Role} ARIA Role name. + */ +goog.a11y.aria.getRole = function(element) { + var role = element.getAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_); + return /** @type {goog.a11y.aria.Role} */ (role) || null; +}; + + +/** + * Removes role of an element. + * @param {!Element} element DOM element to remove the role from. + */ +goog.a11y.aria.removeRole = function(element) { + element.removeAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_); +}; + + +/** + * Sets the state or property of an element. + * @param {!Element} element DOM node where we set state. + * @param {!(goog.a11y.aria.State|string)} stateName State attribute being set. + * Automatically adds prefix 'aria-' to the state name if the attribute is + * not an extra attribute. + * @param {string|boolean|number|!goog.array.ArrayLike.} value Value + * for the state attribute. + */ +goog.a11y.aria.setState = function(element, stateName, value) { + if (goog.isArrayLike(value)) { + var array = /** @type {!goog.array.ArrayLike.} */ (value); + value = array.join(' '); + } + var attrStateName = goog.a11y.aria.getAriaAttributeName_(stateName); + if (value === '' || value == undefined) { + var defaultValueMap = goog.a11y.aria.datatables.getDefaultValuesMap(); + // Work around for browsers that don't properly support ARIA. + // According to the ARIA W3C standard, user agents should allow + // setting empty value which results in setting the default value + // for the ARIA state if such exists. The exact text from the ARIA W3C + // standard (http://www.w3.org/TR/wai-aria/states_and_properties): + // "When a value is indicated as the default, the user agent + // MUST follow the behavior prescribed by this value when the state or + // property is empty or undefined." + // The defaultValueMap contains the default values for the ARIA states + // and has as a key the goog.a11y.aria.State constant for the state. + if (stateName in defaultValueMap) { + element.setAttribute(attrStateName, defaultValueMap[stateName]); + } else { + element.removeAttribute(attrStateName); + } + } else { + element.setAttribute(attrStateName, value); + } +}; + + +/** + * Remove the state or property for the element. + * @param {!Element} element DOM node where we set state. + * @param {!goog.a11y.aria.State} stateName State name. + */ +goog.a11y.aria.removeState = function(element, stateName) { + element.removeAttribute(goog.a11y.aria.getAriaAttributeName_(stateName)); +}; + + +/** + * Gets value of specified state or property. + * @param {!Element} element DOM node to get state from. + * @param {!goog.a11y.aria.State|string} stateName State name. + * @return {string} Value of the state attribute. + */ +goog.a11y.aria.getState = function(element, stateName) { + // TODO(user): return properly typed value result -- + // boolean, number, string, null. We should be able to chain + // getState(...) and setState(...) methods. + + var attr = + /** @type {string|number|boolean} */ (element.getAttribute( + goog.a11y.aria.getAriaAttributeName_(stateName))); + var isNullOrUndefined = attr == null || attr == undefined; + return isNullOrUndefined ? '' : String(attr); +}; + + +/** + * Returns the activedescendant element for the input element by + * using the activedescendant ARIA property of the given element. + * @param {!Element} element DOM node to get activedescendant + * element for. + * @return {?Element} DOM node of the activedescendant, if found. + */ +goog.a11y.aria.getActiveDescendant = function(element) { + var id = goog.a11y.aria.getState( + element, goog.a11y.aria.State.ACTIVEDESCENDANT); + return goog.dom.getOwnerDocument(element).getElementById(id); +}; + + +/** + * Sets the activedescendant ARIA property value for an element. + * If the activeElement is not null, it should have an id set. + * @param {!Element} element DOM node to set activedescendant ARIA property to. + * @param {?Element} activeElement DOM node being set as activedescendant. + */ +goog.a11y.aria.setActiveDescendant = function(element, activeElement) { + var id = ''; + if (activeElement) { + id = activeElement.id; + goog.asserts.assert(id, 'The active element should have an id.'); + } + + goog.a11y.aria.setState(element, goog.a11y.aria.State.ACTIVEDESCENDANT, id); +}; + + +/** + * Gets the label of the given element. + * @param {!Element} element DOM node to get label from. + * @return {string} label The label. + */ +goog.a11y.aria.getLabel = function(element) { + return goog.a11y.aria.getState(element, goog.a11y.aria.State.LABEL); +}; + + +/** + * Sets the label of the given element. + * @param {!Element} element DOM node to set label to. + * @param {string} label The label to set. + */ +goog.a11y.aria.setLabel = function(element, label) { + goog.a11y.aria.setState(element, goog.a11y.aria.State.LABEL, label); +}; + + +/** + * Asserts that the element has a role set if it's not an HTML element whose + * semantics is well supported by most screen readers. + * Only to be used internally by the ARIA library in goog.a11y.aria.*. + * @param {!Element} element The element to assert an ARIA role set. + * @param {!goog.array.ArrayLike.} allowedRoles The child roles of + * the roles. + */ +goog.a11y.aria.assertRoleIsSetInternalUtil = function(element, allowedRoles) { + if (goog.array.contains(goog.a11y.aria.TAGS_WITH_ASSUMED_ROLES_, + element.tagName)) { + return; + } + var elementRole = /** @type {string}*/ (goog.a11y.aria.getRole(element)); + goog.asserts.assert(elementRole != null, + 'The element ARIA role cannot be null.'); + + goog.asserts.assert(goog.array.contains(allowedRoles, elementRole), + 'Non existing or incorrect role set for element.' + + 'The role set is "' + elementRole + + '". The role should be any of "' + allowedRoles + + '". Check the ARIA specification for more details ' + + 'http://www.w3.org/TR/wai-aria/roles.'); +}; + + +/** + * Gets the boolean value of an ARIA state/property. + * @param {!Element} element The element to get the ARIA state for. + * @param {!goog.a11y.aria.State|string} stateName the ARIA state name. + * @return {?boolean} Boolean value for the ARIA state value or null if + * the state value is not 'true', not 'false', or not set. + */ +goog.a11y.aria.getStateBoolean = function(element, stateName) { + var attr = + /** @type {string|boolean} */ (element.getAttribute( + goog.a11y.aria.getAriaAttributeName_(stateName))); + goog.asserts.assert( + goog.isBoolean(attr) || attr == null || attr == 'true' || + attr == 'false'); + if (attr == null) { + return attr; + } + return goog.isBoolean(attr) ? attr : attr == 'true'; +}; + + +/** + * Gets the number value of an ARIA state/property. + * @param {!Element} element The element to get the ARIA state for. + * @param {!goog.a11y.aria.State|string} stateName the ARIA state name. + * @return {?number} Number value for the ARIA state value or null if + * the state value is not a number or not set. + */ +goog.a11y.aria.getStateNumber = function(element, stateName) { + var attr = + /** @type {string|number} */ (element.getAttribute( + goog.a11y.aria.getAriaAttributeName_(stateName))); + goog.asserts.assert((attr == null || !isNaN(Number(attr))) && + !goog.isBoolean(attr)); + return attr == null ? null : Number(attr); +}; + + +/** + * Gets the string value of an ARIA state/property. + * @param {!Element} element The element to get the ARIA state for. + * @param {!goog.a11y.aria.State|string} stateName the ARIA state name. + * @return {?string} String value for the ARIA state value or null if + * the state value is empty string or not set. + */ +goog.a11y.aria.getStateString = function(element, stateName) { + var attr = element.getAttribute( + goog.a11y.aria.getAriaAttributeName_(stateName)); + goog.asserts.assert((attr == null || goog.isString(attr)) && + isNaN(Number(attr)) && attr != 'true' && attr != 'false'); + return attr == null ? null : attr; +}; + + +/** + * Gets array of strings value of the specified state or + * property for the element. + * Only to be used internally by the ARIA library in goog.a11y.aria.*. + * @param {!Element} element DOM node to get state from. + * @param {!goog.a11y.aria.State} stateName State name. + * @return {!goog.array.ArrayLike.} string Array + * value of the state attribute. + */ +goog.a11y.aria.getStringArrayStateInternalUtil = function(element, stateName) { + var attrValue = element.getAttribute( + goog.a11y.aria.getAriaAttributeName_(stateName)); + return goog.a11y.aria.splitStringOnWhitespace_(attrValue); +}; + + +/** + * Splits the input stringValue on whitespace. + * @param {string} stringValue The value of the string to split. + * @return {!goog.array.ArrayLike.} string Array + * value as result of the split. + * @private + */ +goog.a11y.aria.splitStringOnWhitespace_ = function(stringValue) { + return stringValue ? stringValue.split(/\s+/) : []; +}; + + +/** + * Adds the 'aria-' prefix to ariaName. + * @param {string} ariaName ARIA state/property name. + * @private + * @return {string} The ARIA attribute name with added 'aria-' prefix. + * @throws {Error} If no such attribute exists. + */ +goog.a11y.aria.getAriaAttributeName_ = function(ariaName) { + if (goog.asserts.ENABLE_ASSERTS) { + goog.asserts.assert(ariaName, 'ARIA attribute cannot be empty.'); + goog.asserts.assert(goog.object.containsValue( + goog.a11y.aria.State, ariaName), + 'No such ARIA attribute ' + ariaName); + } + return goog.a11y.aria.ARIA_PREFIX_ + ariaName; +}; diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/attributes.js b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/attributes.js new file mode 100644 index 00000000..f4e0a3d0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/attributes.js @@ -0,0 +1,389 @@ +// Copyright 2013 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/** + * @fileoverview The file contains generated enumerations for ARIA states + * and properties as defined by W3C ARIA standard: + * http://www.w3.org/TR/wai-aria/. + * + * This is auto-generated code. Do not manually edit! For more details + * about how to edit it via the generator check go/closure-ariagen. + */ + +goog.provide('goog.a11y.aria.AutoCompleteValues'); +goog.provide('goog.a11y.aria.CheckedValues'); +goog.provide('goog.a11y.aria.DropEffectValues'); +goog.provide('goog.a11y.aria.ExpandedValues'); +goog.provide('goog.a11y.aria.GrabbedValues'); +goog.provide('goog.a11y.aria.InvalidValues'); +goog.provide('goog.a11y.aria.LivePriority'); +goog.provide('goog.a11y.aria.OrientationValues'); +goog.provide('goog.a11y.aria.PressedValues'); +goog.provide('goog.a11y.aria.RelevantValues'); +goog.provide('goog.a11y.aria.SelectedValues'); +goog.provide('goog.a11y.aria.SortValues'); +goog.provide('goog.a11y.aria.State'); + + +/** + * ARIA states and properties. + * @enum {string} + */ +goog.a11y.aria.State = { + // ARIA property for setting the currently active descendant of an element, + // for example the selected item in a list box. Value: ID of an element. + ACTIVEDESCENDANT: 'activedescendant', + + // ARIA property that, if true, indicates that all of a changed region should + // be presented, instead of only parts. Value: one of {true, false}. + ATOMIC: 'atomic', + + // ARIA property to specify that input completion is provided. Value: + // one of {'inline', 'list', 'both', 'none'}. + AUTOCOMPLETE: 'autocomplete', + + // ARIA state to indicate that an element and its subtree are being updated. + // Value: one of {true, false}. + BUSY: 'busy', + + // ARIA state for a checked item. Value: one of {'true', 'false', 'mixed', + // undefined}. + CHECKED: 'checked', + + // ARIA property that identifies the element or elements whose contents or + // presence are controlled by this element. + // Value: space-separated IDs of other elements. + CONTROLS: 'controls', + + // ARIA property that identifies the element or elements that describe + // this element. Value: space-separated IDs of other elements. + DESCRIBEDBY: 'describedby', + + // ARIA state for a disabled item. Value: one of {true, false}. + DISABLED: 'disabled', + + // ARIA property that indicates what functions can be performed when a + // dragged object is released on the drop target. Value: one of + // {'copy', 'move', 'link', 'execute', 'popup', 'none'}. + DROPEFFECT: 'dropeffect', + + // ARIA state for setting whether the element like a tree node is expanded. + // Value: one of {true, false, undefined}. + EXPANDED: 'expanded', + + // ARIA property that identifies the next element (or elements) in the + // recommended reading order of content. Value: space-separated ids of + // elements to flow to. + FLOWTO: 'flowto', + + // ARIA state that indicates an element's "grabbed" state in drag-and-drop. + // Value: one of {true, false, undefined}. + GRABBED: 'grabbed', + + // ARIA property indicating whether the element has a popup. + // Value: one of {true, false}. + HASPOPUP: 'haspopup', + + // ARIA state indicating that the element is not visible or perceivable + // to any user. Value: one of {true, false}. + HIDDEN: 'hidden', + + // ARIA state indicating that the entered value does not conform. Value: + // one of {false, true, 'grammar', 'spelling'} + INVALID: 'invalid', + + // ARIA property that provides a label to override any other text, value, or + // contents used to describe this element. Value: string. + LABEL: 'label', + + // ARIA property for setting the element which labels another element. + // Value: space-separated IDs of elements. + LABELLEDBY: 'labelledby', + + // ARIA property for setting the level of an element in the hierarchy. + // Value: integer. + LEVEL: 'level', + + // ARIA property indicating that an element will be updated, and + // describes the types of updates the user agents, assistive technologies, + // and user can expect from the live region. Value: one of {'off', 'polite', + // 'assertive'}. + LIVE: 'live', + + // ARIA property indicating whether a text box can accept multiline input. + // Value: one of {true, false}. + MULTILINE: 'multiline', + + // ARIA property indicating if the user may select more than one item. + // Value: one of {true, false}. + MULTISELECTABLE: 'multiselectable', + + // ARIA property indicating if the element is horizontal or vertical. + // Value: one of {'vertical', 'horizontal'}. + ORIENTATION: 'orientation', + + // ARIA property creating a visual, functional, or contextual parent/child + // relationship when the DOM hierarchy can't be used to represent it. + // Value: Space-separated IDs of elements. + OWNS: 'owns', + + // ARIA property that defines an element's number of position in a list. + // Value: integer. + POSINSET: 'posinset', + + // ARIA state for a pressed item. + // Value: one of {true, false, undefined, 'mixed'}. + PRESSED: 'pressed', + + // ARIA property indicating that an element is not editable. + // Value: one of {true, false}. + READONLY: 'readonly', + + // ARIA property indicating that change notifications within this subtree + // of a live region should be announced. Value: one of {'additions', + // 'removals', 'text', 'all', 'additions text'}. + RELEVANT: 'relevant', + + // ARIA property indicating that user input is required on this element + // before a form may be submitted. Value: one of {true, false}. + REQUIRED: 'required', + + // ARIA state for setting the currently selected item in the list. + // Value: one of {true, false, undefined}. + SELECTED: 'selected', + + // ARIA property defining the number of items in a list. Value: integer. + SETSIZE: 'setsize', + + // ARIA property indicating if items are sorted. Value: one of {'ascending', + // 'descending', 'none', 'other'}. + SORT: 'sort', + + // ARIA property for slider maximum value. Value: number. + VALUEMAX: 'valuemax', + + // ARIA property for slider minimum value. Value: number. + VALUEMIN: 'valuemin', + + // ARIA property for slider active value. Value: number. + VALUENOW: 'valuenow', + + // ARIA property for slider active value represented as text. + // Value: string. + VALUETEXT: 'valuetext' +}; + + +/** + * ARIA state values for AutoCompleteValues. + * @enum {string} + */ +goog.a11y.aria.AutoCompleteValues = { + // The system provides text after the caret as a suggestion + // for how to complete the field. + INLINE: 'inline', + // A list of choices appears from which the user can choose, + // but the edit box retains focus. + LIST: 'list', + // A list of choices appears and the currently selected suggestion + // also appears inline. + BOTH: 'both', + // No input completion suggestions are provided. + NONE: 'none' +}; + + +/** + * ARIA state values for DropEffectValues. + * @enum {string} + */ +goog.a11y.aria.DropEffectValues = { + // A duplicate of the source object will be dropped into the target. + COPY: 'copy', + // The source object will be removed from its current location + // and dropped into the target. + MOVE: 'move', + // A reference or shortcut to the dragged object + // will be created in the target object. + LINK: 'link', + // A function supported by the drop target is + // executed, using the drag source as an input. + EXECUTE: 'execute', + // There is a popup menu or dialog that allows the user to choose + // one of the drag operations (copy, move, link, execute) and any other + // drag functionality, such as cancel. + POPUP: 'popup', + // No operation can be performed; effectively + // cancels the drag operation if an attempt is made to drop on this object. + NONE: 'none' +}; + + +/** + * ARIA state values for LivePriority. + * @enum {string} + */ +goog.a11y.aria.LivePriority = { + // Updates to the region will not be presented to the user + // unless the assitive technology is currently focused on that region. + OFF: 'off', + // (Background change) Assistive technologies SHOULD announce + // updates at the next graceful opportunity, such as at the end of + // speaking the current sentence or when the user pauses typing. + POLITE: 'polite', + // This information has the highest priority and assistive + // technologies SHOULD notify the user immediately. + // Because an interruption may disorient users or cause them to not complete + // their current task, authors SHOULD NOT use the assertive value unless the + // interruption is imperative. + ASSERTIVE: 'assertive' +}; + + +/** + * ARIA state values for OrientationValues. + * @enum {string} + */ +goog.a11y.aria.OrientationValues = { + // The element is oriented vertically. + VERTICAL: 'vertical', + // The element is oriented horizontally. + HORIZONTAL: 'horizontal' +}; + + +/** + * ARIA state values for RelevantValues. + * @enum {string} + */ +goog.a11y.aria.RelevantValues = { + // Element nodes are added to the DOM within the live region. + ADDITIONS: 'additions', + // Text or element nodes within the live region are removed from the DOM. + REMOVALS: 'removals', + // Text is added to any DOM descendant nodes of the live region. + TEXT: 'text', + // Equivalent to the combination of all values, "additions removals text". + ALL: 'all' +}; + + +/** + * ARIA state values for SortValues. + * @enum {string} + */ +goog.a11y.aria.SortValues = { + // Items are sorted in ascending order by this column. + ASCENDING: 'ascending', + // Items are sorted in descending order by this column. + DESCENDING: 'descending', + // There is no defined sort applied to the column. + NONE: 'none', + // A sort algorithm other than ascending or descending has been applied. + OTHER: 'other' +}; + + +/** + * ARIA state values for CheckedValues. + * @enum {string} + */ +goog.a11y.aria.CheckedValues = { + // The selectable element is checked. + TRUE: 'true', + // The selectable element is not checked. + FALSE: 'false', + // Indicates a mixed mode value for a tri-state + // checkbox or menuitemcheckbox. + MIXED: 'mixed', + // The element does not support being checked. + UNDEFINED: 'undefined' +}; + + +/** + * ARIA state values for ExpandedValues. + * @enum {string} + */ +goog.a11y.aria.ExpandedValues = { + // The element, or another grouping element it controls, is expanded. + TRUE: 'true', + // The element, or another grouping element it controls, is collapsed. + FALSE: 'false', + // The element, or another grouping element + // it controls, is neither expandable nor collapsible; all its + // child elements are shown or there are no child elements. + UNDEFINED: 'undefined' +}; + + +/** + * ARIA state values for GrabbedValues. + * @enum {string} + */ +goog.a11y.aria.GrabbedValues = { + // Indicates that the element has been "grabbed" for dragging. + TRUE: 'true', + // Indicates that the element supports being dragged. + FALSE: 'false', + // Indicates that the element does not support being dragged. + UNDEFINED: 'undefined' +}; + + +/** + * ARIA state values for InvalidValues. + * @enum {string} + */ +goog.a11y.aria.InvalidValues = { + // There are no detected errors in the value. + FALSE: 'false', + // The value entered by the user has failed validation. + TRUE: 'true', + // A grammatical error was detected. + GRAMMAR: 'grammar', + // A spelling error was detected. + SPELLING: 'spelling' +}; + + +/** + * ARIA state values for PressedValues. + * @enum {string} + */ +goog.a11y.aria.PressedValues = { + // The element is pressed. + TRUE: 'true', + // The element supports being pressed but is not currently pressed. + FALSE: 'false', + // Indicates a mixed mode value for a tri-state toggle button. + MIXED: 'mixed', + // The element does not support being pressed. + UNDEFINED: 'undefined' +}; + + +/** + * ARIA state values for SelectedValues. + * @enum {string} + */ +goog.a11y.aria.SelectedValues = { + // The selectable element is selected. + TRUE: 'true', + // The selectable element is not selected. + FALSE: 'false', + // The element is not selectable. + UNDEFINED: 'undefined' +}; diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/datatables.js b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/datatables.js new file mode 100644 index 00000000..c97df208 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/datatables.js @@ -0,0 +1,68 @@ +// Copyright 2013 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + + +/** + * @fileoverview The file contains data tables generated from the ARIA + * standard schema http://www.w3.org/TR/wai-aria/. + * + * This is auto-generated code. Do not manually edit! + */ + +goog.provide('goog.a11y.aria.datatables'); + +goog.require('goog.a11y.aria.State'); +goog.require('goog.object'); + + +/** + * A map that contains mapping between an ARIA state and the default value + * for it. Note that not all ARIA states have default values. + * + * @type {Object.} + */ +goog.a11y.aria.DefaultStateValueMap_; + + +/** + * A method that creates a map that contains mapping between an ARIA state and + * the default value for it. Note that not all ARIA states have default values. + * + * @return {Object.} + * The names for each of the notification methods. + */ +goog.a11y.aria.datatables.getDefaultValuesMap = function() { + if (!goog.a11y.aria.DefaultStateValueMap_) { + goog.a11y.aria.DefaultStateValueMap_ = goog.object.create( + goog.a11y.aria.State.ATOMIC, false, + goog.a11y.aria.State.AUTOCOMPLETE, 'none', + goog.a11y.aria.State.DROPEFFECT, 'none', + goog.a11y.aria.State.HASPOPUP, false, + goog.a11y.aria.State.LIVE, 'off', + goog.a11y.aria.State.MULTILINE, false, + goog.a11y.aria.State.MULTISELECTABLE, false, + goog.a11y.aria.State.ORIENTATION, 'vertical', + goog.a11y.aria.State.READONLY, false, + goog.a11y.aria.State.RELEVANT, 'additions text', + goog.a11y.aria.State.REQUIRED, false, + goog.a11y.aria.State.SORT, 'none', + goog.a11y.aria.State.BUSY, false, + goog.a11y.aria.State.DISABLED, false, + goog.a11y.aria.State.HIDDEN, false, + goog.a11y.aria.State.INVALID, 'false'); + } + + return goog.a11y.aria.DefaultStateValueMap_; +}; diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/roles.js b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/roles.js new file mode 100644 index 00000000..a282cc2d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/a11y/aria/roles.js @@ -0,0 +1,216 @@ +// Copyright 2013 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/** + * @fileoverview The file contains generated enumerations for ARIA roles + * as defined by W3C ARIA standard: http://www.w3.org/TR/wai-aria/. + * + * This is auto-generated code. Do not manually edit! For more details + * about how to edit it via the generator check go/closure-ariagen. + */ + +goog.provide('goog.a11y.aria.Role'); + + +/** + * ARIA role values. + * @enum {string} + */ +goog.a11y.aria.Role = { + // ARIA role for an alert element that doesn't need to be explicitly closed. + ALERT: 'alert', + + // ARIA role for an alert dialog element that takes focus and must be closed. + ALERTDIALOG: 'alertdialog', + + // ARIA role for an application that implements its own keyboard navigation. + APPLICATION: 'application', + + // ARIA role for an article. + ARTICLE: 'article', + + // ARIA role for a banner containing mostly site content, not page content. + BANNER: 'banner', + + // ARIA role for a button element. + BUTTON: 'button', + + // ARIA role for a checkbox button element; use with the CHECKED state. + CHECKBOX: 'checkbox', + + // ARIA role for a column header of a table or grid. + COLUMNHEADER: 'columnheader', + + // ARIA role for a combo box element. + COMBOBOX: 'combobox', + + // ARIA role for a supporting section of the document. + COMPLEMENTARY: 'complementary', + + // ARIA role for a large perceivable region that contains information + // about the parent document. + CONTENTINFO: 'contentinfo', + + // ARIA role for a definition of a term or concept. + DEFINITION: 'definition', + + // ARIA role for a dialog, some descendant must take initial focus. + DIALOG: 'dialog', + + // ARIA role for a directory, like a table of contents. + DIRECTORY: 'directory', + + // ARIA role for a part of a page that's a document, not a web application. + DOCUMENT: 'document', + + // ARIA role for a landmark region logically considered one form. + FORM: 'form', + + // ARIA role for an interactive control of tabular data. + GRID: 'grid', + + // ARIA role for a cell in a grid. + GRIDCELL: 'gridcell', + + // ARIA role for a group of related elements like tree item siblings. + GROUP: 'group', + + // ARIA role for a heading element. + HEADING: 'heading', + + // ARIA role for a container of elements that together comprise one image. + IMG: 'img', + + // ARIA role for a link. + LINK: 'link', + + // ARIA role for a list of non-interactive list items. + LIST: 'list', + + // ARIA role for a listbox. + LISTBOX: 'listbox', + + // ARIA role for a list item. + LISTITEM: 'listitem', + + // ARIA role for a live region where new information is added. + LOG: 'log', + + // ARIA landmark role for the main content in a document. Use only once. + MAIN: 'main', + + // ARIA role for a live region of non-essential information that changes. + MARQUEE: 'marquee', + + // ARIA role for a mathematical expression. + MATH: 'math', + + // ARIA role for a popup menu. + MENU: 'menu', + + // ARIA role for a menubar element containing menu elements. + MENUBAR: 'menubar', + + // ARIA role for menu item elements. + MENU_ITEM: 'menuitem', + + // ARIA role for a checkbox box element inside a menu. + MENU_ITEM_CHECKBOX: 'menuitemcheckbox', + + // ARIA role for a radio button element inside a menu. + MENU_ITEM_RADIO: 'menuitemradio', + + // ARIA landmark role for a collection of navigation links. + NAVIGATION: 'navigation', + + // ARIA role for a section ancillary to the main content. + NOTE: 'note', + + // ARIA role for option items that are children of combobox, listbox, menu, + // radiogroup, or tree elements. + OPTION: 'option', + + // ARIA role for ignorable cosmetic elements with no semantic significance. + PRESENTATION: 'presentation', + + // ARIA role for a progress bar element. + PROGRESSBAR: 'progressbar', + + // ARIA role for a radio button element. + RADIO: 'radio', + + // ARIA role for a group of connected radio button elements. + RADIOGROUP: 'radiogroup', + + // ARIA role for an important region of the page. + REGION: 'region', + + // ARIA role for a row of cells in a grid. + ROW: 'row', + + // ARIA role for a group of one or more rows in a grid. + ROWGROUP: 'rowgroup', + + // ARIA role for a row header of a table or grid. + ROWHEADER: 'rowheader', + + // ARIA role for a scrollbar element. + SCROLLBAR: 'scrollbar', + + // ARIA landmark role for a part of the page providing search functionality. + SEARCH: 'search', + + // ARIA role for a menu separator. + SEPARATOR: 'separator', + + // ARIA role for a slider. + SLIDER: 'slider', + + // ARIA role for a spin button. + SPINBUTTON: 'spinbutton', + + // ARIA role for a live region with advisory info less severe than an alert. + STATUS: 'status', + + // ARIA role for a tab button. + TAB: 'tab', + + // ARIA role for a tab bar (i.e. a list of tab buttons). + TAB_LIST: 'tablist', + + // ARIA role for a tab page (i.e. the element holding tab contents). + TAB_PANEL: 'tabpanel', + + // ARIA role for a textbox element. + TEXTBOX: 'textbox', + + // ARIA role for an element displaying elapsed time or time remaining. + TIMER: 'timer', + + // ARIA role for a toolbar element. + TOOLBAR: 'toolbar', + + // ARIA role for a tooltip element. + TOOLTIP: 'tooltip', + + // ARIA role for a tree. + TREE: 'tree', + + // ARIA role for a grid whose rows can be expanded and collapsed like a tree. + TREEGRID: 'treegrid', + + // ARIA role for a tree item that sometimes may be expanded or collapsed. + TREEITEM: 'treeitem' +}; diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/array/array.js b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/array/array.js new file mode 100644 index 00000000..d782cba8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/array/array.js @@ -0,0 +1,1526 @@ +// Copyright 2006 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Utilities for manipulating arrays. + * + */ + + +goog.provide('goog.array'); +goog.provide('goog.array.ArrayLike'); + +goog.require('goog.asserts'); + + +/** + * @define {boolean} NATIVE_ARRAY_PROTOTYPES indicates whether the code should + * rely on Array.prototype functions, if available. + * + * The Array.prototype functions can be defined by external libraries like + * Prototype and setting this flag to false forces closure to use its own + * goog.array implementation. + * + * If your javascript can be loaded by a third party site and you are wary about + * relying on the prototype functions, specify + * "--define goog.NATIVE_ARRAY_PROTOTYPES=false" to the JSCompiler. + * + * Setting goog.TRUSTED_SITE to false will automatically set + * NATIVE_ARRAY_PROTOTYPES to false. + */ +goog.define('goog.NATIVE_ARRAY_PROTOTYPES', goog.TRUSTED_SITE); + + +/** + * @typedef {Array|NodeList|Arguments|{length: number}} + */ +goog.array.ArrayLike; + + +/** + * Returns the last element in an array without removing it. + * @param {Array.|goog.array.ArrayLike} array The array. + * @return {T} Last item in array. + * @template T + */ +goog.array.peek = function(array) { + return array[array.length - 1]; +}; + + +/** + * Reference to the original {@code Array.prototype}. + * @private + */ +goog.array.ARRAY_PROTOTYPE_ = Array.prototype; + + +// NOTE(arv): Since most of the array functions are generic it allows you to +// pass an array-like object. Strings have a length and are considered array- +// like. However, the 'in' operator does not work on strings so we cannot just +// use the array path even if the browser supports indexing into strings. We +// therefore end up splitting the string. + + +/** + * Returns the index of the first element of an array with a specified value, or + * -1 if the element is not present in the array. + * + * See {@link http://tinyurl.com/developer-mozilla-org-array-indexof} + * + * @param {Array.|goog.array.ArrayLike} arr The array to be searched. + * @param {T} obj The object for which we are searching. + * @param {number=} opt_fromIndex The index at which to start the search. If + * omitted the search starts at index 0. + * @return {number} The index of the first matching array element. + * @template T + */ +goog.array.indexOf = goog.NATIVE_ARRAY_PROTOTYPES && + goog.array.ARRAY_PROTOTYPE_.indexOf ? + function(arr, obj, opt_fromIndex) { + goog.asserts.assert(arr.length != null); + + return goog.array.ARRAY_PROTOTYPE_.indexOf.call(arr, obj, opt_fromIndex); + } : + function(arr, obj, opt_fromIndex) { + var fromIndex = opt_fromIndex == null ? + 0 : (opt_fromIndex < 0 ? + Math.max(0, arr.length + opt_fromIndex) : opt_fromIndex); + + if (goog.isString(arr)) { + // Array.prototype.indexOf uses === so only strings should be found. + if (!goog.isString(obj) || obj.length != 1) { + return -1; + } + return arr.indexOf(obj, fromIndex); + } + + for (var i = fromIndex; i < arr.length; i++) { + if (i in arr && arr[i] === obj) + return i; + } + return -1; + }; + + +/** + * Returns the index of the last element of an array with a specified value, or + * -1 if the element is not present in the array. + * + * See {@link http://tinyurl.com/developer-mozilla-org-array-lastindexof} + * + * @param {!Array.|!goog.array.ArrayLike} arr The array to be searched. + * @param {T} obj The object for which we are searching. + * @param {?number=} opt_fromIndex The index at which to start the search. If + * omitted the search starts at the end of the array. + * @return {number} The index of the last matching array element. + * @template T + */ +goog.array.lastIndexOf = goog.NATIVE_ARRAY_PROTOTYPES && + goog.array.ARRAY_PROTOTYPE_.lastIndexOf ? + function(arr, obj, opt_fromIndex) { + goog.asserts.assert(arr.length != null); + + // Firefox treats undefined and null as 0 in the fromIndex argument which + // leads it to always return -1 + var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex; + return goog.array.ARRAY_PROTOTYPE_.lastIndexOf.call(arr, obj, fromIndex); + } : + function(arr, obj, opt_fromIndex) { + var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex; + + if (fromIndex < 0) { + fromIndex = Math.max(0, arr.length + fromIndex); + } + + if (goog.isString(arr)) { + // Array.prototype.lastIndexOf uses === so only strings should be found. + if (!goog.isString(obj) || obj.length != 1) { + return -1; + } + return arr.lastIndexOf(obj, fromIndex); + } + + for (var i = fromIndex; i >= 0; i--) { + if (i in arr && arr[i] === obj) + return i; + } + return -1; + }; + + +/** + * Calls a function for each element in an array. Skips holes in the array. + * See {@link http://tinyurl.com/developer-mozilla-org-array-foreach} + * + * @param {Array.|goog.array.ArrayLike} arr Array or array like object over + * which to iterate. + * @param {?function(this: S, T, number, ?): ?} f The function to call for every + * element. This function takes 3 arguments (the element, the index and the + * array). The return value is ignored. + * @param {S=} opt_obj The object to be used as the value of 'this' within f. + * @template T,S + */ +goog.array.forEach = goog.NATIVE_ARRAY_PROTOTYPES && + goog.array.ARRAY_PROTOTYPE_.forEach ? + function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + + goog.array.ARRAY_PROTOTYPE_.forEach.call(arr, f, opt_obj); + } : + function(arr, f, opt_obj) { + var l = arr.length; // must be fixed during loop... see docs + var arr2 = goog.isString(arr) ? arr.split('') : arr; + for (var i = 0; i < l; i++) { + if (i in arr2) { + f.call(opt_obj, arr2[i], i, arr); + } + } + }; + + +/** + * Calls a function for each element in an array, starting from the last + * element rather than the first. + * + * @param {Array.|goog.array.ArrayLike} arr Array or array + * like object over which to iterate. + * @param {?function(this: S, T, number, ?): ?} f The function to call for every + * element. This function + * takes 3 arguments (the element, the index and the array). The return + * value is ignored. + * @param {S=} opt_obj The object to be used as the value of 'this' + * within f. + * @template T,S + */ +goog.array.forEachRight = function(arr, f, opt_obj) { + var l = arr.length; // must be fixed during loop... see docs + var arr2 = goog.isString(arr) ? arr.split('') : arr; + for (var i = l - 1; i >= 0; --i) { + if (i in arr2) { + f.call(opt_obj, arr2[i], i, arr); + } + } +}; + + +/** + * Calls a function for each element in an array, and if the function returns + * true adds the element to a new array. + * + * See {@link http://tinyurl.com/developer-mozilla-org-array-filter} + * + * @param {Array.|goog.array.ArrayLike} arr Array or array + * like object over which to iterate. + * @param {?function(this:S, T, number, ?):boolean} f The function to call for + * every element. This function + * takes 3 arguments (the element, the index and the array) and must + * return a Boolean. If the return value is true the element is added to the + * result array. If it is false the element is not included. + * @param {S=} opt_obj The object to be used as the value of 'this' + * within f. + * @return {!Array.} a new array in which only elements that passed the test + * are present. + * @template T,S + */ +goog.array.filter = goog.NATIVE_ARRAY_PROTOTYPES && + goog.array.ARRAY_PROTOTYPE_.filter ? + function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + + return goog.array.ARRAY_PROTOTYPE_.filter.call(arr, f, opt_obj); + } : + function(arr, f, opt_obj) { + var l = arr.length; // must be fixed during loop... see docs + var res = []; + var resLength = 0; + var arr2 = goog.isString(arr) ? arr.split('') : arr; + for (var i = 0; i < l; i++) { + if (i in arr2) { + var val = arr2[i]; // in case f mutates arr2 + if (f.call(opt_obj, val, i, arr)) { + res[resLength++] = val; + } + } + } + return res; + }; + + +/** + * Calls a function for each element in an array and inserts the result into a + * new array. + * + * See {@link http://tinyurl.com/developer-mozilla-org-array-map} + * + * @param {Array.|goog.array.ArrayLike} arr Array or array + * like object over which to iterate. + * @param {?function(this:S, T, number, ?):?} f The function to call for every + * element. This function + * takes 3 arguments (the element, the index and the array) and should + * return something. The result will be inserted into a new array. + * @param {S=} opt_obj The object to be used as the value of 'this' + * within f. + * @return {!Array} a new array with the results from f. + * @template T,S + */ +goog.array.map = goog.NATIVE_ARRAY_PROTOTYPES && + goog.array.ARRAY_PROTOTYPE_.map ? + function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + + return goog.array.ARRAY_PROTOTYPE_.map.call(arr, f, opt_obj); + } : + function(arr, f, opt_obj) { + var l = arr.length; // must be fixed during loop... see docs + var res = new Array(l); + var arr2 = goog.isString(arr) ? arr.split('') : arr; + for (var i = 0; i < l; i++) { + if (i in arr2) { + res[i] = f.call(opt_obj, arr2[i], i, arr); + } + } + return res; + }; + + +/** + * Passes every element of an array into a function and accumulates the result. + * + * See {@link http://tinyurl.com/developer-mozilla-org-array-reduce} + * + * For example: + * var a = [1, 2, 3, 4]; + * goog.array.reduce(a, function(r, v, i, arr) {return r + v;}, 0); + * returns 10 + * + * @param {Array.|goog.array.ArrayLike} arr Array or array + * like object over which to iterate. + * @param {?function(this:S, R, T, number, ?) : R} f The function to call for + * every element. This function + * takes 4 arguments (the function's previous result or the initial value, + * the value of the current array element, the current array index, and the + * array itself) + * function(previousValue, currentValue, index, array). + * @param {?} val The initial value to pass into the function on the first call. + * @param {S=} opt_obj The object to be used as the value of 'this' + * within f. + * @return {R} Result of evaluating f repeatedly across the values of the array. + * @template T,S,R + */ +goog.array.reduce = goog.NATIVE_ARRAY_PROTOTYPES && + goog.array.ARRAY_PROTOTYPE_.reduce ? + function(arr, f, val, opt_obj) { + goog.asserts.assert(arr.length != null); + if (opt_obj) { + f = goog.bind(f, opt_obj); + } + return goog.array.ARRAY_PROTOTYPE_.reduce.call(arr, f, val); + } : + function(arr, f, val, opt_obj) { + var rval = val; + goog.array.forEach(arr, function(val, index) { + rval = f.call(opt_obj, rval, val, index, arr); + }); + return rval; + }; + + +/** + * Passes every element of an array into a function and accumulates the result, + * starting from the last element and working towards the first. + * + * See {@link http://tinyurl.com/developer-mozilla-org-array-reduceright} + * + * For example: + * var a = ['a', 'b', 'c']; + * goog.array.reduceRight(a, function(r, v, i, arr) {return r + v;}, ''); + * returns 'cba' + * + * @param {Array.|goog.array.ArrayLike} arr Array or array + * like object over which to iterate. + * @param {?function(this:S, R, T, number, ?) : R} f The function to call for + * every element. This function + * takes 4 arguments (the function's previous result or the initial value, + * the value of the current array element, the current array index, and the + * array itself) + * function(previousValue, currentValue, index, array). + * @param {?} val The initial value to pass into the function on the first call. + * @param {S=} opt_obj The object to be used as the value of 'this' + * within f. + * @return {R} Object returned as a result of evaluating f repeatedly across the + * values of the array. + * @template T,S,R + */ +goog.array.reduceRight = goog.NATIVE_ARRAY_PROTOTYPES && + goog.array.ARRAY_PROTOTYPE_.reduceRight ? + function(arr, f, val, opt_obj) { + goog.asserts.assert(arr.length != null); + if (opt_obj) { + f = goog.bind(f, opt_obj); + } + return goog.array.ARRAY_PROTOTYPE_.reduceRight.call(arr, f, val); + } : + function(arr, f, val, opt_obj) { + var rval = val; + goog.array.forEachRight(arr, function(val, index) { + rval = f.call(opt_obj, rval, val, index, arr); + }); + return rval; + }; + + +/** + * Calls f for each element of an array. If any call returns true, some() + * returns true (without checking the remaining elements). If all calls + * return false, some() returns false. + * + * See {@link http://tinyurl.com/developer-mozilla-org-array-some} + * + * @param {Array.|goog.array.ArrayLike} arr Array or array + * like object over which to iterate. + * @param {?function(this:S, T, number, ?) : boolean} f The function to call for + * for every element. This function takes 3 arguments (the element, the + * index and the array) and should return a boolean. + * @param {S=} opt_obj The object to be used as the value of 'this' + * within f. + * @return {boolean} true if any element passes the test. + * @template T,S + */ +goog.array.some = goog.NATIVE_ARRAY_PROTOTYPES && + goog.array.ARRAY_PROTOTYPE_.some ? + function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + + return goog.array.ARRAY_PROTOTYPE_.some.call(arr, f, opt_obj); + } : + function(arr, f, opt_obj) { + var l = arr.length; // must be fixed during loop... see docs + var arr2 = goog.isString(arr) ? arr.split('') : arr; + for (var i = 0; i < l; i++) { + if (i in arr2 && f.call(opt_obj, arr2[i], i, arr)) { + return true; + } + } + return false; + }; + + +/** + * Call f for each element of an array. If all calls return true, every() + * returns true. If any call returns false, every() returns false and + * does not continue to check the remaining elements. + * + * See {@link http://tinyurl.com/developer-mozilla-org-array-every} + * + * @param {Array.|goog.array.ArrayLike} arr Array or array + * like object over which to iterate. + * @param {?function(this:S, T, number, ?) : boolean} f The function to call for + * for every element. This function takes 3 arguments (the element, the + * index and the array) and should return a boolean. + * @param {S=} opt_obj The object to be used as the value of 'this' + * within f. + * @return {boolean} false if any element fails the test. + * @template T,S + */ +goog.array.every = goog.NATIVE_ARRAY_PROTOTYPES && + goog.array.ARRAY_PROTOTYPE_.every ? + function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + + return goog.array.ARRAY_PROTOTYPE_.every.call(arr, f, opt_obj); + } : + function(arr, f, opt_obj) { + var l = arr.length; // must be fixed during loop... see docs + var arr2 = goog.isString(arr) ? arr.split('') : arr; + for (var i = 0; i < l; i++) { + if (i in arr2 && !f.call(opt_obj, arr2[i], i, arr)) { + return false; + } + } + return true; + }; + + +/** + * Counts the array elements that fulfill the predicate, i.e. for which the + * callback function returns true. Skips holes in the array. + * + * @param {!(Array.|goog.array.ArrayLike)} arr Array or array like object + * over which to iterate. + * @param {function(this: S, T, number, ?): boolean} f The function to call for + * every element. Takes 3 arguments (the element, the index and the array). + * @param {S=} opt_obj The object to be used as the value of 'this' within f. + * @return {number} The number of the matching elements. + * @template T,S + */ +goog.array.count = function(arr, f, opt_obj) { + var count = 0; + goog.array.forEach(arr, function(element, index, arr) { + if (f.call(opt_obj, element, index, arr)) { + ++count; + } + }, opt_obj); + return count; +}; + + +/** + * Search an array for the first element that satisfies a given condition and + * return that element. + * @param {Array.|goog.array.ArrayLike} arr Array or array + * like object over which to iterate. + * @param {?function(this:S, T, number, ?) : boolean} f The function to call + * for every element. This function takes 3 arguments (the element, the + * index and the array) and should return a boolean. + * @param {S=} opt_obj An optional "this" context for the function. + * @return {T} The first array element that passes the test, or null if no + * element is found. + * @template T,S + */ +goog.array.find = function(arr, f, opt_obj) { + var i = goog.array.findIndex(arr, f, opt_obj); + return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i]; +}; + + +/** + * Search an array for the first element that satisfies a given condition and + * return its index. + * @param {Array.|goog.array.ArrayLike} arr Array or array + * like object over which to iterate. + * @param {?function(this:S, T, number, ?) : boolean} f The function to call for + * every element. This function + * takes 3 arguments (the element, the index and the array) and should + * return a boolean. + * @param {S=} opt_obj An optional "this" context for the function. + * @return {number} The index of the first array element that passes the test, + * or -1 if no element is found. + * @template T,S + */ +goog.array.findIndex = function(arr, f, opt_obj) { + var l = arr.length; // must be fixed during loop... see docs + var arr2 = goog.isString(arr) ? arr.split('') : arr; + for (var i = 0; i < l; i++) { + if (i in arr2 && f.call(opt_obj, arr2[i], i, arr)) { + return i; + } + } + return -1; +}; + + +/** + * Search an array (in reverse order) for the last element that satisfies a + * given condition and return that element. + * @param {Array.|goog.array.ArrayLike} arr Array or array + * like object over which to iterate. + * @param {?function(this:S, T, number, ?) : boolean} f The function to call + * for every element. This function + * takes 3 arguments (the element, the index and the array) and should + * return a boolean. + * @param {S=} opt_obj An optional "this" context for the function. + * @return {T} The last array element that passes the test, or null if no + * element is found. + * @template T,S + */ +goog.array.findRight = function(arr, f, opt_obj) { + var i = goog.array.findIndexRight(arr, f, opt_obj); + return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i]; +}; + + +/** + * Search an array (in reverse order) for the last element that satisfies a + * given condition and return its index. + * @param {Array.|goog.array.ArrayLike} arr Array or array + * like object over which to iterate. + * @param {?function(this:S, T, number, ?) : boolean} f The function to call + * for every element. This function + * takes 3 arguments (the element, the index and the array) and should + * return a boolean. + * @param {Object=} opt_obj An optional "this" context for the function. + * @return {number} The index of the last array element that passes the test, + * or -1 if no element is found. + * @template T,S + */ +goog.array.findIndexRight = function(arr, f, opt_obj) { + var l = arr.length; // must be fixed during loop... see docs + var arr2 = goog.isString(arr) ? arr.split('') : arr; + for (var i = l - 1; i >= 0; i--) { + if (i in arr2 && f.call(opt_obj, arr2[i], i, arr)) { + return i; + } + } + return -1; +}; + + +/** + * Whether the array contains the given object. + * @param {goog.array.ArrayLike} arr The array to test for the presence of the + * element. + * @param {*} obj The object for which to test. + * @return {boolean} true if obj is present. + */ +goog.array.contains = function(arr, obj) { + return goog.array.indexOf(arr, obj) >= 0; +}; + + +/** + * Whether the array is empty. + * @param {goog.array.ArrayLike} arr The array to test. + * @return {boolean} true if empty. + */ +goog.array.isEmpty = function(arr) { + return arr.length == 0; +}; + + +/** + * Clears the array. + * @param {goog.array.ArrayLike} arr Array or array like object to clear. + */ +goog.array.clear = function(arr) { + // For non real arrays we don't have the magic length so we delete the + // indices. + if (!goog.isArray(arr)) { + for (var i = arr.length - 1; i >= 0; i--) { + delete arr[i]; + } + } + arr.length = 0; +}; + + +/** + * Pushes an item into an array, if it's not already in the array. + * @param {Array.} arr Array into which to insert the item. + * @param {T} obj Value to add. + * @template T + */ +goog.array.insert = function(arr, obj) { + if (!goog.array.contains(arr, obj)) { + arr.push(obj); + } +}; + + +/** + * Inserts an object at the given index of the array. + * @param {goog.array.ArrayLike} arr The array to modify. + * @param {*} obj The object to insert. + * @param {number=} opt_i The index at which to insert the object. If omitted, + * treated as 0. A negative index is counted from the end of the array. + */ +goog.array.insertAt = function(arr, obj, opt_i) { + goog.array.splice(arr, opt_i, 0, obj); +}; + + +/** + * Inserts at the given index of the array, all elements of another array. + * @param {goog.array.ArrayLike} arr The array to modify. + * @param {goog.array.ArrayLike} elementsToAdd The array of elements to add. + * @param {number=} opt_i The index at which to insert the object. If omitted, + * treated as 0. A negative index is counted from the end of the array. + */ +goog.array.insertArrayAt = function(arr, elementsToAdd, opt_i) { + goog.partial(goog.array.splice, arr, opt_i, 0).apply(null, elementsToAdd); +}; + + +/** + * Inserts an object into an array before a specified object. + * @param {Array.} arr The array to modify. + * @param {T} obj The object to insert. + * @param {T=} opt_obj2 The object before which obj should be inserted. If obj2 + * is omitted or not found, obj is inserted at the end of the array. + * @template T + */ +goog.array.insertBefore = function(arr, obj, opt_obj2) { + var i; + if (arguments.length == 2 || (i = goog.array.indexOf(arr, opt_obj2)) < 0) { + arr.push(obj); + } else { + goog.array.insertAt(arr, obj, i); + } +}; + + +/** + * Removes the first occurrence of a particular value from an array. + * @param {Array.|goog.array.ArrayLike} arr Array from which to remove + * value. + * @param {T} obj Object to remove. + * @return {boolean} True if an element was removed. + * @template T + */ +goog.array.remove = function(arr, obj) { + var i = goog.array.indexOf(arr, obj); + var rv; + if ((rv = i >= 0)) { + goog.array.removeAt(arr, i); + } + return rv; +}; + + +/** + * Removes from an array the element at index i + * @param {goog.array.ArrayLike} arr Array or array like object from which to + * remove value. + * @param {number} i The index to remove. + * @return {boolean} True if an element was removed. + */ +goog.array.removeAt = function(arr, i) { + goog.asserts.assert(arr.length != null); + + // use generic form of splice + // splice returns the removed items and if successful the length of that + // will be 1 + return goog.array.ARRAY_PROTOTYPE_.splice.call(arr, i, 1).length == 1; +}; + + +/** + * Removes the first value that satisfies the given condition. + * @param {Array.|goog.array.ArrayLike} arr Array or array + * like object over which to iterate. + * @param {?function(this:S, T, number, ?) : boolean} f The function to call + * for every element. This function + * takes 3 arguments (the element, the index and the array) and should + * return a boolean. + * @param {S=} opt_obj An optional "this" context for the function. + * @return {boolean} True if an element was removed. + * @template T,S + */ +goog.array.removeIf = function(arr, f, opt_obj) { + var i = goog.array.findIndex(arr, f, opt_obj); + if (i >= 0) { + goog.array.removeAt(arr, i); + return true; + } + return false; +}; + + +/** + * Returns a new array that is the result of joining the arguments. If arrays + * are passed then their items are added, however, if non-arrays are passed they + * will be added to the return array as is. + * + * Note that ArrayLike objects will be added as is, rather than having their + * items added. + * + * goog.array.concat([1, 2], [3, 4]) -> [1, 2, 3, 4] + * goog.array.concat(0, [1, 2]) -> [0, 1, 2] + * goog.array.concat([1, 2], null) -> [1, 2, null] + * + * There is bug in all current versions of IE (6, 7 and 8) where arrays created + * in an iframe become corrupted soon (not immediately) after the iframe is + * destroyed. This is common if loading data via goog.net.IframeIo, for example. + * This corruption only affects the concat method which will start throwing + * Catastrophic Errors (#-2147418113). + * + * See http://endoflow.com/scratch/corrupted-arrays.html for a test case. + * + * Internally goog.array should use this, so that all methods will continue to + * work on these broken array objects. + * + * @param {...*} var_args Items to concatenate. Arrays will have each item + * added, while primitives and objects will be added as is. + * @return {!Array} The new resultant array. + */ +goog.array.concat = function(var_args) { + return goog.array.ARRAY_PROTOTYPE_.concat.apply( + goog.array.ARRAY_PROTOTYPE_, arguments); +}; + + +/** + * Converts an object to an array. + * @param {goog.array.ArrayLike} object The object to convert to an array. + * @return {!Array} The object converted into an array. If object has a + * length property, every property indexed with a non-negative number + * less than length will be included in the result. If object does not + * have a length property, an empty array will be returned. + */ +goog.array.toArray = function(object) { + var length = object.length; + + // If length is not a number the following it false. This case is kept for + // backwards compatibility since there are callers that pass objects that are + // not array like. + if (length > 0) { + var rv = new Array(length); + for (var i = 0; i < length; i++) { + rv[i] = object[i]; + } + return rv; + } + return []; +}; + + +/** + * Does a shallow copy of an array. + * @param {goog.array.ArrayLike} arr Array or array-like object to clone. + * @return {!Array} Clone of the input array. + */ +goog.array.clone = goog.array.toArray; + + +/** + * Extends an array with another array, element, or "array like" object. + * This function operates 'in-place', it does not create a new Array. + * + * Example: + * var a = []; + * goog.array.extend(a, [0, 1]); + * a; // [0, 1] + * goog.array.extend(a, 2); + * a; // [0, 1, 2] + * + * @param {Array} arr1 The array to modify. + * @param {...*} var_args The elements or arrays of elements to add to arr1. + */ +goog.array.extend = function(arr1, var_args) { + for (var i = 1; i < arguments.length; i++) { + var arr2 = arguments[i]; + // If we have an Array or an Arguments object we can just call push + // directly. + var isArrayLike; + if (goog.isArray(arr2) || + // Detect Arguments. ES5 says that the [[Class]] of an Arguments object + // is "Arguments" but only V8 and JSC/Safari gets this right. We instead + // detect Arguments by checking for array like and presence of "callee". + (isArrayLike = goog.isArrayLike(arr2)) && + // The getter for callee throws an exception in strict mode + // according to section 10.6 in ES5 so check for presence instead. + Object.prototype.hasOwnProperty.call(arr2, 'callee')) { + arr1.push.apply(arr1, arr2); + } else if (isArrayLike) { + // Otherwise loop over arr2 to prevent copying the object. + var len1 = arr1.length; + var len2 = arr2.length; + for (var j = 0; j < len2; j++) { + arr1[len1 + j] = arr2[j]; + } + } else { + arr1.push(arr2); + } + } +}; + + +/** + * Adds or removes elements from an array. This is a generic version of Array + * splice. This means that it might work on other objects similar to arrays, + * such as the arguments object. + * + * @param {Array.|goog.array.ArrayLike} arr The array to modify. + * @param {number|undefined} index The index at which to start changing the + * array. If not defined, treated as 0. + * @param {number} howMany How many elements to remove (0 means no removal. A + * value below 0 is treated as zero and so is any other non number. Numbers + * are floored). + * @param {...T} var_args Optional, additional elements to insert into the + * array. + * @return {!Array.} the removed elements. + * @template T + */ +goog.array.splice = function(arr, index, howMany, var_args) { + goog.asserts.assert(arr.length != null); + + return goog.array.ARRAY_PROTOTYPE_.splice.apply( + arr, goog.array.slice(arguments, 1)); +}; + + +/** + * Returns a new array from a segment of an array. This is a generic version of + * Array slice. This means that it might work on other objects similar to + * arrays, such as the arguments object. + * + * @param {Array.|goog.array.ArrayLike} arr The array from + * which to copy a segment. + * @param {number} start The index of the first element to copy. + * @param {number=} opt_end The index after the last element to copy. + * @return {!Array.} A new array containing the specified segment of the + * original array. + * @template T + */ +goog.array.slice = function(arr, start, opt_end) { + goog.asserts.assert(arr.length != null); + + // passing 1 arg to slice is not the same as passing 2 where the second is + // null or undefined (in that case the second argument is treated as 0). + // we could use slice on the arguments object and then use apply instead of + // testing the length + if (arguments.length <= 2) { + return goog.array.ARRAY_PROTOTYPE_.slice.call(arr, start); + } else { + return goog.array.ARRAY_PROTOTYPE_.slice.call(arr, start, opt_end); + } +}; + + +/** + * Removes all duplicates from an array (retaining only the first + * occurrence of each array element). This function modifies the + * array in place and doesn't change the order of the non-duplicate items. + * + * For objects, duplicates are identified as having the same unique ID as + * defined by {@link goog.getUid}. + * + * Alternatively you can specify a custom hash function that returns a unique + * value for each item in the array it should consider unique. + * + * Runtime: N, + * Worstcase space: 2N (no dupes) + * + * @param {Array.|goog.array.ArrayLike} arr The array from which to remove + * duplicates. + * @param {Array=} opt_rv An optional array in which to return the results, + * instead of performing the removal inplace. If specified, the original + * array will remain unchanged. + * @param {function(T):string=} opt_hashFn An optional function to use to + * apply to every item in the array. This function should return a unique + * value for each item in the array it should consider unique. + * @template T + */ +goog.array.removeDuplicates = function(arr, opt_rv, opt_hashFn) { + var returnArray = opt_rv || arr; + var defaultHashFn = function(item) { + // Prefix each type with a single character representing the type to + // prevent conflicting keys (e.g. true and 'true'). + return goog.isObject(current) ? 'o' + goog.getUid(current) : + (typeof current).charAt(0) + current; + }; + var hashFn = opt_hashFn || defaultHashFn; + + var seen = {}, cursorInsert = 0, cursorRead = 0; + while (cursorRead < arr.length) { + var current = arr[cursorRead++]; + var key = hashFn(current); + if (!Object.prototype.hasOwnProperty.call(seen, key)) { + seen[key] = true; + returnArray[cursorInsert++] = current; + } + } + returnArray.length = cursorInsert; +}; + + +/** + * Searches the specified array for the specified target using the binary + * search algorithm. If no opt_compareFn is specified, elements are compared + * using goog.array.defaultCompare, which compares the elements + * using the built in < and > operators. This will produce the expected + * behavior for homogeneous arrays of String(s) and Number(s). The array + * specified must be sorted in ascending order (as defined by the + * comparison function). If the array is not sorted, results are undefined. + * If the array contains multiple instances of the specified target value, any + * of these instances may be found. + * + * Runtime: O(log n) + * + * @param {goog.array.ArrayLike} arr The array to be searched. + * @param {*} target The sought value. + * @param {Function=} opt_compareFn Optional comparison function by which the + * array is ordered. Should take 2 arguments to compare, and return a + * negative number, zero, or a positive number depending on whether the + * first argument is less than, equal to, or greater than the second. + * @return {number} Lowest index of the target value if found, otherwise + * (-(insertion point) - 1). The insertion point is where the value should + * be inserted into arr to preserve the sorted property. Return value >= 0 + * iff target is found. + */ +goog.array.binarySearch = function(arr, target, opt_compareFn) { + return goog.array.binarySearch_(arr, + opt_compareFn || goog.array.defaultCompare, false /* isEvaluator */, + target); +}; + + +/** + * Selects an index in the specified array using the binary search algorithm. + * The evaluator receives an element and determines whether the desired index + * is before, at, or after it. The evaluator must be consistent (formally, + * goog.array.map(goog.array.map(arr, evaluator, opt_obj), goog.math.sign) + * must be monotonically non-increasing). + * + * Runtime: O(log n) + * + * @param {goog.array.ArrayLike} arr The array to be searched. + * @param {Function} evaluator Evaluator function that receives 3 arguments + * (the element, the index and the array). Should return a negative number, + * zero, or a positive number depending on whether the desired index is + * before, at, or after the element passed to it. + * @param {Object=} opt_obj The object to be used as the value of 'this' + * within evaluator. + * @return {number} Index of the leftmost element matched by the evaluator, if + * such exists; otherwise (-(insertion point) - 1). The insertion point is + * the index of the first element for which the evaluator returns negative, + * or arr.length if no such element exists. The return value is non-negative + * iff a match is found. + */ +goog.array.binarySelect = function(arr, evaluator, opt_obj) { + return goog.array.binarySearch_(arr, evaluator, true /* isEvaluator */, + undefined /* opt_target */, opt_obj); +}; + + +/** + * Implementation of a binary search algorithm which knows how to use both + * comparison functions and evaluators. If an evaluator is provided, will call + * the evaluator with the given optional data object, conforming to the + * interface defined in binarySelect. Otherwise, if a comparison function is + * provided, will call the comparison function against the given data object. + * + * This implementation purposefully does not use goog.bind or goog.partial for + * performance reasons. + * + * Runtime: O(log n) + * + * @param {goog.array.ArrayLike} arr The array to be searched. + * @param {Function} compareFn Either an evaluator or a comparison function, + * as defined by binarySearch and binarySelect above. + * @param {boolean} isEvaluator Whether the function is an evaluator or a + * comparison function. + * @param {*=} opt_target If the function is a comparison function, then this is + * the target to binary search for. + * @param {Object=} opt_selfObj If the function is an evaluator, this is an + * optional this object for the evaluator. + * @return {number} Lowest index of the target value if found, otherwise + * (-(insertion point) - 1). The insertion point is where the value should + * be inserted into arr to preserve the sorted property. Return value >= 0 + * iff target is found. + * @private + */ +goog.array.binarySearch_ = function(arr, compareFn, isEvaluator, opt_target, + opt_selfObj) { + var left = 0; // inclusive + var right = arr.length; // exclusive + var found; + while (left < right) { + var middle = (left + right) >> 1; + var compareResult; + if (isEvaluator) { + compareResult = compareFn.call(opt_selfObj, arr[middle], middle, arr); + } else { + compareResult = compareFn(opt_target, arr[middle]); + } + if (compareResult > 0) { + left = middle + 1; + } else { + right = middle; + // We are looking for the lowest index so we can't return immediately. + found = !compareResult; + } + } + // left is the index if found, or the insertion point otherwise. + // ~left is a shorthand for -left - 1. + return found ? left : ~left; +}; + + +/** + * Sorts the specified array into ascending order. If no opt_compareFn is + * specified, elements are compared using + * goog.array.defaultCompare, which compares the elements using + * the built in < and > operators. This will produce the expected behavior + * for homogeneous arrays of String(s) and Number(s), unlike the native sort, + * but will give unpredictable results for heterogenous lists of strings and + * numbers with different numbers of digits. + * + * This sort is not guaranteed to be stable. + * + * Runtime: Same as Array.prototype.sort + * + * @param {Array.} arr The array to be sorted. + * @param {?function(T,T):number=} opt_compareFn Optional comparison + * function by which the + * array is to be ordered. Should take 2 arguments to compare, and return a + * negative number, zero, or a positive number depending on whether the + * first argument is less than, equal to, or greater than the second. + * @template T + */ +goog.array.sort = function(arr, opt_compareFn) { + // TODO(arv): Update type annotation since null is not accepted. + goog.asserts.assert(arr.length != null); + + goog.array.ARRAY_PROTOTYPE_.sort.call( + arr, opt_compareFn || goog.array.defaultCompare); +}; + + +/** + * Sorts the specified array into ascending order in a stable way. If no + * opt_compareFn is specified, elements are compared using + * goog.array.defaultCompare, which compares the elements using + * the built in < and > operators. This will produce the expected behavior + * for homogeneous arrays of String(s) and Number(s). + * + * Runtime: Same as Array.prototype.sort, plus an additional + * O(n) overhead of copying the array twice. + * + * @param {Array.} arr The array to be sorted. + * @param {?function(T, T): number=} opt_compareFn Optional comparison function + * by which the array is to be ordered. Should take 2 arguments to compare, + * and return a negative number, zero, or a positive number depending on + * whether the first argument is less than, equal to, or greater than the + * second. + * @template T + */ +goog.array.stableSort = function(arr, opt_compareFn) { + for (var i = 0; i < arr.length; i++) { + arr[i] = {index: i, value: arr[i]}; + } + var valueCompareFn = opt_compareFn || goog.array.defaultCompare; + function stableCompareFn(obj1, obj2) { + return valueCompareFn(obj1.value, obj2.value) || obj1.index - obj2.index; + }; + goog.array.sort(arr, stableCompareFn); + for (var i = 0; i < arr.length; i++) { + arr[i] = arr[i].value; + } +}; + + +/** + * Sorts an array of objects by the specified object key and compare + * function. If no compare function is provided, the key values are + * compared in ascending order using goog.array.defaultCompare. + * This won't work for keys that get renamed by the compiler. So use + * {'foo': 1, 'bar': 2} rather than {foo: 1, bar: 2}. + * @param {Array.} arr An array of objects to sort. + * @param {string} key The object key to sort by. + * @param {Function=} opt_compareFn The function to use to compare key + * values. + */ +goog.array.sortObjectsByKey = function(arr, key, opt_compareFn) { + var compare = opt_compareFn || goog.array.defaultCompare; + goog.array.sort(arr, function(a, b) { + return compare(a[key], b[key]); + }); +}; + + +/** + * Tells if the array is sorted. + * @param {!Array.} arr The array. + * @param {?function(T,T):number=} opt_compareFn Function to compare the + * array elements. + * Should take 2 arguments to compare, and return a negative number, zero, + * or a positive number depending on whether the first argument is less + * than, equal to, or greater than the second. + * @param {boolean=} opt_strict If true no equal elements are allowed. + * @return {boolean} Whether the array is sorted. + * @template T + */ +goog.array.isSorted = function(arr, opt_compareFn, opt_strict) { + var compare = opt_compareFn || goog.array.defaultCompare; + for (var i = 1; i < arr.length; i++) { + var compareResult = compare(arr[i - 1], arr[i]); + if (compareResult > 0 || compareResult == 0 && opt_strict) { + return false; + } + } + return true; +}; + + +/** + * Compares two arrays for equality. Two arrays are considered equal if they + * have the same length and their corresponding elements are equal according to + * the comparison function. + * + * @param {goog.array.ArrayLike} arr1 The first array to compare. + * @param {goog.array.ArrayLike} arr2 The second array to compare. + * @param {Function=} opt_equalsFn Optional comparison function. + * Should take 2 arguments to compare, and return true if the arguments + * are equal. Defaults to {@link goog.array.defaultCompareEquality} which + * compares the elements using the built-in '===' operator. + * @return {boolean} Whether the two arrays are equal. + */ +goog.array.equals = function(arr1, arr2, opt_equalsFn) { + if (!goog.isArrayLike(arr1) || !goog.isArrayLike(arr2) || + arr1.length != arr2.length) { + return false; + } + var l = arr1.length; + var equalsFn = opt_equalsFn || goog.array.defaultCompareEquality; + for (var i = 0; i < l; i++) { + if (!equalsFn(arr1[i], arr2[i])) { + return false; + } + } + return true; +}; + + +/** + * @deprecated Use {@link goog.array.equals}. + * @param {goog.array.ArrayLike} arr1 See {@link goog.array.equals}. + * @param {goog.array.ArrayLike} arr2 See {@link goog.array.equals}. + * @param {Function=} opt_equalsFn See {@link goog.array.equals}. + * @return {boolean} See {@link goog.array.equals}. + */ +goog.array.compare = function(arr1, arr2, opt_equalsFn) { + return goog.array.equals(arr1, arr2, opt_equalsFn); +}; + + +/** + * 3-way array compare function. + * @param {!goog.array.ArrayLike} arr1 The first array to compare. + * @param {!goog.array.ArrayLike} arr2 The second array to compare. + * @param {?function(?, ?): number=} opt_compareFn Optional comparison function + * by which the array is to be ordered. Should take 2 arguments to compare, + * and return a negative number, zero, or a positive number depending on + * whether the first argument is less than, equal to, or greater than the + * second. + * @return {number} Negative number, zero, or a positive number depending on + * whether the first argument is less than, equal to, or greater than the + * second. + */ +goog.array.compare3 = function(arr1, arr2, opt_compareFn) { + var compare = opt_compareFn || goog.array.defaultCompare; + var l = Math.min(arr1.length, arr2.length); + for (var i = 0; i < l; i++) { + var result = compare(arr1[i], arr2[i]); + if (result != 0) { + return result; + } + } + return goog.array.defaultCompare(arr1.length, arr2.length); +}; + + +/** + * Compares its two arguments for order, using the built in < and > + * operators. + * @param {*} a The first object to be compared. + * @param {*} b The second object to be compared. + * @return {number} A negative number, zero, or a positive number as the first + * argument is less than, equal to, or greater than the second. + */ +goog.array.defaultCompare = function(a, b) { + return a > b ? 1 : a < b ? -1 : 0; +}; + + +/** + * Compares its two arguments for equality, using the built in === operator. + * @param {*} a The first object to compare. + * @param {*} b The second object to compare. + * @return {boolean} True if the two arguments are equal, false otherwise. + */ +goog.array.defaultCompareEquality = function(a, b) { + return a === b; +}; + + +/** + * Inserts a value into a sorted array. The array is not modified if the + * value is already present. + * @param {Array.} array The array to modify. + * @param {T} value The object to insert. + * @param {?function(T,T):number=} opt_compareFn Optional comparison function by + * which the + * array is ordered. Should take 2 arguments to compare, and return a + * negative number, zero, or a positive number depending on whether the + * first argument is less than, equal to, or greater than the second. + * @return {boolean} True if an element was inserted. + * @template T + */ +goog.array.binaryInsert = function(array, value, opt_compareFn) { + var index = goog.array.binarySearch(array, value, opt_compareFn); + if (index < 0) { + goog.array.insertAt(array, value, -(index + 1)); + return true; + } + return false; +}; + + +/** + * Removes a value from a sorted array. + * @param {Array} array The array to modify. + * @param {*} value The object to remove. + * @param {Function=} opt_compareFn Optional comparison function by which the + * array is ordered. Should take 2 arguments to compare, and return a + * negative number, zero, or a positive number depending on whether the + * first argument is less than, equal to, or greater than the second. + * @return {boolean} True if an element was removed. + */ +goog.array.binaryRemove = function(array, value, opt_compareFn) { + var index = goog.array.binarySearch(array, value, opt_compareFn); + return (index >= 0) ? goog.array.removeAt(array, index) : false; +}; + + +/** + * Splits an array into disjoint buckets according to a splitting function. + * @param {Array.} array The array. + * @param {function(this:S, T,number,Array.):?} sorter Function to call for + * every element. This takes 3 arguments (the element, the index and the + * array) and must return a valid object key (a string, number, etc), or + * undefined, if that object should not be placed in a bucket. + * @param {S=} opt_obj The object to be used as the value of 'this' within + * sorter. + * @return {!Object} An object, with keys being all of the unique return values + * of sorter, and values being arrays containing the items for + * which the splitter returned that key. + * @template T,S + */ +goog.array.bucket = function(array, sorter, opt_obj) { + var buckets = {}; + + for (var i = 0; i < array.length; i++) { + var value = array[i]; + var key = sorter.call(opt_obj, value, i, array); + if (goog.isDef(key)) { + // Push the value to the right bucket, creating it if necessary. + var bucket = buckets[key] || (buckets[key] = []); + bucket.push(value); + } + } + + return buckets; +}; + + +/** + * Creates a new object built from the provided array and the key-generation + * function. + * @param {Array.|goog.array.ArrayLike} arr Array or array like object over + * which to iterate whose elements will be the values in the new object. + * @param {?function(this:S, T, number, ?) : string} keyFunc The function to + * call for every element. This function takes 3 arguments (the element, the + * index and the array) and should return a string that will be used as the + * key for the element in the new object. If the function returns the same + * key for more than one element, the value for that key is + * implementation-defined. + * @param {S=} opt_obj The object to be used as the value of 'this' + * within keyFunc. + * @return {!Object.} The new object. + * @template T,S + */ +goog.array.toObject = function(arr, keyFunc, opt_obj) { + var ret = {}; + goog.array.forEach(arr, function(element, index) { + ret[keyFunc.call(opt_obj, element, index, arr)] = element; + }); + return ret; +}; + + +/** + * Creates a range of numbers in an arithmetic progression. + * + * Range takes 1, 2, or 3 arguments: + *
    + * range(5) is the same as range(0, 5, 1) and produces [0, 1, 2, 3, 4]
    + * range(2, 5) is the same as range(2, 5, 1) and produces [2, 3, 4]
    + * range(-2, -5, -1) produces [-2, -3, -4]
    + * range(-2, -5, 1) produces [], since stepping by 1 wouldn't ever reach -5.
    + * 
    + * + * @param {number} startOrEnd The starting value of the range if an end argument + * is provided. Otherwise, the start value is 0, and this is the end value. + * @param {number=} opt_end The optional end value of the range. + * @param {number=} opt_step The step size between range values. Defaults to 1 + * if opt_step is undefined or 0. + * @return {!Array.} An array of numbers for the requested range. May be + * an empty array if adding the step would not converge toward the end + * value. + */ +goog.array.range = function(startOrEnd, opt_end, opt_step) { + var array = []; + var start = 0; + var end = startOrEnd; + var step = opt_step || 1; + if (opt_end !== undefined) { + start = startOrEnd; + end = opt_end; + } + + if (step * (end - start) < 0) { + // Sign mismatch: start + step will never reach the end value. + return []; + } + + if (step > 0) { + for (var i = start; i < end; i += step) { + array.push(i); + } + } else { + for (var i = start; i > end; i += step) { + array.push(i); + } + } + return array; +}; + + +/** + * Returns an array consisting of the given value repeated N times. + * + * @param {*} value The value to repeat. + * @param {number} n The repeat count. + * @return {!Array} An array with the repeated value. + */ +goog.array.repeat = function(value, n) { + var array = []; + for (var i = 0; i < n; i++) { + array[i] = value; + } + return array; +}; + + +/** + * Returns an array consisting of every argument with all arrays + * expanded in-place recursively. + * + * @param {...*} var_args The values to flatten. + * @return {!Array} An array containing the flattened values. + */ +goog.array.flatten = function(var_args) { + var result = []; + for (var i = 0; i < arguments.length; i++) { + var element = arguments[i]; + if (goog.isArray(element)) { + result.push.apply(result, goog.array.flatten.apply(null, element)); + } else { + result.push(element); + } + } + return result; +}; + + +/** + * Rotates an array in-place. After calling this method, the element at + * index i will be the element previously at index (i - n) % + * array.length, for all values of i between 0 and array.length - 1, + * inclusive. + * + * For example, suppose list comprises [t, a, n, k, s]. After invoking + * rotate(array, 1) (or rotate(array, -4)), array will comprise [s, t, a, n, k]. + * + * @param {!Array.} array The array to rotate. + * @param {number} n The amount to rotate. + * @return {!Array.} The array. + * @template T + */ +goog.array.rotate = function(array, n) { + goog.asserts.assert(array.length != null); + + if (array.length) { + n %= array.length; + if (n > 0) { + goog.array.ARRAY_PROTOTYPE_.unshift.apply(array, array.splice(-n, n)); + } else if (n < 0) { + goog.array.ARRAY_PROTOTYPE_.push.apply(array, array.splice(0, -n)); + } + } + return array; +}; + + +/** + * Moves one item of an array to a new position keeping the order of the rest + * of the items. Example use case: keeping a list of JavaScript objects + * synchronized with the corresponding list of DOM elements after one of the + * elements has been dragged to a new position. + * @param {!(Array|Arguments|{length:number})} arr The array to modify. + * @param {number} fromIndex Index of the item to move between 0 and + * {@code arr.length - 1}. + * @param {number} toIndex Target index between 0 and {@code arr.length - 1}. + */ +goog.array.moveItem = function(arr, fromIndex, toIndex) { + goog.asserts.assert(fromIndex >= 0 && fromIndex < arr.length); + goog.asserts.assert(toIndex >= 0 && toIndex < arr.length); + // Remove 1 item at fromIndex. + var removedItems = goog.array.ARRAY_PROTOTYPE_.splice.call(arr, fromIndex, 1); + // Insert the removed item at toIndex. + goog.array.ARRAY_PROTOTYPE_.splice.call(arr, toIndex, 0, removedItems[0]); + // We don't use goog.array.insertAt and goog.array.removeAt, because they're + // significantly slower than splice. +}; + + +/** + * Creates a new array for which the element at position i is an array of the + * ith element of the provided arrays. The returned array will only be as long + * as the shortest array provided; additional values are ignored. For example, + * the result of zipping [1, 2] and [3, 4, 5] is [[1,3], [2, 4]]. + * + * This is similar to the zip() function in Python. See {@link + * http://docs.python.org/library/functions.html#zip} + * + * @param {...!goog.array.ArrayLike} var_args Arrays to be combined. + * @return {!Array.} A new array of arrays created from provided arrays. + */ +goog.array.zip = function(var_args) { + if (!arguments.length) { + return []; + } + var result = []; + for (var i = 0; true; i++) { + var value = []; + for (var j = 0; j < arguments.length; j++) { + var arr = arguments[j]; + // If i is larger than the array length, this is the shortest array. + if (i >= arr.length) { + return result; + } + value.push(arr[i]); + } + result.push(value); + } +}; + + +/** + * Shuffles the values in the specified array using the Fisher-Yates in-place + * shuffle (also known as the Knuth Shuffle). By default, calls Math.random() + * and so resets the state of that random number generator. Similarly, may reset + * the state of the any other specified random number generator. + * + * Runtime: O(n) + * + * @param {!Array} arr The array to be shuffled. + * @param {function():number=} opt_randFn Optional random function to use for + * shuffling. + * Takes no arguments, and returns a random number on the interval [0, 1). + * Defaults to Math.random() using JavaScript's built-in Math library. + */ +goog.array.shuffle = function(arr, opt_randFn) { + var randFn = opt_randFn || Math.random; + + for (var i = arr.length - 1; i > 0; i--) { + // Choose a random array index in [0, i] (inclusive with i). + var j = Math.floor(randFn() * (i + 1)); + + var tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } +}; diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/asserts/asserts.js b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/asserts/asserts.js new file mode 100644 index 00000000..c37c8c55 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/asserts/asserts.js @@ -0,0 +1,315 @@ +// Copyright 2008 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Utilities to check the preconditions, postconditions and + * invariants runtime. + * + * Methods in this package should be given special treatment by the compiler + * for type-inference. For example, goog.asserts.assert(foo) + * will restrict foo to a truthy value. + * + * The compiler has an option to disable asserts. So code like: + * + * var x = goog.asserts.assert(foo()); goog.asserts.assert(bar()); + * + * will be transformed into: + * + * var x = foo(); + * + * The compiler will leave in foo() (because its return value is used), + * but it will remove bar() because it assumes it does not have side-effects. + * + */ + +goog.provide('goog.asserts'); +goog.provide('goog.asserts.AssertionError'); + +goog.require('goog.debug.Error'); +goog.require('goog.dom.NodeType'); +goog.require('goog.string'); + + +/** + * @define {boolean} Whether to strip out asserts or to leave them in. + */ +goog.define('goog.asserts.ENABLE_ASSERTS', goog.DEBUG); + + + +/** + * Error object for failed assertions. + * @param {string} messagePattern The pattern that was used to form message. + * @param {!Array.<*>} messageArgs The items to substitute into the pattern. + * @constructor + * @extends {goog.debug.Error} + * @final + */ +goog.asserts.AssertionError = function(messagePattern, messageArgs) { + messageArgs.unshift(messagePattern); + goog.debug.Error.call(this, goog.string.subs.apply(null, messageArgs)); + // Remove the messagePattern afterwards to avoid permenantly modifying the + // passed in array. + messageArgs.shift(); + + /** + * The message pattern used to format the error message. Error handlers can + * use this to uniquely identify the assertion. + * @type {string} + */ + this.messagePattern = messagePattern; +}; +goog.inherits(goog.asserts.AssertionError, goog.debug.Error); + + +/** @override */ +goog.asserts.AssertionError.prototype.name = 'AssertionError'; + + +/** + * Throws an exception with the given message and "Assertion failed" prefixed + * onto it. + * @param {string} defaultMessage The message to use if givenMessage is empty. + * @param {Array.<*>} defaultArgs The substitution arguments for defaultMessage. + * @param {string|undefined} givenMessage Message supplied by the caller. + * @param {Array.<*>} givenArgs The substitution arguments for givenMessage. + * @throws {goog.asserts.AssertionError} When the value is not a number. + * @private + */ +goog.asserts.doAssertFailure_ = + function(defaultMessage, defaultArgs, givenMessage, givenArgs) { + var message = 'Assertion failed'; + if (givenMessage) { + message += ': ' + givenMessage; + var args = givenArgs; + } else if (defaultMessage) { + message += ': ' + defaultMessage; + args = defaultArgs; + } + // The '' + works around an Opera 10 bug in the unit tests. Without it, + // a stack trace is added to var message above. With this, a stack trace is + // not added until this line (it causes the extra garbage to be added after + // the assertion message instead of in the middle of it). + throw new goog.asserts.AssertionError('' + message, args || []); +}; + + +/** + * Checks if the condition evaluates to true if goog.asserts.ENABLE_ASSERTS is + * true. + * @param {*} condition The condition to check. + * @param {string=} opt_message Error message in case of failure. + * @param {...*} var_args The items to substitute into the failure message. + * @return {*} The value of the condition. + * @throws {goog.asserts.AssertionError} When the condition evaluates to false. + */ +goog.asserts.assert = function(condition, opt_message, var_args) { + if (goog.asserts.ENABLE_ASSERTS && !condition) { + goog.asserts.doAssertFailure_('', null, opt_message, + Array.prototype.slice.call(arguments, 2)); + } + return condition; +}; + + +/** + * Fails if goog.asserts.ENABLE_ASSERTS is true. This function is useful in case + * when we want to add a check in the unreachable area like switch-case + * statement: + * + *
    + *  switch(type) {
    + *    case FOO: doSomething(); break;
    + *    case BAR: doSomethingElse(); break;
    + *    default: goog.assert.fail('Unrecognized type: ' + type);
    + *      // We have only 2 types - "default:" section is unreachable code.
    + *  }
    + * 
    + * + * @param {string=} opt_message Error message in case of failure. + * @param {...*} var_args The items to substitute into the failure message. + * @throws {goog.asserts.AssertionError} Failure. + */ +goog.asserts.fail = function(opt_message, var_args) { + if (goog.asserts.ENABLE_ASSERTS) { + throw new goog.asserts.AssertionError( + 'Failure' + (opt_message ? ': ' + opt_message : ''), + Array.prototype.slice.call(arguments, 1)); + } +}; + + +/** + * Checks if the value is a number if goog.asserts.ENABLE_ASSERTS is true. + * @param {*} value The value to check. + * @param {string=} opt_message Error message in case of failure. + * @param {...*} var_args The items to substitute into the failure message. + * @return {number} The value, guaranteed to be a number when asserts enabled. + * @throws {goog.asserts.AssertionError} When the value is not a number. + */ +goog.asserts.assertNumber = function(value, opt_message, var_args) { + if (goog.asserts.ENABLE_ASSERTS && !goog.isNumber(value)) { + goog.asserts.doAssertFailure_('Expected number but got %s: %s.', + [goog.typeOf(value), value], opt_message, + Array.prototype.slice.call(arguments, 2)); + } + return /** @type {number} */ (value); +}; + + +/** + * Checks if the value is a string if goog.asserts.ENABLE_ASSERTS is true. + * @param {*} value The value to check. + * @param {string=} opt_message Error message in case of failure. + * @param {...*} var_args The items to substitute into the failure message. + * @return {string} The value, guaranteed to be a string when asserts enabled. + * @throws {goog.asserts.AssertionError} When the value is not a string. + */ +goog.asserts.assertString = function(value, opt_message, var_args) { + if (goog.asserts.ENABLE_ASSERTS && !goog.isString(value)) { + goog.asserts.doAssertFailure_('Expected string but got %s: %s.', + [goog.typeOf(value), value], opt_message, + Array.prototype.slice.call(arguments, 2)); + } + return /** @type {string} */ (value); +}; + + +/** + * Checks if the value is a function if goog.asserts.ENABLE_ASSERTS is true. + * @param {*} value The value to check. + * @param {string=} opt_message Error message in case of failure. + * @param {...*} var_args The items to substitute into the failure message. + * @return {!Function} The value, guaranteed to be a function when asserts + * enabled. + * @throws {goog.asserts.AssertionError} When the value is not a function. + */ +goog.asserts.assertFunction = function(value, opt_message, var_args) { + if (goog.asserts.ENABLE_ASSERTS && !goog.isFunction(value)) { + goog.asserts.doAssertFailure_('Expected function but got %s: %s.', + [goog.typeOf(value), value], opt_message, + Array.prototype.slice.call(arguments, 2)); + } + return /** @type {!Function} */ (value); +}; + + +/** + * Checks if the value is an Object if goog.asserts.ENABLE_ASSERTS is true. + * @param {*} value The value to check. + * @param {string=} opt_message Error message in case of failure. + * @param {...*} var_args The items to substitute into the failure message. + * @return {!Object} The value, guaranteed to be a non-null object. + * @throws {goog.asserts.AssertionError} When the value is not an object. + */ +goog.asserts.assertObject = function(value, opt_message, var_args) { + if (goog.asserts.ENABLE_ASSERTS && !goog.isObject(value)) { + goog.asserts.doAssertFailure_('Expected object but got %s: %s.', + [goog.typeOf(value), value], + opt_message, Array.prototype.slice.call(arguments, 2)); + } + return /** @type {!Object} */ (value); +}; + + +/** + * Checks if the value is an Array if goog.asserts.ENABLE_ASSERTS is true. + * @param {*} value The value to check. + * @param {string=} opt_message Error message in case of failure. + * @param {...*} var_args The items to substitute into the failure message. + * @return {!Array} The value, guaranteed to be a non-null array. + * @throws {goog.asserts.AssertionError} When the value is not an array. + */ +goog.asserts.assertArray = function(value, opt_message, var_args) { + if (goog.asserts.ENABLE_ASSERTS && !goog.isArray(value)) { + goog.asserts.doAssertFailure_('Expected array but got %s: %s.', + [goog.typeOf(value), value], opt_message, + Array.prototype.slice.call(arguments, 2)); + } + return /** @type {!Array} */ (value); +}; + + +/** + * Checks if the value is a boolean if goog.asserts.ENABLE_ASSERTS is true. + * @param {*} value The value to check. + * @param {string=} opt_message Error message in case of failure. + * @param {...*} var_args The items to substitute into the failure message. + * @return {boolean} The value, guaranteed to be a boolean when asserts are + * enabled. + * @throws {goog.asserts.AssertionError} When the value is not a boolean. + */ +goog.asserts.assertBoolean = function(value, opt_message, var_args) { + if (goog.asserts.ENABLE_ASSERTS && !goog.isBoolean(value)) { + goog.asserts.doAssertFailure_('Expected boolean but got %s: %s.', + [goog.typeOf(value), value], opt_message, + Array.prototype.slice.call(arguments, 2)); + } + return /** @type {boolean} */ (value); +}; + + +/** + * Checks if the value is a DOM Element if goog.asserts.ENABLE_ASSERTS is true. + * @param {*} value The value to check. + * @param {string=} opt_message Error message in case of failure. + * @param {...*} var_args The items to substitute into the failure message. + * @return {!Element} The value, likely to be a DOM Element when asserts are + * enabled. + * @throws {goog.asserts.AssertionError} When the value is not a boolean. + */ +goog.asserts.assertElement = function(value, opt_message, var_args) { + if (goog.asserts.ENABLE_ASSERTS && (!goog.isObject(value) || + value.nodeType != goog.dom.NodeType.ELEMENT)) { + goog.asserts.doAssertFailure_('Expected Element but got %s: %s.', + [goog.typeOf(value), value], opt_message, + Array.prototype.slice.call(arguments, 2)); + } + return /** @type {!Element} */ (value); +}; + + +/** + * Checks if the value is an instance of the user-defined type if + * goog.asserts.ENABLE_ASSERTS is true. + * + * The compiler may tighten the type returned by this function. + * + * @param {*} value The value to check. + * @param {function(new: T, ...)} type A user-defined constructor. + * @param {string=} opt_message Error message in case of failure. + * @param {...*} var_args The items to substitute into the failure message. + * @throws {goog.asserts.AssertionError} When the value is not an instance of + * type. + * @return {!T} + * @template T + */ +goog.asserts.assertInstanceof = function(value, type, opt_message, var_args) { + if (goog.asserts.ENABLE_ASSERTS && !(value instanceof type)) { + goog.asserts.doAssertFailure_('instanceof check failed.', null, + opt_message, Array.prototype.slice.call(arguments, 3)); + } + return value; +}; + + +/** + * Checks that no enumerable keys are present in Object.prototype. Such keys + * would break most code that use {@code for (var ... in ...)} loops. + */ +goog.asserts.assertObjectPrototypeIsIntact = function() { + for (var key in Object.prototype) { + goog.asserts.fail(key + ' should not be enumerable in Object.prototype.'); + } +}; diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/async/nexttick.js b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/async/nexttick.js new file mode 100644 index 00000000..47b2c442 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/async/nexttick.js @@ -0,0 +1,176 @@ +// Copyright 2013 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Provides a function to schedule running a function as soon + * as possible after the current JS execution stops and yields to the event + * loop. + * + */ + +goog.provide('goog.async.nextTick'); + +goog.require('goog.debug.entryPointRegistry'); +goog.require('goog.functions'); + + +/** + * Fires the provided callbacks as soon as possible after the current JS + * execution context. setTimeout(…, 0) always takes at least 5ms for legacy + * reasons. + * @param {function(this:SCOPE)} callback Callback function to fire as soon as + * possible. + * @param {SCOPE=} opt_context Object in whose scope to call the listener. + * @template SCOPE + */ +goog.async.nextTick = function(callback, opt_context) { + var cb = callback; + if (opt_context) { + cb = goog.bind(callback, opt_context); + } + cb = goog.async.nextTick.wrapCallback_(cb); + // Introduced and currently only supported by IE10. + if (goog.isFunction(goog.global.setImmediate)) { + goog.global.setImmediate(cb); + return; + } + // Look for and cache the custom fallback version of setImmediate. + if (!goog.async.nextTick.setImmediate_) { + goog.async.nextTick.setImmediate_ = + goog.async.nextTick.getSetImmediateEmulator_(); + } + goog.async.nextTick.setImmediate_(cb); +}; + + +/** + * Cache for the setImmediate implementation. + * @type {function(function())} + * @private + */ +goog.async.nextTick.setImmediate_; + + +/** + * Determines the best possible implementation to run a function as soon as + * the JS event loop is idle. + * @return {function(function())} The "setImmediate" implementation. + * @private + */ +goog.async.nextTick.getSetImmediateEmulator_ = function() { + // Create a private message channel and use it to postMessage empty messages + // to ourselves. + var Channel = goog.global['MessageChannel']; + // If MessageChannel is not available and we are in a browser, implement + // an iframe based polyfill in browsers that have postMessage and + // document.addEventListener. The latter excludes IE8 because it has a + // synchronous postMessage implementation. + if (typeof Channel === 'undefined' && typeof window !== 'undefined' && + window.postMessage && window.addEventListener) { + /** @constructor */ + Channel = function() { + // Make an empty, invisible iframe. + var iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.src = ''; + document.documentElement.appendChild(iframe); + var win = iframe.contentWindow; + var doc = win.document; + doc.open(); + doc.write(''); + doc.close(); + var message = 'callImmediate' + Math.random(); + var origin = win.location.protocol + '//' + win.location.host; + var onmessage = goog.bind(function(e) { + // Validate origin and message to make sure that this message was + // intended for us. + if (e.origin != origin && e.data != message) { + return; + } + this['port1'].onmessage(); + }, this); + win.addEventListener('message', onmessage, false); + this['port1'] = {}; + this['port2'] = { + postMessage: function() { + win.postMessage(message, origin); + } + }; + }; + } + if (typeof Channel !== 'undefined') { + var channel = new Channel(); + // Use a fifo linked list to call callbacks in the right order. + var head = {}; + var tail = head; + channel['port1'].onmessage = function() { + head = head.next; + var cb = head.cb; + head.cb = null; + cb(); + }; + return function(cb) { + tail.next = { + cb: cb + }; + tail = tail.next; + channel['port2'].postMessage(0); + }; + } + // Implementation for IE6-8: Script elements fire an asynchronous + // onreadystatechange event when inserted into the DOM. + if (typeof document !== 'undefined' && 'onreadystatechange' in + document.createElement('script')) { + return function(cb) { + var script = document.createElement('script'); + script.onreadystatechange = function() { + // Clean up and call the callback. + script.onreadystatechange = null; + script.parentNode.removeChild(script); + script = null; + cb(); + cb = null; + }; + document.documentElement.appendChild(script); + }; + } + // Fall back to setTimeout with 0. In browsers this creates a delay of 5ms + // or more. + return function(cb) { + goog.global.setTimeout(cb, 0); + }; +}; + + +/** + * Helper function that is overrided to protect callbacks with entry point + * monitor if the application monitors entry points. + * @param {function()} callback Callback function to fire as soon as possible. + * @return {function()} The wrapped callback. + * @private + */ +goog.async.nextTick.wrapCallback_ = goog.functions.identity; + + +// Register the callback function as an entry point, so that it can be +// monitored for exception handling, etc. This has to be done in this file +// since it requires special code to handle all browsers. +goog.debug.entryPointRegistry.register( + /** + * @param {function(!Function): !Function} transformer The transforming + * function. + */ + function(transformer) { + goog.async.nextTick.wrapCallback_ = transformer; + }); diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/async/run.js b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/async/run.js new file mode 100644 index 00000000..d152c3ef --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/async/run.js @@ -0,0 +1,118 @@ +// Copyright 2013 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +goog.provide('goog.async.run'); +goog.provide('goog.async.throwException'); + +goog.require('goog.async.nextTick'); +goog.require('goog.testing.watchers'); + + +/** + * Throw an item without interrupting the current execution context. For + * example, if processing a group of items in a loop, sometimes it is useful + * to report an error while still allowing the rest of the batch to be + * processed. + * @param {*} exception + */ +goog.async.throwException = function(exception) { + // Each throw needs to be in its own context. + goog.async.nextTick(function() { throw exception; }); +}; + + +/** + * Fires the provided callback just before the current callstack unwinds, or as + * soon as possible after the current JS execution context. + * @param {function(this:THIS)} callback + * @param {THIS=} opt_context Object to use as the "this value" when calling + * the provided function. + * @template THIS + */ +goog.async.run = function(callback, opt_context) { + if (!goog.async.run.workQueueScheduled_) { + // Nothing is currently scheduled, schedule it now. + goog.async.nextTick(goog.async.run.processWorkQueue); + goog.async.run.workQueueScheduled_ = true; + } + + goog.async.run.workQueue_.push( + new goog.async.run.WorkItem_(callback, opt_context)); +}; + + +/** @private {boolean} */ +goog.async.run.workQueueScheduled_ = false; + + +/** @private {!Array.} */ +goog.async.run.workQueue_ = []; + + +if (goog.DEBUG) { + /** + * Reset the event queue. + * @private + */ + goog.async.run.resetQueue_ = function() { + goog.async.run.workQueueScheduled_ = false; + goog.async.run.workQueue_ = []; + }; + + // If there is a clock implemenation in use for testing + // and it is reset, reset the queue. + goog.testing.watchers.watchClockReset(goog.async.run.resetQueue_); +} + + +/** + * Run any pending goog.async.run work items. This function is not intended + * for general use, but for use by entry point handlers to run items ahead of + * goog.async.nextTick. + */ +goog.async.run.processWorkQueue = function() { + // NOTE: additional work queue items may be pushed while processing. + while (goog.async.run.workQueue_.length) { + // Don't let the work queue grow indefinitely. + var workItems = goog.async.run.workQueue_; + goog.async.run.workQueue_ = []; + for (var i = 0; i < workItems.length; i++) { + var workItem = workItems[i]; + try { + workItem.fn.call(workItem.scope); + } catch (e) { + goog.async.throwException(e); + } + } + } + + // There are no more work items, reset the work queue. + goog.async.run.workQueueScheduled_ = false; +}; + + + +/** + * @constructor + * @final + * @struct + * @private + * + * @param {function()} fn + * @param {Object|null|undefined} scope + */ +goog.async.run.WorkItem_ = function(fn, scope) { + /** @const */ this.fn = fn; + /** @const */ this.scope = scope; +}; diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/async/throttle.js b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/async/throttle.js new file mode 100644 index 00000000..346a5b68 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/async/throttle.js @@ -0,0 +1,191 @@ +// Copyright 2007 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Definition of the goog.async.Throttle class. + * + * @see ../demos/timers.html + */ + +goog.provide('goog.Throttle'); +goog.provide('goog.async.Throttle'); + +goog.require('goog.Disposable'); +goog.require('goog.Timer'); + + + +/** + * Throttle will perform an action that is passed in no more than once + * per interval (specified in milliseconds). If it gets multiple signals + * to perform the action while it is waiting, it will only perform the action + * once at the end of the interval. + * @param {Function} listener Function to callback when the action is triggered. + * @param {number} interval Interval over which to throttle. The handler can + * only be called once per interval. + * @param {Object=} opt_handler Object in whose scope to call the listener. + * @constructor + * @extends {goog.Disposable} + * @final + */ +goog.async.Throttle = function(listener, interval, opt_handler) { + goog.Disposable.call(this); + + /** + * Function to callback + * @type {Function} + * @private + */ + this.listener_ = listener; + + /** + * Interval for the throttle time + * @type {number} + * @private + */ + this.interval_ = interval; + + /** + * "this" context for the listener + * @type {Object|undefined} + * @private + */ + this.handler_ = opt_handler; + + /** + * Cached callback function invoked after the throttle timeout completes + * @type {Function} + * @private + */ + this.callback_ = goog.bind(this.onTimer_, this); +}; +goog.inherits(goog.async.Throttle, goog.Disposable); + + + +/** + * A deprecated alias. + * @deprecated Use goog.async.Throttle instead. + * @constructor + * @final + */ +goog.Throttle = goog.async.Throttle; + + +/** + * Indicates that the action is pending and needs to be fired. + * @type {boolean} + * @private + */ +goog.async.Throttle.prototype.shouldFire_ = false; + + +/** + * Indicates the count of nested pauses currently in effect on the throttle. + * When this count is not zero, fired actions will be postponed until the + * throttle is resumed enough times to drop the pause count to zero. + * @type {number} + * @private + */ +goog.async.Throttle.prototype.pauseCount_ = 0; + + +/** + * Timer for scheduling the next callback + * @type {?number} + * @private + */ +goog.async.Throttle.prototype.timer_ = null; + + +/** + * Notifies the throttle that the action has happened. It will throttle the call + * so that the callback is not called too often according to the interval + * parameter passed to the constructor. + */ +goog.async.Throttle.prototype.fire = function() { + if (!this.timer_ && !this.pauseCount_) { + this.doAction_(); + } else { + this.shouldFire_ = true; + } +}; + + +/** + * Cancels any pending action callback. The throttle can be restarted by + * calling {@link #fire}. + */ +goog.async.Throttle.prototype.stop = function() { + if (this.timer_) { + goog.Timer.clear(this.timer_); + this.timer_ = null; + this.shouldFire_ = false; + } +}; + + +/** + * Pauses the throttle. All pending and future action callbacks will be + * delayed until the throttle is resumed. Pauses can be nested. + */ +goog.async.Throttle.prototype.pause = function() { + this.pauseCount_++; +}; + + +/** + * Resumes the throttle. If doing so drops the pausing count to zero, pending + * action callbacks will be executed as soon as possible, but still no sooner + * than an interval's delay after the previous call. Future action callbacks + * will be executed as normal. + */ +goog.async.Throttle.prototype.resume = function() { + this.pauseCount_--; + if (!this.pauseCount_ && this.shouldFire_ && !this.timer_) { + this.shouldFire_ = false; + this.doAction_(); + } +}; + + +/** @override */ +goog.async.Throttle.prototype.disposeInternal = function() { + goog.async.Throttle.superClass_.disposeInternal.call(this); + this.stop(); +}; + + +/** + * Handler for the timer to fire the throttle + * @private + */ +goog.async.Throttle.prototype.onTimer_ = function() { + this.timer_ = null; + + if (this.shouldFire_ && !this.pauseCount_) { + this.shouldFire_ = false; + this.doAction_(); + } +}; + + +/** + * Calls the callback + * @private + */ +goog.async.Throttle.prototype.doAction_ = function() { + this.timer_ = goog.Timer.callOnce(this.callback_, this.interval_); + this.listener_.call(this.handler_); +}; diff --git a/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/base.js b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/base.js new file mode 100644 index 00000000..66674e65 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/closure/lib/closure/goog/base.js @@ -0,0 +1,1631 @@ +// Copyright 2006 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Bootstrap for the Google JS Library (Closure). + * + * In uncompiled mode base.js will write out Closure's deps file, unless the + * global CLOSURE_NO_DEPS is set to true. This allows projects to + * include their own deps file(s) from different locations. + * + * + * @provideGoog + */ + + +/** + * @define {boolean} Overridden to true by the compiler when --closure_pass + * or --mark_as_compiled is specified. + */ +var COMPILED = false; + + +/** + * Base namespace for the Closure library. Checks to see goog is already + * defined in the current scope before assigning to prevent clobbering if + * base.js is loaded more than once. + * + * @const + */ +var goog = goog || {}; + + +/** + * Reference to the global context. In most cases this will be 'window'. + */ +goog.global = this; + + +/** + * A hook for overriding the define values in uncompiled mode. + * + * In uncompiled mode, {@code CLOSURE_DEFINES} may be defined before loading + * base.js. If a key is defined in {@code CLOSURE_DEFINES}, {@code goog.define} + * will use the value instead of the default value. This allows flags to be + * overwritten without compilation (this is normally accomplished with the + * compiler's "define" flag). + * + * Example: + *
    + *   var CLOSURE_DEFINES = {'goog.DEBUG': false};
    + * 
    + * + * @type {Object.|undefined} + */ +goog.global.CLOSURE_DEFINES; + + +/** + * Builds an object structure for the provided namespace path, ensuring that + * names that already exist are not overwritten. For example: + * "a.b.c" -> a = {};a.b={};a.b.c={}; + * Used by goog.provide and goog.exportSymbol. + * @param {string} name name of the object that this file defines. + * @param {*=} opt_object the object to expose at the end of the path. + * @param {Object=} opt_objectToExportTo The object to add the path to; default + * is |goog.global|. + * @private + */ +goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) { + var parts = name.split('.'); + var cur = opt_objectToExportTo || goog.global; + + // Internet Explorer exhibits strange behavior when throwing errors from + // methods externed in this manner. See the testExportSymbolExceptions in + // base_test.html for an example. + if (!(parts[0] in cur) && cur.execScript) { + cur.execScript('var ' + parts[0]); + } + + // Certain browsers cannot parse code in the form for((a in b); c;); + // This pattern is produced by the JSCompiler when it collapses the + // statement above into the conditional loop below. To prevent this from + // happening, use a for-loop and reserve the init logic as below. + + // Parentheses added to eliminate strict JS warning in Firefox. + for (var part; parts.length && (part = parts.shift());) { + if (!parts.length && opt_object !== undefined) { + // last part and we have an object; use it + cur[part] = opt_object; + } else if (cur[part]) { + cur = cur[part]; + } else { + cur = cur[part] = {}; + } + } +}; + + +/** + * Defines a named value. In uncompiled mode, the value is retreived from + * CLOSURE_DEFINES if the object is defined and has the property specified, + * and otherwise used the defined defaultValue. When compiled, the default + * can be overridden using compiler command-line options. + * + * @param {string} name The distinguished name to provide. + * @param {string|number|boolean} defaultValue + */ +goog.define = function(name, defaultValue) { + var value = defaultValue; + if (!COMPILED) { + if (goog.global.CLOSURE_DEFINES && Object.prototype.hasOwnProperty.call( + goog.global.CLOSURE_DEFINES, name)) { + value = goog.global.CLOSURE_DEFINES[name]; + } + } + goog.exportPath_(name, value); +}; + + +/** + * @define {boolean} DEBUG is provided as a convenience so that debugging code + * that should not be included in a production js_binary can be easily stripped + * by specifying --define goog.DEBUG=false to the JSCompiler. For example, most + * toString() methods should be declared inside an "if (goog.DEBUG)" conditional + * because they are generally used for debugging purposes and it is difficult + * for the JSCompiler to statically determine whether they are used. + */ +goog.DEBUG = true; + + +/** + * @define {string} LOCALE defines the locale being used for compilation. It is + * used to select locale specific data to be compiled in js binary. BUILD rule + * can specify this value by "--define goog.LOCALE=" as JSCompiler + * option. + * + * Take into account that the locale code format is important. You should use + * the canonical Unicode format with hyphen as a delimiter. Language must be + * lowercase, Language Script - Capitalized, Region - UPPERCASE. + * There are few examples: pt-BR, en, en-US, sr-Latin-BO, zh-Hans-CN. + * + * See more info about locale codes here: + * http://www.unicode.org/reports/tr35/#Unicode_Language_and_Locale_Identifiers + * + * For language codes you should use values defined by ISO 693-1. See it here + * http://www.w3.org/WAI/ER/IG/ert/iso639.htm. There is only one exception from + * this rule: the Hebrew language. For legacy reasons the old code (iw) should + * be used instead of the new code (he), see http://wiki/Main/IIISynonyms. + */ +goog.define('goog.LOCALE', 'en'); // default to en + + +/** + * @define {boolean} Whether this code is running on trusted sites. + * + * On untrusted sites, several native functions can be defined or overridden by + * external libraries like Prototype, Datejs, and JQuery and setting this flag + * to false forces closure to use its own implementations when possible. + * + * If your JavaScript can be loaded by a third party site and you are wary about + * relying on non-standard implementations, specify + * "--define goog.TRUSTED_SITE=false" to the JSCompiler. + */ +goog.define('goog.TRUSTED_SITE', true); + + +/** + * Creates object stubs for a namespace. The presence of one or more + * goog.provide() calls indicate that the file defines the given + * objects/namespaces. Build tools also scan for provide/require statements + * to discern dependencies, build dependency files (see deps.js), etc. + * @see goog.require + * @param {string} name Namespace provided by this file in the form + * "goog.package.part". + */ +goog.provide = function(name) { + if (!COMPILED) { + // Ensure that the same namespace isn't provided twice. This is intended + // to teach new developers that 'goog.provide' is effectively a variable + // declaration. And when JSCompiler transforms goog.provide into a real + // variable declaration, the compiled JS should work the same as the raw + // JS--even when the raw JS uses goog.provide incorrectly. + if (goog.isProvided_(name)) { + throw Error('Namespace "' + name + '" already declared.'); + } + delete goog.implicitNamespaces_[name]; + + var namespace = name; + while ((namespace = namespace.substring(0, namespace.lastIndexOf('.')))) { + if (goog.getObjectByName(namespace)) { + break; + } + goog.implicitNamespaces_[namespace] = true; + } + } + + goog.exportPath_(name); +}; + + +/** + * Marks that the current file should only be used for testing, and never for + * live code in production. + * + * In the case of unit tests, the message may optionally be an exact namespace + * for the test (e.g. 'goog.stringTest'). The linter will then ignore the extra + * provide (if not explicitly defined in the code). + * + * @param {string=} opt_message Optional message to add to the error that's + * raised when used in production code. + */ +goog.setTestOnly = function(opt_message) { + if (COMPILED && !goog.DEBUG) { + opt_message = opt_message || ''; + throw Error('Importing test-only code into non-debug environment' + + opt_message ? ': ' + opt_message : '.'); + } +}; + + +if (!COMPILED) { + + /** + * Check if the given name has been goog.provided. This will return false for + * names that are available only as implicit namespaces. + * @param {string} name name of the object to look for. + * @return {boolean} Whether the name has been provided. + * @private + */ + goog.isProvided_ = function(name) { + return !goog.implicitNamespaces_[name] && !!goog.getObjectByName(name); + }; + + /** + * Namespaces implicitly defined by goog.provide. For example, + * goog.provide('goog.events.Event') implicitly declares that 'goog' and + * 'goog.events' must be namespaces. + * + * @type {Object} + * @private + */ + goog.implicitNamespaces_ = {}; +} + + +/** + * Returns an object based on its fully qualified external name. If you are + * using a compilation pass that renames property names beware that using this + * function will not find renamed properties. + * + * @param {string} name The fully qualified name. + * @param {Object=} opt_obj The object within which to look; default is + * |goog.global|. + * @return {?} The value (object or primitive) or, if not found, null. + */ +goog.getObjectByName = function(name, opt_obj) { + var parts = name.split('.'); + var cur = opt_obj || goog.global; + for (var part; part = parts.shift(); ) { + if (goog.isDefAndNotNull(cur[part])) { + cur = cur[part]; + } else { + return null; + } + } + return cur; +}; + + +/** + * Globalizes a whole namespace, such as goog or goog.lang. + * + * @param {Object} obj The namespace to globalize. + * @param {Object=} opt_global The object to add the properties to. + * @deprecated Properties may be explicitly exported to the global scope, but + * this should no longer be done in bulk. + */ +goog.globalize = function(obj, opt_global) { + var global = opt_global || goog.global; + for (var x in obj) { + global[x] = obj[x]; + } +}; + + +/** + * Adds a dependency from a file to the files it requires. + * @param {string} relPath The path to the js file. + * @param {Array} provides An array of strings with the names of the objects + * this file provides. + * @param {Array} requires An array of strings with the names of the objects + * this file requires. + */ +goog.addDependency = function(relPath, provides, requires) { + if (goog.DEPENDENCIES_ENABLED) { + var provide, require; + var path = relPath.replace(/\\/g, '/'); + var deps = goog.dependencies_; + for (var i = 0; provide = provides[i]; i++) { + deps.nameToPath[provide] = path; + if (!(path in deps.pathToNames)) { + deps.pathToNames[path] = {}; + } + deps.pathToNames[path][provide] = true; + } + for (var j = 0; require = requires[j]; j++) { + if (!(path in deps.requires)) { + deps.requires[path] = {}; + } + deps.requires[path][require] = true; + } + } +}; + + + + +// NOTE(nnaze): The debug DOM loader was included in base.js as an original way +// to do "debug-mode" development. The dependency system can sometimes be +// confusing, as can the debug DOM loader's asynchronous nature. +// +// With the DOM loader, a call to goog.require() is not blocking -- the script +// will not load until some point after the current script. If a namespace is +// needed at runtime, it needs to be defined in a previous script, or loaded via +// require() with its registered dependencies. +// User-defined namespaces may need their own deps file. See http://go/js_deps, +// http://go/genjsdeps, or, externally, DepsWriter. +// http://code.google.com/closure/library/docs/depswriter.html +// +// Because of legacy clients, the DOM loader can't be easily removed from +// base.js. Work is being done to make it disableable or replaceable for +// different environments (DOM-less JavaScript interpreters like Rhino or V8, +// for example). See bootstrap/ for more information. + + +/** + * @define {boolean} Whether to enable the debug loader. + * + * If enabled, a call to goog.require() will attempt to load the namespace by + * appending a script tag to the DOM (if the namespace has been registered). + * + * If disabled, goog.require() will simply assert that the namespace has been + * provided (and depend on the fact that some outside tool correctly ordered + * the script). + */ +goog.define('goog.ENABLE_DEBUG_LOADER', true); + + +/** + * Implements a system for the dynamic resolution of dependencies that works in + * parallel with the BUILD system. Note that all calls to goog.require will be + * stripped by the JSCompiler when the --closure_pass option is used. + * @see goog.provide + * @param {string} name Namespace to include (as was given in goog.provide()) in + * the form "goog.package.part". + */ +goog.require = function(name) { + + // If the object already exists we do not need do do anything. + // TODO(arv): If we start to support require based on file name this has to + // change. + // TODO(arv): If we allow goog.foo.* this has to change. + // TODO(arv): If we implement dynamic load after page load we should probably + // not remove this code for the compiled output. + if (!COMPILED) { + if (goog.isProvided_(name)) { + return; + } + + if (goog.ENABLE_DEBUG_LOADER) { + var path = goog.getPathFromDeps_(name); + if (path) { + goog.included_[path] = true; + goog.writeScripts_(); + return; + } + } + + var errorMessage = 'goog.require could not find: ' + name; + if (goog.global.console) { + goog.global.console['error'](errorMessage); + } + + + throw Error(errorMessage); + + } +}; + + +/** + * Path for included scripts. + * @type {string} + */ +goog.basePath = ''; + + +/** + * A hook for overriding the base path. + * @type {string|undefined} + */ +goog.global.CLOSURE_BASE_PATH; + + +/** + * Whether to write out Closure's deps file. By default, the deps are written. + * @type {boolean|undefined} + */ +goog.global.CLOSURE_NO_DEPS; + + +/** + * A function to import a single script. This is meant to be overridden when + * Closure is being run in non-HTML contexts, such as web workers. It's defined + * in the global scope so that it can be set before base.js is loaded, which + * allows deps.js to be imported properly. + * + * The function is passed the script source, which is a relative URI. It should + * return true if the script was imported, false otherwise. + */ +goog.global.CLOSURE_IMPORT_SCRIPT; + + +/** + * Null function used for default values of callbacks, etc. + * @return {void} Nothing. + */ +goog.nullFunction = function() {}; + + +/** + * The identity function. Returns its first argument. + * + * @param {*=} opt_returnValue The single value that will be returned. + * @param {...*} var_args Optional trailing arguments. These are ignored. + * @return {?} The first argument. We can't know the type -- just pass it along + * without type. + * @deprecated Use goog.functions.identity instead. + */ +goog.identityFunction = function(opt_returnValue, var_args) { + return opt_returnValue; +}; + + +/** + * When defining a class Foo with an abstract method bar(), you can do: + * Foo.prototype.bar = goog.abstractMethod + * + * Now if a subclass of Foo fails to override bar(), an error will be thrown + * when bar() is invoked. + * + * Note: This does not take the name of the function to override as an argument + * because that would make it more difficult to obfuscate our JavaScript code. + * + * @type {!Function} + * @throws {Error} when invoked to indicate the method should be overridden. + */ +goog.abstractMethod = function() { + throw Error('unimplemented abstract method'); +}; + + +/** + * Adds a {@code getInstance} static method that always returns the same + * instance object. + * @param {!Function} ctor The constructor for the class to add the static + * method to. + */ +goog.addSingletonGetter = function(ctor) { + ctor.getInstance = function() { + if (ctor.instance_) { + return ctor.instance_; + } + if (goog.DEBUG) { + // NOTE: JSCompiler can't optimize away Array#push. + goog.instantiatedSingletons_[goog.instantiatedSingletons_.length] = ctor; + } + return ctor.instance_ = new ctor; + }; +}; + + +/** + * All singleton classes that have been instantiated, for testing. Don't read + * it directly, use the {@code goog.testing.singleton} module. The compiler + * removes this variable if unused. + * @type {!Array.} + * @private + */ +goog.instantiatedSingletons_ = []; + + +/** + * True if goog.dependencies_ is available. + * @const {boolean} + */ +goog.DEPENDENCIES_ENABLED = !COMPILED && goog.ENABLE_DEBUG_LOADER; + + +if (goog.DEPENDENCIES_ENABLED) { + /** + * Object used to keep track of urls that have already been added. This record + * allows the prevention of circular dependencies. + * @type {Object} + * @private + */ + goog.included_ = {}; + + + /** + * This object is used to keep track of dependencies and other data that is + * used for loading scripts. + * @private + * @type {Object} + */ + goog.dependencies_ = { + pathToNames: {}, // 1 to many + nameToPath: {}, // 1 to 1 + requires: {}, // 1 to many + // Used when resolving dependencies to prevent us from visiting file twice. + visited: {}, + written: {} // Used to keep track of script files we have written. + }; + + + /** + * Tries to detect whether is in the context of an HTML document. + * @return {boolean} True if it looks like HTML document. + * @private + */ + goog.inHtmlDocument_ = function() { + var doc = goog.global.document; + return typeof doc != 'undefined' && + 'write' in doc; // XULDocument misses write. + }; + + + /** + * Tries to detect the base path of base.js script that bootstraps Closure. + * @private + */ + goog.findBasePath_ = function() { + if (goog.global.CLOSURE_BASE_PATH) { + goog.basePath = goog.global.CLOSURE_BASE_PATH; + return; + } else if (!goog.inHtmlDocument_()) { + return; + } + var doc = goog.global.document; + var scripts = doc.getElementsByTagName('script'); + // Search backwards since the current script is in almost all cases the one + // that has base.js. + for (var i = scripts.length - 1; i >= 0; --i) { + var src = scripts[i].src; + var qmark = src.lastIndexOf('?'); + var l = qmark == -1 ? src.length : qmark; + if (src.substr(l - 7, 7) == 'base.js') { + goog.basePath = src.substr(0, l - 7); + return; + } + } + }; + + + /** + * Imports a script if, and only if, that script hasn't already been imported. + * (Must be called at execution time) + * @param {string} src Script source. + * @private + */ + goog.importScript_ = function(src) { + var importScript = goog.global.CLOSURE_IMPORT_SCRIPT || + goog.writeScriptTag_; + if (!goog.dependencies_.written[src] && importScript(src)) { + goog.dependencies_.written[src] = true; + } + }; + + + /** + * The default implementation of the import function. Writes a script tag to + * import the script. + * + * @param {string} src The script source. + * @return {boolean} True if the script was imported, false otherwise. + * @private + */ + goog.writeScriptTag_ = function(src) { + if (goog.inHtmlDocument_()) { + var doc = goog.global.document; + + // If the user tries to require a new symbol after document load, + // something has gone terribly wrong. Doing a document.write would + // wipe out the page. + if (doc.readyState == 'complete') { + // Certain test frameworks load base.js multiple times, which tries + // to write deps.js each time. If that happens, just fail silently. + // These frameworks wipe the page between each load of base.js, so this + // is OK. + var isDeps = /\bdeps.js$/.test(src); + if (isDeps) { + return false; + } else { + throw Error('Cannot write "' + src + '" after document load'); + } + } + + doc.write( + '" that closes the next token. If + // non-empty, the subsequent call to Next will return a raw or RCDATA text + // token: one that treats "

    " as text instead of an element. + // rawTag's contents are lower-cased. + rawTag string + // textIsRaw is whether the current text token's data is not escaped. + textIsRaw bool + // convertNUL is whether NUL bytes in the current token's data should + // be converted into \ufffd replacement characters. + convertNUL bool + // allowCDATA is whether CDATA sections are allowed in the current context. + allowCDATA bool +} + +// AllowCDATA sets whether or not the tokenizer recognizes as +// the text "foo". The default value is false, which means to recognize it as +// a bogus comment "" instead. +// +// Strictly speaking, an HTML5 compliant tokenizer should allow CDATA if and +// only if tokenizing foreign content, such as MathML and SVG. However, +// tracking foreign-contentness is difficult to do purely in the tokenizer, +// as opposed to the parser, due to HTML integration points: an element +// can contain a that is foreign-to-SVG but not foreign-to- +// HTML. For strict compliance with the HTML5 tokenization algorithm, it is the +// responsibility of the user of a tokenizer to call AllowCDATA as appropriate. +// In practice, if using the tokenizer without caring whether MathML or SVG +// CDATA is text or comments, such as tokenizing HTML to find all the anchor +// text, it is acceptable to ignore this responsibility. +func (z *Tokenizer) AllowCDATA(allowCDATA bool) { + z.allowCDATA = allowCDATA +} + +// NextIsNotRawText instructs the tokenizer that the next token should not be +// considered as 'raw text'. Some elements, such as script and title elements, +// normally require the next token after the opening tag to be 'raw text' that +// has no child elements. For example, tokenizing "a<b>c</b>d" +// yields a start tag token for "", a text token for "a<b>c</b>d", and +// an end tag token for "". There are no distinct start tag or end tag +// tokens for the "" and "". +// +// This tokenizer implementation will generally look for raw text at the right +// times. Strictly speaking, an HTML5 compliant tokenizer should not look for +// raw text if in foreign content: generally needs raw text, but a +// <title> inside an <svg> does not. Another example is that a <textarea> +// generally needs raw text, but a <textarea> is not allowed as an immediate +// child of a <select>; in normal parsing, a <textarea> implies </select>, but +// one cannot close the implicit element when parsing a <select>'s InnerHTML. +// Similarly to AllowCDATA, tracking the correct moment to override raw-text- +// ness is difficult to do purely in the tokenizer, as opposed to the parser. +// For strict compliance with the HTML5 tokenization algorithm, it is the +// responsibility of the user of a tokenizer to call NextIsNotRawText as +// appropriate. In practice, like AllowCDATA, it is acceptable to ignore this +// responsibility for basic usage. +// +// Note that this 'raw text' concept is different from the one offered by the +// Tokenizer.Raw method. +func (z *Tokenizer) NextIsNotRawText() { + z.rawTag = "" +} + +// Err returns the error associated with the most recent ErrorToken token. +// This is typically io.EOF, meaning the end of tokenization. +func (z *Tokenizer) Err() error { + if z.tt != ErrorToken { + return nil + } + return z.err +} + +// readByte returns the next byte from the input stream, doing a buffered read +// from z.r into z.buf if necessary. z.buf[z.raw.start:z.raw.end] remains a contiguous byte +// slice that holds all the bytes read so far for the current token. +// It sets z.err if the underlying reader returns an error. +// Pre-condition: z.err == nil. +func (z *Tokenizer) readByte() byte { + if z.raw.end >= len(z.buf) { + // Our buffer is exhausted and we have to read from z.r. Check if the + // previous read resulted in an error. + if z.readErr != nil { + z.err = z.readErr + return 0 + } + // We copy z.buf[z.raw.start:z.raw.end] to the beginning of z.buf. If the length + // z.raw.end - z.raw.start is more than half the capacity of z.buf, then we + // allocate a new buffer before the copy. + c := cap(z.buf) + d := z.raw.end - z.raw.start + var buf1 []byte + if 2*d > c { + buf1 = make([]byte, d, 2*c) + } else { + buf1 = z.buf[:d] + } + copy(buf1, z.buf[z.raw.start:z.raw.end]) + if x := z.raw.start; x != 0 { + // Adjust the data/attr spans to refer to the same contents after the copy. + z.data.start -= x + z.data.end -= x + z.pendingAttr[0].start -= x + z.pendingAttr[0].end -= x + z.pendingAttr[1].start -= x + z.pendingAttr[1].end -= x + for i := range z.attr { + z.attr[i][0].start -= x + z.attr[i][0].end -= x + z.attr[i][1].start -= x + z.attr[i][1].end -= x + } + } + z.raw.start, z.raw.end, z.buf = 0, d, buf1[:d] + // Now that we have copied the live bytes to the start of the buffer, + // we read from z.r into the remainder. + var n int + n, z.readErr = readAtLeastOneByte(z.r, buf1[d:cap(buf1)]) + if n == 0 { + z.err = z.readErr + return 0 + } + z.buf = buf1[:d+n] + } + x := z.buf[z.raw.end] + z.raw.end++ + if z.maxBuf > 0 && z.raw.end-z.raw.start >= z.maxBuf { + z.err = ErrBufferExceeded + return 0 + } + return x +} + +// Buffered returns a slice containing data buffered but not yet tokenized. +func (z *Tokenizer) Buffered() []byte { + return z.buf[z.raw.end:] +} + +// readAtLeastOneByte wraps an io.Reader so that reading cannot return (0, nil). +// It returns io.ErrNoProgress if the underlying r.Read method returns (0, nil) +// too many times in succession. +func readAtLeastOneByte(r io.Reader, b []byte) (int, error) { + for i := 0; i < 100; i++ { + n, err := r.Read(b) + if n != 0 || err != nil { + return n, err + } + } + return 0, io.ErrNoProgress +} + +// skipWhiteSpace skips past any white space. +func (z *Tokenizer) skipWhiteSpace() { + if z.err != nil { + return + } + for { + c := z.readByte() + if z.err != nil { + return + } + switch c { + case ' ', '\n', '\r', '\t', '\f': + // No-op. + default: + z.raw.end-- + return + } + } +} + +// readRawOrRCDATA reads until the next "</foo>", where "foo" is z.rawTag and +// is typically something like "script" or "textarea". +func (z *Tokenizer) readRawOrRCDATA() { + if z.rawTag == "script" { + z.readScript() + z.textIsRaw = true + z.rawTag = "" + return + } +loop: + for { + c := z.readByte() + if z.err != nil { + break loop + } + if c != '<' { + continue loop + } + c = z.readByte() + if z.err != nil { + break loop + } + if c != '/' { + continue loop + } + if z.readRawEndTag() || z.err != nil { + break loop + } + } + z.data.end = z.raw.end + // A textarea's or title's RCDATA can contain escaped entities. + z.textIsRaw = z.rawTag != "textarea" && z.rawTag != "title" + z.rawTag = "" +} + +// readRawEndTag attempts to read a tag like "</foo>", where "foo" is z.rawTag. +// If it succeeds, it backs up the input position to reconsume the tag and +// returns true. Otherwise it returns false. The opening "</" has already been +// consumed. +func (z *Tokenizer) readRawEndTag() bool { + for i := 0; i < len(z.rawTag); i++ { + c := z.readByte() + if z.err != nil { + return false + } + if c != z.rawTag[i] && c != z.rawTag[i]-('a'-'A') { + z.raw.end-- + return false + } + } + c := z.readByte() + if z.err != nil { + return false + } + switch c { + case ' ', '\n', '\r', '\t', '\f', '/', '>': + // The 3 is 2 for the leading "</" plus 1 for the trailing character c. + z.raw.end -= 3 + len(z.rawTag) + return true + } + z.raw.end-- + return false +} + +// readScript reads until the next </script> tag, following the byzantine +// rules for escaping/hiding the closing tag. +func (z *Tokenizer) readScript() { + defer func() { + z.data.end = z.raw.end + }() + var c byte + +scriptData: + c = z.readByte() + if z.err != nil { + return + } + if c == '<' { + goto scriptDataLessThanSign + } + goto scriptData + +scriptDataLessThanSign: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '/': + goto scriptDataEndTagOpen + case '!': + goto scriptDataEscapeStart + } + z.raw.end-- + goto scriptData + +scriptDataEndTagOpen: + if z.readRawEndTag() || z.err != nil { + return + } + goto scriptData + +scriptDataEscapeStart: + c = z.readByte() + if z.err != nil { + return + } + if c == '-' { + goto scriptDataEscapeStartDash + } + z.raw.end-- + goto scriptData + +scriptDataEscapeStartDash: + c = z.readByte() + if z.err != nil { + return + } + if c == '-' { + goto scriptDataEscapedDashDash + } + z.raw.end-- + goto scriptData + +scriptDataEscaped: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '-': + goto scriptDataEscapedDash + case '<': + goto scriptDataEscapedLessThanSign + } + goto scriptDataEscaped + +scriptDataEscapedDash: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '-': + goto scriptDataEscapedDashDash + case '<': + goto scriptDataEscapedLessThanSign + } + goto scriptDataEscaped + +scriptDataEscapedDashDash: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '-': + goto scriptDataEscapedDashDash + case '<': + goto scriptDataEscapedLessThanSign + case '>': + goto scriptData + } + goto scriptDataEscaped + +scriptDataEscapedLessThanSign: + c = z.readByte() + if z.err != nil { + return + } + if c == '/' { + goto scriptDataEscapedEndTagOpen + } + if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' { + goto scriptDataDoubleEscapeStart + } + z.raw.end-- + goto scriptData + +scriptDataEscapedEndTagOpen: + if z.readRawEndTag() || z.err != nil { + return + } + goto scriptDataEscaped + +scriptDataDoubleEscapeStart: + z.raw.end-- + for i := 0; i < len("script"); i++ { + c = z.readByte() + if z.err != nil { + return + } + if c != "script"[i] && c != "SCRIPT"[i] { + z.raw.end-- + goto scriptDataEscaped + } + } + c = z.readByte() + if z.err != nil { + return + } + switch c { + case ' ', '\n', '\r', '\t', '\f', '/', '>': + goto scriptDataDoubleEscaped + } + z.raw.end-- + goto scriptDataEscaped + +scriptDataDoubleEscaped: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '-': + goto scriptDataDoubleEscapedDash + case '<': + goto scriptDataDoubleEscapedLessThanSign + } + goto scriptDataDoubleEscaped + +scriptDataDoubleEscapedDash: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '-': + goto scriptDataDoubleEscapedDashDash + case '<': + goto scriptDataDoubleEscapedLessThanSign + } + goto scriptDataDoubleEscaped + +scriptDataDoubleEscapedDashDash: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '-': + goto scriptDataDoubleEscapedDashDash + case '<': + goto scriptDataDoubleEscapedLessThanSign + case '>': + goto scriptData + } + goto scriptDataDoubleEscaped + +scriptDataDoubleEscapedLessThanSign: + c = z.readByte() + if z.err != nil { + return + } + if c == '/' { + goto scriptDataDoubleEscapeEnd + } + z.raw.end-- + goto scriptDataDoubleEscaped + +scriptDataDoubleEscapeEnd: + if z.readRawEndTag() { + z.raw.end += len("</script>") + goto scriptDataEscaped + } + if z.err != nil { + return + } + goto scriptDataDoubleEscaped +} + +// readComment reads the next comment token starting with "<!--". The opening +// "<!--" has already been consumed. +func (z *Tokenizer) readComment() { + z.data.start = z.raw.end + defer func() { + if z.data.end < z.data.start { + // It's a comment with no data, like <!-->. + z.data.end = z.data.start + } + }() + for dashCount := 2; ; { + c := z.readByte() + if z.err != nil { + // Ignore up to two dashes at EOF. + if dashCount > 2 { + dashCount = 2 + } + z.data.end = z.raw.end - dashCount + return + } + switch c { + case '-': + dashCount++ + continue + case '>': + if dashCount >= 2 { + z.data.end = z.raw.end - len("-->") + return + } + case '!': + if dashCount >= 2 { + c = z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return + } + if c == '>' { + z.data.end = z.raw.end - len("--!>") + return + } + } + } + dashCount = 0 + } +} + +// readUntilCloseAngle reads until the next ">". +func (z *Tokenizer) readUntilCloseAngle() { + z.data.start = z.raw.end + for { + c := z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return + } + if c == '>' { + z.data.end = z.raw.end - len(">") + return + } + } +} + +// readMarkupDeclaration reads the next token starting with "<!". It might be +// a "<!--comment-->", a "<!DOCTYPE foo>", a "<![CDATA[section]]>" or +// "<!a bogus comment". The opening "<!" has already been consumed. +func (z *Tokenizer) readMarkupDeclaration() TokenType { + z.data.start = z.raw.end + var c [2]byte + for i := 0; i < 2; i++ { + c[i] = z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return CommentToken + } + } + if c[0] == '-' && c[1] == '-' { + z.readComment() + return CommentToken + } + z.raw.end -= 2 + if z.readDoctype() { + return DoctypeToken + } + if z.allowCDATA && z.readCDATA() { + z.convertNUL = true + return TextToken + } + // It's a bogus comment. + z.readUntilCloseAngle() + return CommentToken +} + +// readDoctype attempts to read a doctype declaration and returns true if +// successful. The opening "<!" has already been consumed. +func (z *Tokenizer) readDoctype() bool { + const s = "DOCTYPE" + for i := 0; i < len(s); i++ { + c := z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return false + } + if c != s[i] && c != s[i]+('a'-'A') { + // Back up to read the fragment of "DOCTYPE" again. + z.raw.end = z.data.start + return false + } + } + if z.skipWhiteSpace(); z.err != nil { + z.data.start = z.raw.end + z.data.end = z.raw.end + return true + } + z.readUntilCloseAngle() + return true +} + +// readCDATA attempts to read a CDATA section and returns true if +// successful. The opening "<!" has already been consumed. +func (z *Tokenizer) readCDATA() bool { + const s = "[CDATA[" + for i := 0; i < len(s); i++ { + c := z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return false + } + if c != s[i] { + // Back up to read the fragment of "[CDATA[" again. + z.raw.end = z.data.start + return false + } + } + z.data.start = z.raw.end + brackets := 0 + for { + c := z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return true + } + switch c { + case ']': + brackets++ + case '>': + if brackets >= 2 { + z.data.end = z.raw.end - len("]]>") + return true + } + brackets = 0 + default: + brackets = 0 + } + } +} + +// startTagIn returns whether the start tag in z.buf[z.data.start:z.data.end] +// case-insensitively matches any element of ss. +func (z *Tokenizer) startTagIn(ss ...string) bool { +loop: + for _, s := range ss { + if z.data.end-z.data.start != len(s) { + continue loop + } + for i := 0; i < len(s); i++ { + c := z.buf[z.data.start+i] + if 'A' <= c && c <= 'Z' { + c += 'a' - 'A' + } + if c != s[i] { + continue loop + } + } + return true + } + return false +} + +// readStartTag reads the next start tag token. The opening "<a" has already +// been consumed, where 'a' means anything in [A-Za-z]. +func (z *Tokenizer) readStartTag() TokenType { + z.readTag(true) + if z.err != nil { + return ErrorToken + } + // Several tags flag the tokenizer's next token as raw. + c, raw := z.buf[z.data.start], false + if 'A' <= c && c <= 'Z' { + c += 'a' - 'A' + } + switch c { + case 'i': + raw = z.startTagIn("iframe") + case 'n': + raw = z.startTagIn("noembed", "noframes", "noscript") + case 'p': + raw = z.startTagIn("plaintext") + case 's': + raw = z.startTagIn("script", "style") + case 't': + raw = z.startTagIn("textarea", "title") + case 'x': + raw = z.startTagIn("xmp") + } + if raw { + z.rawTag = strings.ToLower(string(z.buf[z.data.start:z.data.end])) + } + // Look for a self-closing token like "<br/>". + if z.err == nil && z.buf[z.raw.end-2] == '/' { + return SelfClosingTagToken + } + return StartTagToken +} + +// readTag reads the next tag token and its attributes. If saveAttr, those +// attributes are saved in z.attr, otherwise z.attr is set to an empty slice. +// The opening "<a" or "</a" has already been consumed, where 'a' means anything +// in [A-Za-z]. +func (z *Tokenizer) readTag(saveAttr bool) { + z.attr = z.attr[:0] + z.nAttrReturned = 0 + // Read the tag name and attribute key/value pairs. + z.readTagName() + if z.skipWhiteSpace(); z.err != nil { + return + } + for { + c := z.readByte() + if z.err != nil || c == '>' { + break + } + z.raw.end-- + z.readTagAttrKey() + z.readTagAttrVal() + // Save pendingAttr if saveAttr and that attribute has a non-empty key. + if saveAttr && z.pendingAttr[0].start != z.pendingAttr[0].end { + z.attr = append(z.attr, z.pendingAttr) + } + if z.skipWhiteSpace(); z.err != nil { + break + } + } +} + +// readTagName sets z.data to the "div" in "<div k=v>". The reader (z.raw.end) +// is positioned such that the first byte of the tag name (the "d" in "<div") +// has already been consumed. +func (z *Tokenizer) readTagName() { + z.data.start = z.raw.end - 1 + for { + c := z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return + } + switch c { + case ' ', '\n', '\r', '\t', '\f': + z.data.end = z.raw.end - 1 + return + case '/', '>': + z.raw.end-- + z.data.end = z.raw.end + return + } + } +} + +// readTagAttrKey sets z.pendingAttr[0] to the "k" in "<div k=v>". +// Precondition: z.err == nil. +func (z *Tokenizer) readTagAttrKey() { + z.pendingAttr[0].start = z.raw.end + for { + c := z.readByte() + if z.err != nil { + z.pendingAttr[0].end = z.raw.end + return + } + switch c { + case ' ', '\n', '\r', '\t', '\f', '/': + z.pendingAttr[0].end = z.raw.end - 1 + return + case '=', '>': + z.raw.end-- + z.pendingAttr[0].end = z.raw.end + return + } + } +} + +// readTagAttrVal sets z.pendingAttr[1] to the "v" in "<div k=v>". +func (z *Tokenizer) readTagAttrVal() { + z.pendingAttr[1].start = z.raw.end + z.pendingAttr[1].end = z.raw.end + if z.skipWhiteSpace(); z.err != nil { + return + } + c := z.readByte() + if z.err != nil { + return + } + if c != '=' { + z.raw.end-- + return + } + if z.skipWhiteSpace(); z.err != nil { + return + } + quote := z.readByte() + if z.err != nil { + return + } + switch quote { + case '>': + z.raw.end-- + return + + case '\'', '"': + z.pendingAttr[1].start = z.raw.end + for { + c := z.readByte() + if z.err != nil { + z.pendingAttr[1].end = z.raw.end + return + } + if c == quote { + z.pendingAttr[1].end = z.raw.end - 1 + return + } + } + + default: + z.pendingAttr[1].start = z.raw.end - 1 + for { + c := z.readByte() + if z.err != nil { + z.pendingAttr[1].end = z.raw.end + return + } + switch c { + case ' ', '\n', '\r', '\t', '\f': + z.pendingAttr[1].end = z.raw.end - 1 + return + case '>': + z.raw.end-- + z.pendingAttr[1].end = z.raw.end + return + } + } + } +} + +// Next scans the next token and returns its type. +func (z *Tokenizer) Next() TokenType { + z.raw.start = z.raw.end + z.data.start = z.raw.end + z.data.end = z.raw.end + if z.err != nil { + z.tt = ErrorToken + return z.tt + } + if z.rawTag != "" { + if z.rawTag == "plaintext" { + // Read everything up to EOF. + for z.err == nil { + z.readByte() + } + z.data.end = z.raw.end + z.textIsRaw = true + } else { + z.readRawOrRCDATA() + } + if z.data.end > z.data.start { + z.tt = TextToken + z.convertNUL = true + return z.tt + } + } + z.textIsRaw = false + z.convertNUL = false + +loop: + for { + c := z.readByte() + if z.err != nil { + break loop + } + if c != '<' { + continue loop + } + + // Check if the '<' we have just read is part of a tag, comment + // or doctype. If not, it's part of the accumulated text token. + c = z.readByte() + if z.err != nil { + break loop + } + var tokenType TokenType + switch { + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z': + tokenType = StartTagToken + case c == '/': + tokenType = EndTagToken + case c == '!' || c == '?': + // We use CommentToken to mean any of "<!--actual comments-->", + // "<!DOCTYPE declarations>" and "<?xml processing instructions?>". + tokenType = CommentToken + default: + continue + } + + // We have a non-text token, but we might have accumulated some text + // before that. If so, we return the text first, and return the non- + // text token on the subsequent call to Next. + if x := z.raw.end - len("<a"); z.raw.start < x { + z.raw.end = x + z.data.end = x + z.tt = TextToken + return z.tt + } + switch tokenType { + case StartTagToken: + z.tt = z.readStartTag() + return z.tt + case EndTagToken: + c = z.readByte() + if z.err != nil { + break loop + } + if c == '>' { + // "</>" does not generate a token at all. Generate an empty comment + // to allow passthrough clients to pick up the data using Raw. + // Reset the tokenizer state and start again. + z.tt = CommentToken + return z.tt + } + if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' { + z.readTag(false) + if z.err != nil { + z.tt = ErrorToken + } else { + z.tt = EndTagToken + } + return z.tt + } + z.raw.end-- + z.readUntilCloseAngle() + z.tt = CommentToken + return z.tt + case CommentToken: + if c == '!' { + z.tt = z.readMarkupDeclaration() + return z.tt + } + z.raw.end-- + z.readUntilCloseAngle() + z.tt = CommentToken + return z.tt + } + } + if z.raw.start < z.raw.end { + z.data.end = z.raw.end + z.tt = TextToken + return z.tt + } + z.tt = ErrorToken + return z.tt +} + +// Raw returns the unmodified text of the current token. Calling Next, Token, +// Text, TagName or TagAttr may change the contents of the returned slice. +func (z *Tokenizer) Raw() []byte { + return z.buf[z.raw.start:z.raw.end] +} + +// convertNewlines converts "\r" and "\r\n" in s to "\n". +// The conversion happens in place, but the resulting slice may be shorter. +func convertNewlines(s []byte) []byte { + for i, c := range s { + if c != '\r' { + continue + } + + src := i + 1 + if src >= len(s) || s[src] != '\n' { + s[i] = '\n' + continue + } + + dst := i + for src < len(s) { + if s[src] == '\r' { + if src+1 < len(s) && s[src+1] == '\n' { + src++ + } + s[dst] = '\n' + } else { + s[dst] = s[src] + } + src++ + dst++ + } + return s[:dst] + } + return s +} + +var ( + nul = []byte("\x00") + replacement = []byte("\ufffd") +) + +// Text returns the unescaped text of a text, comment or doctype token. The +// contents of the returned slice may change on the next call to Next. +func (z *Tokenizer) Text() []byte { + switch z.tt { + case TextToken, CommentToken, DoctypeToken: + s := z.buf[z.data.start:z.data.end] + z.data.start = z.raw.end + z.data.end = z.raw.end + s = convertNewlines(s) + if (z.convertNUL || z.tt == CommentToken) && bytes.Contains(s, nul) { + s = bytes.Replace(s, nul, replacement, -1) + } + if !z.textIsRaw { + s = unescape(s, false) + } + return s + } + return nil +} + +// TagName returns the lower-cased name of a tag token (the `img` out of +// `<IMG SRC="foo">`) and whether the tag has attributes. +// The contents of the returned slice may change on the next call to Next. +func (z *Tokenizer) TagName() (name []byte, hasAttr bool) { + if z.data.start < z.data.end { + switch z.tt { + case StartTagToken, EndTagToken, SelfClosingTagToken: + s := z.buf[z.data.start:z.data.end] + z.data.start = z.raw.end + z.data.end = z.raw.end + return lower(s), z.nAttrReturned < len(z.attr) + } + } + return nil, false +} + +// TagAttr returns the lower-cased key and unescaped value of the next unparsed +// attribute for the current tag token and whether there are more attributes. +// The contents of the returned slices may change on the next call to Next. +func (z *Tokenizer) TagAttr() (key, val []byte, moreAttr bool) { + if z.nAttrReturned < len(z.attr) { + switch z.tt { + case StartTagToken, SelfClosingTagToken: + x := z.attr[z.nAttrReturned] + z.nAttrReturned++ + key = z.buf[x[0].start:x[0].end] + val = z.buf[x[1].start:x[1].end] + return lower(key), unescape(convertNewlines(val), true), z.nAttrReturned < len(z.attr) + } + } + return nil, nil, false +} + +// Token returns the next Token. The result's Data and Attr values remain valid +// after subsequent Next calls. +func (z *Tokenizer) Token() Token { + t := Token{Type: z.tt} + switch z.tt { + case TextToken, CommentToken, DoctypeToken: + t.Data = string(z.Text()) + case StartTagToken, SelfClosingTagToken, EndTagToken: + name, moreAttr := z.TagName() + for moreAttr { + var key, val []byte + key, val, moreAttr = z.TagAttr() + t.Attr = append(t.Attr, Attribute{"", atom.String(key), string(val)}) + } + if a := atom.Lookup(name); a != 0 { + t.DataAtom, t.Data = a, a.String() + } else { + t.DataAtom, t.Data = 0, string(name) + } + } + return t +} + +// SetMaxBuf sets a limit on the amount of data buffered during tokenization. +// A value of 0 means unlimited. +func (z *Tokenizer) SetMaxBuf(n int) { + z.maxBuf = n +} + +// NewTokenizer returns a new HTML Tokenizer for the given Reader. +// The input is assumed to be UTF-8 encoded. +func NewTokenizer(r io.Reader) *Tokenizer { + return NewTokenizerFragment(r, "") +} + +// NewTokenizerFragment returns a new HTML Tokenizer for the given Reader, for +// tokenizing an exisitng element's InnerHTML fragment. contextTag is that +// element's tag, such as "div" or "iframe". +// +// For example, how the InnerHTML "a<b" is tokenized depends on whether it is +// for a <p> tag or a <script> tag. +// +// The input is assumed to be UTF-8 encoded. +func NewTokenizerFragment(r io.Reader, contextTag string) *Tokenizer { + z := &Tokenizer{ + r: r, + buf: make([]byte, 0, 4096), + } + if contextTag != "" { + switch s := strings.ToLower(contextTag); s { + case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "title", "textarea", "xmp": + z.rawTag = s + } + } + return z +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/token_test.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/token_test.go new file mode 100644 index 00000000..38d80d7f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/go.net/html/token_test.go @@ -0,0 +1,743 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "bytes" + "io" + "io/ioutil" + "reflect" + "runtime" + "strings" + "testing" +) + +type tokenTest struct { + // A short description of the test case. + desc string + // The HTML to parse. + html string + // The string representations of the expected tokens, joined by '$'. + golden string +} + +var tokenTests = []tokenTest{ + { + "empty", + "", + "", + }, + // A single text node. The tokenizer should not break text nodes on whitespace, + // nor should it normalize whitespace within a text node. + { + "text", + "foo bar", + "foo bar", + }, + // An entity. + { + "entity", + "one < two", + "one < two", + }, + // A start, self-closing and end tag. The tokenizer does not care if the start + // and end tokens don't match; that is the job of the parser. + { + "tags", + "<a>b<c/>d</e>", + "<a>$b$<c/>$d$</e>", + }, + // Angle brackets that aren't a tag. + { + "not a tag #0", + "<", + "<", + }, + { + "not a tag #1", + "</", + "</", + }, + { + "not a tag #2", + "</>", + "<!---->", + }, + { + "not a tag #3", + "a</>b", + "a$<!---->$b", + }, + { + "not a tag #4", + "</ >", + "<!-- -->", + }, + { + "not a tag #5", + "</.", + "<!--.-->", + }, + { + "not a tag #6", + "</.>", + "<!--.-->", + }, + { + "not a tag #7", + "a < b", + "a < b", + }, + { + "not a tag #8", + "<.>", + "<.>", + }, + { + "not a tag #9", + "a<<<b>>>c", + "a<<$<b>$>>c", + }, + { + "not a tag #10", + "if x<0 and y < 0 then x*y>0", + "if x<0 and y < 0 then x*y>0", + }, + // EOF in a tag name. + { + "tag name eof #0", + "<a", + "", + }, + { + "tag name eof #1", + "<a ", + "", + }, + { + "tag name eof #2", + "a<b", + "a", + }, + { + "tag name eof #3", + "<a><b", + "<a>", + }, + { + "tag name eof #4", + `<a x`, + ``, + }, + // Some malformed tags that are missing a '>'. + { + "malformed tag #0", + `<p</p>`, + `<p< p="">`, + }, + { + "malformed tag #1", + `<p </p>`, + `<p <="" p="">`, + }, + { + "malformed tag #2", + `<p id`, + ``, + }, + { + "malformed tag #3", + `<p id=`, + ``, + }, + { + "malformed tag #4", + `<p id=>`, + `<p id="">`, + }, + { + "malformed tag #5", + `<p id=0`, + ``, + }, + { + "malformed tag #6", + `<p id=0</p>`, + `<p id="0</p">`, + }, + { + "malformed tag #7", + `<p id="0</p>`, + ``, + }, + { + "malformed tag #8", + `<p id="0"</p>`, + `<p id="0" <="" p="">`, + }, + { + "malformed tag #9", + `<p></p id`, + `<p>`, + }, + // Raw text and RCDATA. + { + "basic raw text", + "<script><a></b></script>", + "<script>$<a></b>$</script>", + }, + { + "unfinished script end tag", + "<SCRIPT>a</SCR", + "<script>$a</SCR", + }, + { + "broken script end tag", + "<SCRIPT>a</SCR ipt>", + "<script>$a</SCR ipt>", + }, + { + "EOF in script end tag", + "<SCRIPT>a</SCRipt", + "<script>$a</SCRipt", + }, + { + "scriptx end tag", + "<SCRIPT>a</SCRiptx", + "<script>$a</SCRiptx", + }, + { + "' ' completes script end tag", + "<SCRIPT>a</SCRipt ", + "<script>$a", + }, + { + "'>' completes script end tag", + "<SCRIPT>a</SCRipt>", + "<script>$a$</script>", + }, + { + "self-closing script end tag", + "<SCRIPT>a</SCRipt/>", + "<script>$a$</script>", + }, + { + "nested script tag", + "<SCRIPT>a</SCRipt<script>", + "<script>$a</SCRipt<script>", + }, + { + "script end tag after unfinished", + "<SCRIPT>a</SCRipt</script>", + "<script>$a</SCRipt$</script>", + }, + { + "script/style mismatched tags", + "<script>a</style>", + "<script>$a</style>", + }, + { + "style element with entity", + "<style>'", + "<style>$&apos;", + }, + { + "textarea with tag", + "<textarea><div></textarea>", + "<textarea>$<div>$</textarea>", + }, + { + "title with tag and entity", + "<title><b>K&R C</b>", + "$<b>K&R C</b>$", + }, + // DOCTYPE tests. + { + "Proper DOCTYPE", + "", + "", + }, + { + "DOCTYPE with no space", + "", + "", + }, + { + "DOCTYPE with two spaces", + "", + "", + }, + { + "looks like DOCTYPE but isn't", + "", + "", + }, + { + "DOCTYPE at EOF", + "", + }, + // XML processing instructions. + { + "XML processing instruction", + "", + "", + }, + // Comments. + { + "comment0", + "abcdef", + "abc$$$$def", + }, + { + "comment1", + "az", + "a$$z", + }, + { + "comment2", + "az", + "a$$z", + }, + { + "comment3", + "az", + "a$$z", + }, + { + "comment4", + "az", + "a$$z", + }, + { + "comment5", + "az", + "a$$z", + }, + { + "comment6", + "az", + "a$$z", + }, + { + "comment7", + "a", + }, + { + "comment8", + "a", + }, + { + "comment9", + "a", + }, + { + "comment10", + "a", + }, + { + "comment11", + "a", + }, + { + "comment12", + "a", + }, + { + "comment13", + "az", + "a$$z", + }, + // An attribute with a backslash. + { + "backslash", + `

    `, + `

    `, + }, + // Entities, tag name and attribute key lower-casing, and whitespace + // normalization within a tag. + { + "tricky", + "

    te<&;xt

    ", + `

    $$te<&;xt$$

    `, + }, + // A nonexistent entity. Tokenizing and converting back to a string should + // escape the "&" to become "&". + { + "noSuchEntity", + `<&alsoDoesntExist;&`, + `$<&alsoDoesntExist;&`, + }, + { + "entity without semicolon", + `¬it;∉`, + `¬it;∉$`, + }, + { + "entity with digits", + "½", + "½", + }, + // Attribute tests: + // http://dev.w3.org/html5/spec/Overview.html#attributes-0 + { + "Empty attribute", + ``, + ``, + }, + { + "Empty attribute, whitespace", + ``, + ``, + }, + { + "Unquoted attribute value", + ``, + ``, + }, + { + "Unquoted attribute value, spaces", + ``, + ``, + }, + { + "Unquoted attribute value, trailing space", + ``, + ``, + }, + { + "Single-quoted attribute value", + ``, + ``, + }, + { + "Single-quoted attribute value, trailing space", + ``, + ``, + }, + { + "Double-quoted attribute value", + ``, + ``, + }, + { + "Attribute name characters", + ``, + ``, + }, + { + "Mixed attributes", + `a

    z`, + `a$

    $z`, + }, + { + "Attributes with a solitary single quote", + `

    `, + `

    $

    `, + }, +} + +func TestTokenizer(t *testing.T) { +loop: + for _, tt := range tokenTests { + z := NewTokenizer(strings.NewReader(tt.html)) + if tt.golden != "" { + for i, s := range strings.Split(tt.golden, "$") { + if z.Next() == ErrorToken { + t.Errorf("%s token %d: want %q got error %v", tt.desc, i, s, z.Err()) + continue loop + } + actual := z.Token().String() + if s != actual { + t.Errorf("%s token %d: want %q got %q", tt.desc, i, s, actual) + continue loop + } + } + } + z.Next() + if z.Err() != io.EOF { + t.Errorf("%s: want EOF got %q", tt.desc, z.Err()) + } + } +} + +func TestMaxBuffer(t *testing.T) { + // Exceeding the maximum buffer size generates ErrBufferExceeded. + z := NewTokenizer(strings.NewReader("<" + strings.Repeat("t", 10))) + z.SetMaxBuf(5) + tt := z.Next() + if got, want := tt, ErrorToken; got != want { + t.Fatalf("token type: got: %v want: %v", got, want) + } + if got, want := z.Err(), ErrBufferExceeded; got != want { + t.Errorf("error type: got: %v want: %v", got, want) + } + if got, want := string(z.Raw()), " 0 { + result.Write(z.Text()) + } + case StartTagToken, EndTagToken: + tn, _ := z.TagName() + if len(tn) == 1 && tn[0] == 'a' { + if tt == StartTagToken { + depth++ + } else { + depth-- + } + } + } + } + u := "14567" + v := string(result.Bytes()) + if u != v { + t.Errorf("TestBufAPI: want %q got %q", u, v) + } +} + +func TestConvertNewlines(t *testing.T) { + testCases := map[string]string{ + "Mac\rDOS\r\nUnix\n": "Mac\nDOS\nUnix\n", + "Unix\nMac\rDOS\r\n": "Unix\nMac\nDOS\n", + "DOS\r\nDOS\r\nDOS\r\n": "DOS\nDOS\nDOS\n", + "": "", + "\n": "\n", + "\n\r": "\n\n", + "\r": "\n", + "\r\n": "\n", + "\r\n\n": "\n\n", + "\r\n\r": "\n\n", + "\r\n\r\n": "\n\n", + "\r\r": "\n\n", + "\r\r\n": "\n\n", + "\r\r\n\n": "\n\n\n", + "\r\r\r\n": "\n\n\n", + "\r \n": "\n \n", + "xyz": "xyz", + } + for in, want := range testCases { + if got := string(convertNewlines([]byte(in))); got != want { + t.Errorf("input %q: got %q, want %q", in, got, want) + } + } +} + +func TestReaderEdgeCases(t *testing.T) { + const s = "

    An io.Reader can return (0, nil) or (n, io.EOF).

    " + testCases := []io.Reader{ + &zeroOneByteReader{s: s}, + &eofStringsReader{s: s}, + &stuckReader{}, + } + for i, tc := range testCases { + got := []TokenType{} + z := NewTokenizer(tc) + for { + tt := z.Next() + if tt == ErrorToken { + break + } + got = append(got, tt) + } + if err := z.Err(); err != nil && err != io.EOF { + if err != io.ErrNoProgress { + t.Errorf("i=%d: %v", i, err) + } + continue + } + want := []TokenType{ + StartTagToken, + TextToken, + EndTagToken, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("i=%d: got %v, want %v", i, got, want) + continue + } + } +} + +// zeroOneByteReader is like a strings.Reader that alternates between +// returning 0 bytes and 1 byte at a time. +type zeroOneByteReader struct { + s string + n int +} + +func (r *zeroOneByteReader) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + if len(r.s) == 0 { + return 0, io.EOF + } + r.n++ + if r.n%2 != 0 { + return 0, nil + } + p[0], r.s = r.s[0], r.s[1:] + return 1, nil +} + +// eofStringsReader is like a strings.Reader but can return an (n, err) where +// n > 0 && err != nil. +type eofStringsReader struct { + s string +} + +func (r *eofStringsReader) Read(p []byte) (int, error) { + n := copy(p, r.s) + r.s = r.s[n:] + if r.s != "" { + return n, nil + } + return n, io.EOF +} + +// stuckReader is an io.Reader that always returns no data and no error. +type stuckReader struct{} + +func (*stuckReader) Read(p []byte) (int, error) { + return 0, nil +} + +const ( + rawLevel = iota + lowLevel + highLevel +) + +func benchmarkTokenizer(b *testing.B, level int) { + buf, err := ioutil.ReadFile("testdata/go1.html") + if err != nil { + b.Fatalf("could not read testdata/go1.html: %v", err) + } + b.SetBytes(int64(len(buf))) + runtime.GC() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + z := NewTokenizer(bytes.NewBuffer(buf)) + for { + tt := z.Next() + if tt == ErrorToken { + if err := z.Err(); err != nil && err != io.EOF { + b.Fatalf("tokenizer error: %v", err) + } + break + } + switch level { + case rawLevel: + // Calling z.Raw just returns the raw bytes of the token. It does + // not unescape < to <, or lower-case tag names and attribute keys. + z.Raw() + case lowLevel: + // Caling z.Text, z.TagName and z.TagAttr returns []byte values + // whose contents may change on the next call to z.Next. + switch tt { + case TextToken, CommentToken, DoctypeToken: + z.Text() + case StartTagToken, SelfClosingTagToken: + _, more := z.TagName() + for more { + _, _, more = z.TagAttr() + } + case EndTagToken: + z.TagName() + } + case highLevel: + // Calling z.Token converts []byte values to strings whose validity + // extend beyond the next call to z.Next. + z.Token() + } + } + } +} + +func BenchmarkRawLevelTokenizer(b *testing.B) { benchmarkTokenizer(b, rawLevel) } +func BenchmarkLowLevelTokenizer(b *testing.B) { benchmarkTokenizer(b, lowLevel) } +func BenchmarkHighLevelTokenizer(b *testing.B) { benchmarkTokenizer(b, highLevel) } diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/.hgtags b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/.hgtags new file mode 100644 index 00000000..a6df8480 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/.hgtags @@ -0,0 +1,2 @@ +379476c9e05c5275356e0a82ca079e61869e9192 release +4ee7c273e92e663ef8dc0c476d395350a586ad75 weekly diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/AUTHORS b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/AUTHORS new file mode 100644 index 00000000..5ad2b581 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/AUTHORS @@ -0,0 +1,11 @@ +# This is the official list of goauth2 authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as +# Name or Organization +# The email address is not required for organizations. + +# Please keep the list sorted. + +Google Inc. diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/CONTRIBUTORS b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/CONTRIBUTORS new file mode 100644 index 00000000..2de444d4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/CONTRIBUTORS @@ -0,0 +1,37 @@ +# This is the official list of people who can contribute +# (and typically have contributed) code to the goauth2 repository. +# The AUTHORS file lists the copyright holders; this file +# lists people. For example, Google employees are listed here +# but not in AUTHORS, because Google holds the copyright. +# +# The submission process automatically checks to make sure +# that people submitting code are listed in this file (by email address). +# +# Names should be added to this file only after verifying that +# the individual or the individual's organization has agreed to +# the appropriate Contributor License Agreement, found here: +# +# http://code.google.com/legal/individual-cla-v1.0.html +# http://code.google.com/legal/corporate-cla-v1.0.html +# +# The agreement for individuals can be filled out on the web. +# +# When adding J Random Contributor's name to this file, +# either J's name or J's organization's name should be +# added to the AUTHORS file, depending on whether the +# individual or corporate CLA was used. + +# Names should be added to this file like so: +# Name +# +# An entry with two email addresses specifies that the +# first address should be used in the submit logs and +# that the second address should be recognized as the +# same person when interacting with Rietveld. + +# Please keep the list sorted. + +Andrew Gerrand +Brad Fitzpatrick +Mark-Antoine Ruel +Manu Garg diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/LICENSE b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/LICENSE new file mode 100644 index 00000000..6765f090 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The goauth2 Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/PATENTS b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/PATENTS new file mode 100644 index 00000000..9e871635 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the goauth2 project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/lib/codereview/codereview.cfg b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/lib/codereview/codereview.cfg new file mode 100644 index 00000000..93b55c0a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/lib/codereview/codereview.cfg @@ -0,0 +1 @@ +defaultcc: golang-dev@googlegroups.com diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/oauth/oauth.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/oauth/oauth.go new file mode 100644 index 00000000..29dc8d73 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/oauth/oauth.go @@ -0,0 +1,461 @@ +// Copyright 2011 The goauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package oauth supports making OAuth2-authenticated HTTP requests. +// +// Example usage: +// +// // Specify your configuration. (typically as a global variable) +// var config = &oauth.Config{ +// ClientId: YOUR_CLIENT_ID, +// ClientSecret: YOUR_CLIENT_SECRET, +// Scope: "https://www.googleapis.com/auth/buzz", +// AuthURL: "https://accounts.google.com/o/oauth2/auth", +// TokenURL: "https://accounts.google.com/o/oauth2/token", +// RedirectURL: "http://you.example.org/handler", +// } +// +// // A landing page redirects to the OAuth provider to get the auth code. +// func landing(w http.ResponseWriter, r *http.Request) { +// http.Redirect(w, r, config.AuthCodeURL("foo"), http.StatusFound) +// } +// +// // The user will be redirected back to this handler, that takes the +// // "code" query parameter and Exchanges it for an access token. +// func handler(w http.ResponseWriter, r *http.Request) { +// t := &oauth.Transport{Config: config} +// t.Exchange(r.FormValue("code")) +// // The Transport now has a valid Token. Create an *http.Client +// // with which we can make authenticated API requests. +// c := t.Client() +// c.Post(...) +// // ... +// // btw, r.FormValue("state") == "foo" +// } +// +package oauth + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "mime" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "sync" + "time" +) + +// OAuthError is the error type returned by many operations. +// +// In retrospect it should not exist. Don't depend on it. +type OAuthError struct { + prefix string + msg string +} + +func (oe OAuthError) Error() string { + return "OAuthError: " + oe.prefix + ": " + oe.msg +} + +// Cache specifies the methods that implement a Token cache. +type Cache interface { + Token() (*Token, error) + PutToken(*Token) error +} + +// CacheFile implements Cache. Its value is the name of the file in which +// the Token is stored in JSON format. +type CacheFile string + +func (f CacheFile) Token() (*Token, error) { + file, err := os.Open(string(f)) + if err != nil { + return nil, OAuthError{"CacheFile.Token", err.Error()} + } + defer file.Close() + tok := &Token{} + if err := json.NewDecoder(file).Decode(tok); err != nil { + return nil, OAuthError{"CacheFile.Token", err.Error()} + } + return tok, nil +} + +func (f CacheFile) PutToken(tok *Token) error { + file, err := os.OpenFile(string(f), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return OAuthError{"CacheFile.PutToken", err.Error()} + } + if err := json.NewEncoder(file).Encode(tok); err != nil { + file.Close() + return OAuthError{"CacheFile.PutToken", err.Error()} + } + if err := file.Close(); err != nil { + return OAuthError{"CacheFile.PutToken", err.Error()} + } + return nil +} + +// Config is the configuration of an OAuth consumer. +type Config struct { + // ClientId is the OAuth client identifier used when communicating with + // the configured OAuth provider. + ClientId string + + // ClientSecret is the OAuth client secret used when communicating with + // the configured OAuth provider. + ClientSecret string + + // Scope identifies the level of access being requested. Multiple scope + // values should be provided as a space-delimited string. + Scope string + + // AuthURL is the URL the user will be directed to in order to grant + // access. + AuthURL string + + // TokenURL is the URL used to retrieve OAuth tokens. + TokenURL string + + // RedirectURL is the URL to which the user will be returned after + // granting (or denying) access. + RedirectURL string + + // TokenCache allows tokens to be cached for subsequent requests. + TokenCache Cache + + // AccessType is an OAuth extension that gets sent as the + // "access_type" field in the URL from AuthCodeURL. + // See https://developers.google.com/accounts/docs/OAuth2WebServer. + // It may be "online" (the default) or "offline". + // If your application needs to refresh access tokens when the + // user is not present at the browser, then use offline. This + // will result in your application obtaining a refresh token + // the first time your application exchanges an authorization + // code for a user. + AccessType string + + // ApprovalPrompt indicates whether the user should be + // re-prompted for consent. If set to "auto" (default) the + // user will be prompted only if they haven't previously + // granted consent and the code can only be exchanged for an + // access token. + // If set to "force" the user will always be prompted, and the + // code can be exchanged for a refresh token. + ApprovalPrompt string +} + +// Token contains an end-user's tokens. +// This is the data you must store to persist authentication. +type Token struct { + AccessToken string + RefreshToken string + Expiry time.Time // If zero the token has no (known) expiry time. + + // Extra optionally contains extra metadata from the server + // when updating a token. The only current key that may be + // populated is "id_token". It may be nil and will be + // initialized as needed. + Extra map[string]string +} + +// Expired reports whether the token has expired or is invalid. +func (t *Token) Expired() bool { + if t.AccessToken == "" { + return true + } + if t.Expiry.IsZero() { + return false + } + return t.Expiry.Before(time.Now()) +} + +// Transport implements http.RoundTripper. When configured with a valid +// Config and Token it can be used to make authenticated HTTP requests. +// +// t := &oauth.Transport{config} +// t.Exchange(code) +// // t now contains a valid Token +// r, _, err := t.Client().Get("http://example.org/url/requiring/auth") +// +// It will automatically refresh the Token if it can, +// updating the supplied Token in place. +type Transport struct { + *Config + *Token + + // mu guards modifying the token. + mu sync.Mutex + + // Transport is the HTTP transport to use when making requests. + // It will default to http.DefaultTransport if nil. + // (It should never be an oauth.Transport.) + Transport http.RoundTripper +} + +// Client returns an *http.Client that makes OAuth-authenticated requests. +func (t *Transport) Client() *http.Client { + return &http.Client{Transport: t} +} + +func (t *Transport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} + +// AuthCodeURL returns a URL that the end-user should be redirected to, +// so that they may obtain an authorization code. +func (c *Config) AuthCodeURL(state string) string { + url_, err := url.Parse(c.AuthURL) + if err != nil { + panic("AuthURL malformed: " + err.Error()) + } + q := url.Values{ + "response_type": {"code"}, + "client_id": {c.ClientId}, + "redirect_uri": {c.RedirectURL}, + "scope": {c.Scope}, + "state": {state}, + "access_type": {c.AccessType}, + "approval_prompt": {c.ApprovalPrompt}, + }.Encode() + if url_.RawQuery == "" { + url_.RawQuery = q + } else { + url_.RawQuery += "&" + q + } + return url_.String() +} + +// Exchange takes a code and gets access Token from the remote server. +func (t *Transport) Exchange(code string) (*Token, error) { + if t.Config == nil { + return nil, OAuthError{"Exchange", "no Config supplied"} + } + + // If the transport or the cache already has a token, it is + // passed to `updateToken` to preserve existing refresh token. + tok := t.Token + if tok == nil && t.TokenCache != nil { + tok, _ = t.TokenCache.Token() + } + if tok == nil { + tok = new(Token) + } + err := t.updateToken(tok, url.Values{ + "grant_type": {"authorization_code"}, + "redirect_uri": {t.RedirectURL}, + "scope": {t.Scope}, + "code": {code}, + }) + if err != nil { + return nil, err + } + t.Token = tok + if t.TokenCache != nil { + return tok, t.TokenCache.PutToken(tok) + } + return tok, nil +} + +// RoundTrip executes a single HTTP transaction using the Transport's +// Token as authorization headers. +// +// This method will attempt to renew the Token if it has expired and may return +// an error related to that Token renewal before attempting the client request. +// If the Token cannot be renewed a non-nil os.Error value will be returned. +// If the Token is invalid callers should expect HTTP-level errors, +// as indicated by the Response's StatusCode. +func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + accessToken, err := t.getAccessToken() + if err != nil { + return nil, err + } + // To set the Authorization header, we must make a copy of the Request + // so that we don't modify the Request we were given. + // This is required by the specification of http.RoundTripper. + req = cloneRequest(req) + req.Header.Set("Authorization", "Bearer "+accessToken) + + // Make the HTTP request. + return t.transport().RoundTrip(req) +} + +func (t *Transport) getAccessToken() (string, error) { + t.mu.Lock() + defer t.mu.Unlock() + + if t.Token == nil { + if t.Config == nil { + return "", OAuthError{"RoundTrip", "no Config supplied"} + } + if t.TokenCache == nil { + return "", OAuthError{"RoundTrip", "no Token supplied"} + } + var err error + t.Token, err = t.TokenCache.Token() + if err != nil { + return "", err + } + } + + // Refresh the Token if it has expired. + if t.Expired() { + if err := t.Refresh(); err != nil { + return "", err + } + } + if t.AccessToken == "" { + return "", errors.New("no access token obtained from refresh") + } + return t.AccessToken, nil +} + +// cloneRequest returns a clone of the provided *http.Request. +// The clone is a shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // shallow copy of the struct + r2 := new(http.Request) + *r2 = *r + // deep copy of the Header + r2.Header = make(http.Header) + for k, s := range r.Header { + r2.Header[k] = s + } + return r2 +} + +// Refresh renews the Transport's AccessToken using its RefreshToken. +func (t *Transport) Refresh() error { + if t.Token == nil { + return OAuthError{"Refresh", "no existing Token"} + } + if t.RefreshToken == "" { + return OAuthError{"Refresh", "Token expired; no Refresh Token"} + } + if t.Config == nil { + return OAuthError{"Refresh", "no Config supplied"} + } + + err := t.updateToken(t.Token, url.Values{ + "grant_type": {"refresh_token"}, + "refresh_token": {t.RefreshToken}, + }) + if err != nil { + return err + } + if t.TokenCache != nil { + return t.TokenCache.PutToken(t.Token) + } + return nil +} + +// AuthenticateClient gets an access Token using the client_credentials grant +// type. +func (t *Transport) AuthenticateClient() error { + if t.Config == nil { + return OAuthError{"Exchange", "no Config supplied"} + } + if t.Token == nil { + t.Token = &Token{} + } + return t.updateToken(t.Token, url.Values{"grant_type": {"client_credentials"}}) +} + +// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL +// implements the OAuth2 spec correctly +// See https://code.google.com/p/goauth2/issues/detail?id=31 for background. +// In summary: +// - Reddit only accepts client_secret in Authorization header. +// - Dropbox accepts either, but not both. +// - Google only accepts client_secret (not spec compliant?) +func providerAuthHeaderWorks(tokenURL string) bool { + if strings.HasPrefix(tokenURL, "https://accounts.google.com/") { + // Google fails to implement the OAuth2 spec fully? + return false + } + return true +} + +// updateToken mutates both tok and v. +func (t *Transport) updateToken(tok *Token, v url.Values) error { + v.Set("client_id", t.ClientId) + bustedAuth := !providerAuthHeaderWorks(t.TokenURL) + if bustedAuth { + v.Set("client_secret", t.ClientSecret) + } + client := &http.Client{Transport: t.transport()} + req, err := http.NewRequest("POST", t.TokenURL, strings.NewReader(v.Encode())) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if !bustedAuth { + req.SetBasicAuth(t.ClientId, t.ClientSecret) + } + r, err := client.Do(req) + if err != nil { + return err + } + defer r.Body.Close() + if r.StatusCode != 200 { + return OAuthError{"updateToken", "Unexpected HTTP status " + r.Status} + } + var b struct { + Access string `json:"access_token"` + Refresh string `json:"refresh_token"` + ExpiresIn int64 `json:"expires_in"` // seconds + Id string `json:"id_token"` + } + + body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) + if err != nil { + return err + } + + content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) + switch content { + case "application/x-www-form-urlencoded", "text/plain": + vals, err := url.ParseQuery(string(body)) + if err != nil { + return err + } + + b.Access = vals.Get("access_token") + b.Refresh = vals.Get("refresh_token") + b.ExpiresIn, _ = strconv.ParseInt(vals.Get("expires_in"), 10, 64) + b.Id = vals.Get("id_token") + default: + if err = json.Unmarshal(body, &b); err != nil { + return fmt.Errorf("got bad response from server: %q", body) + } + } + if b.Access == "" { + return errors.New("received empty access token from authorization server") + } + tok.AccessToken = b.Access + // Don't overwrite `RefreshToken` with an empty value + if b.Refresh != "" { + tok.RefreshToken = b.Refresh + } + if b.ExpiresIn == 0 { + tok.Expiry = time.Time{} + } else { + tok.Expiry = time.Now().Add(time.Duration(b.ExpiresIn) * time.Second) + } + if b.Id != "" { + if tok.Extra == nil { + tok.Extra = make(map[string]string) + } + tok.Extra["id_token"] = b.Id + } + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/oauth/oauth_test.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/oauth/oauth_test.go new file mode 100644 index 00000000..81d4992a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/goauth2/oauth/oauth_test.go @@ -0,0 +1,219 @@ +// Copyright 2011 The goauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package oauth + +import ( + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "runtime" + "testing" + "time" +) + +var requests = []struct { + path, query, auth string // request + contenttype, body string // response +}{ + { + path: "/token", + query: "grant_type=authorization_code&code=c0d3&client_id=cl13nt1d", + contenttype: "application/json", + auth: "Basic Y2wxM250MWQ6czNjcjN0", + body: ` + { + "access_token":"token1", + "refresh_token":"refreshtoken1", + "id_token":"idtoken1", + "expires_in":3600 + } + `, + }, + {path: "/secure", auth: "Bearer token1", body: "first payload"}, + { + path: "/token", + query: "grant_type=refresh_token&refresh_token=refreshtoken1&client_id=cl13nt1d", + contenttype: "application/json", + auth: "Basic Y2wxM250MWQ6czNjcjN0", + body: ` + { + "access_token":"token2", + "refresh_token":"refreshtoken2", + "id_token":"idtoken2", + "expires_in":3600 + } + `, + }, + {path: "/secure", auth: "Bearer token2", body: "second payload"}, + { + path: "/token", + query: "grant_type=refresh_token&refresh_token=refreshtoken2&client_id=cl13nt1d", + contenttype: "application/x-www-form-urlencoded", + body: "access_token=token3&refresh_token=refreshtoken3&id_token=idtoken3&expires_in=3600", + auth: "Basic Y2wxM250MWQ6czNjcjN0", + }, + {path: "/secure", auth: "Bearer token3", body: "third payload"}, + { + path: "/token", + query: "grant_type=client_credentials&client_id=cl13nt1d", + contenttype: "application/json", + auth: "Basic Y2wxM250MWQ6czNjcjN0", + body: ` + { + "access_token":"token4", + "expires_in":3600 + } + `, + }, + {path: "/secure", auth: "Bearer token4", body: "fourth payload"}, +} + +func TestOAuth(t *testing.T) { + // Set up test server. + n := 0 + handler := func(w http.ResponseWriter, r *http.Request) { + if n >= len(requests) { + t.Errorf("too many requests: %d", n) + return + } + req := requests[n] + n++ + + // Check request. + if g, w := r.URL.Path, req.path; g != w { + t.Errorf("request[%d] got path %s, want %s", n, g, w) + } + want, _ := url.ParseQuery(req.query) + for k := range want { + if g, w := r.FormValue(k), want.Get(k); g != w { + t.Errorf("query[%s] = %s, want %s", k, g, w) + } + } + if g, w := r.Header.Get("Authorization"), req.auth; w != "" && g != w { + t.Errorf("Authorization: %v, want %v", g, w) + } + + // Send response. + w.Header().Set("Content-Type", req.contenttype) + io.WriteString(w, req.body) + } + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + + config := &Config{ + ClientId: "cl13nt1d", + ClientSecret: "s3cr3t", + Scope: "https://example.net/scope", + AuthURL: server.URL + "/auth", + TokenURL: server.URL + "/token", + } + + // TODO(adg): test AuthCodeURL + + transport := &Transport{Config: config} + _, err := transport.Exchange("c0d3") + if err != nil { + t.Fatalf("Exchange: %v", err) + } + checkToken(t, transport.Token, "token1", "refreshtoken1", "idtoken1") + + c := transport.Client() + resp, err := c.Get(server.URL + "/secure") + if err != nil { + t.Fatalf("Get: %v", err) + } + checkBody(t, resp, "first payload") + + // test automatic refresh + transport.Expiry = time.Now().Add(-time.Hour) + resp, err = c.Get(server.URL + "/secure") + if err != nil { + t.Fatalf("Get: %v", err) + } + checkBody(t, resp, "second payload") + checkToken(t, transport.Token, "token2", "refreshtoken2", "idtoken2") + + // refresh one more time, but get URL-encoded token instead of JSON + transport.Expiry = time.Now().Add(-time.Hour) + resp, err = c.Get(server.URL + "/secure") + if err != nil { + t.Fatalf("Get: %v", err) + } + checkBody(t, resp, "third payload") + checkToken(t, transport.Token, "token3", "refreshtoken3", "idtoken3") + + transport.Token = &Token{} + err = transport.AuthenticateClient() + if err != nil { + t.Fatalf("AuthenticateClient: %v", err) + } + checkToken(t, transport.Token, "token4", "", "") + resp, err = c.Get(server.URL + "/secure") + if err != nil { + t.Fatalf("Get: %v", err) + } + checkBody(t, resp, "fourth payload") +} + +func checkToken(t *testing.T, tok *Token, access, refresh, id string) { + if g, w := tok.AccessToken, access; g != w { + t.Errorf("AccessToken = %q, want %q", g, w) + } + if g, w := tok.RefreshToken, refresh; g != w { + t.Errorf("RefreshToken = %q, want %q", g, w) + } + if g, w := tok.Extra["id_token"], id; g != w { + t.Errorf("Extra['id_token'] = %q, want %q", g, w) + } + if tok.Expiry.IsZero() { + t.Errorf("Expiry is zero; want ~1 hour") + } else { + exp := tok.Expiry.Sub(time.Now()) + const slop = 3 * time.Second // time moving during test + if (time.Hour-slop) > exp || exp > time.Hour { + t.Errorf("Expiry = %v, want ~1 hour", exp) + } + } +} + +func checkBody(t *testing.T, r *http.Response, body string) { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("reading reponse body: %v, want %q", err, body) + } + if g, w := string(b), body; g != w { + t.Errorf("request body mismatch: got %q, want %q", g, w) + } +} + +func TestCachePermissions(t *testing.T) { + if runtime.GOOS == "windows" { + // Windows doesn't support file mode bits. + return + } + + td, err := ioutil.TempDir("", "oauth-test") + if err != nil { + t.Fatalf("ioutil.TempDir: %v", err) + } + defer os.RemoveAll(td) + tempFile := filepath.Join(td, "cache-file") + + cf := CacheFile(tempFile) + if err := cf.PutToken(new(Token)); err != nil { + t.Fatalf("PutToken: %v", err) + } + fi, err := os.Stat(tempFile) + if err != nil { + t.Fatalf("os.Stat: %v", err) + } + if fi.Mode()&0077 != 0 { + t.Errorf("Created cache file has mode %#o, want non-accessible to group+other", fi.Mode()) + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/.hgignore b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/.hgignore new file mode 100644 index 00000000..055f43c9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/.hgignore @@ -0,0 +1,29 @@ +syntax:glob +.DS_Store +.git +.gitignore +*.[568ao] +*.ao +*.so +*.pyc +._* +.nfs.* +[568a].out +*~ +*.orig +*.rej +*.exe +.*.swp +core +*.cgo*.go +*.cgo*.c +_cgo_* +_obj +_test +_testmain.go +build.out +test.out +y.tab.[ch] + +syntax:regexp +^.*/core.[0-9]*$ diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/AUTHORS b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/AUTHORS new file mode 100644 index 00000000..b7f7dc29 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/AUTHORS @@ -0,0 +1,12 @@ +# This is the official list of LevelDB-Go authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as +# Name or Organization +# The email address is not required for organizations. + +# Please keep the list sorted. + +Christoph Hack +Google Inc. diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/CONTRIBUTORS b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/CONTRIBUTORS new file mode 100644 index 00000000..e3b4f67b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/CONTRIBUTORS @@ -0,0 +1,31 @@ +# This is the official list of people who can contribute +# (and typically have contributed) code to the LevelDB-Go repository. +# The AUTHORS file lists the copyright holders; this file +# lists people. For example, Google employees are listed here +# but not in AUTHORS, because Google holds the copyright. +# +# The submission process automatically checks to make sure +# that people submitting code are listed in this file (by email address). +# +# Names should be added to this file only after verifying that +# the individual or the individual's organization has agreed to +# the appropriate Contributor License Agreement, found here: +# +# http://code.google.com/legal/individual-cla-v1.0.html +# http://code.google.com/legal/corporate-cla-v1.0.html +# +# The agreement for individuals can be filled out on the web. +# +# When adding J Random Contributor's name to this file, +# either J's name or J's organization's name should be +# added to the AUTHORS file, depending on whether the +# individual or corporate CLA was used. + +# Names should be added to this file like so: +# Name + +# Please keep the list sorted. + +Brad Fitzpatrick +Christoph Hack +Nigel Tao diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/LICENSE b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/LICENSE new file mode 100644 index 00000000..fec05ce1 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2011 The LevelDB-Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/README b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/README new file mode 100644 index 00000000..79a059c0 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/README @@ -0,0 +1,11 @@ +This is a LevelDB library for the Go programming language. + +To download and install from source: +$ go get code.google.com/p/leveldb-go/leveldb + +Unless otherwise noted, the LevelDB-Go source files are distributed +under the BSD-style license found in the LICENSE file. + +Contributions should follow the same procedure as for the Go project: +http://golang.org/doc/contribute.html + diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/crc/crc.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/crc/crc.go new file mode 100644 index 00000000..b21aeab4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/crc/crc.go @@ -0,0 +1,35 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package crc implements the checksum algorithm used throughout leveldb. +// +// The algorithm is CRC-32 with Castagnoli's polynomial, followed by a bit +// rotation and an additional delta. The additional processing is to lessen +// the probability of arbitrary key/value data coincidental contains bytes +// that look like a checksum. +// +// To calculate the uint32 checksum of some data: +// var u uint32 = crc.New(data).Value() +// In leveldb, the uint32 value is then stored in little-endian format. +package crc + +import ( + "hash/crc32" +) + +var table = crc32.MakeTable(crc32.Castagnoli) + +type CRC uint32 + +func New(b []byte) CRC { + return CRC(0).Update(b) +} + +func (c CRC) Update(b []byte) CRC { + return CRC(crc32.Update(uint32(c), table, b)) +} + +func (c CRC) Value() uint32 { + return uint32(c>>15|c<<17) + 0xa282ead8 +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/comparer.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/comparer.go new file mode 100644 index 00000000..358526c4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/comparer.go @@ -0,0 +1,86 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package db + +import ( + "bytes" +) + +// Comparer defines a total ordering over the space of []byte keys: a 'less +// than' relationship. +type Comparer interface { + // Compare returns -1, 0, or +1 depending on whether a is 'less than', + // 'equal to' or 'greater than' b. The two arguments can only be 'equal' + // if their contents are exactly equal. Furthermore, the empty slice + // must be 'less than' any non-empty slice. + Compare(a, b []byte) int + + // AppendSeparator appends a sequence of bytes x to dst such that + // a <= x && x < b, where 'less than' is consistent with Compare. + // It returns the enlarged slice, like the built-in append function. + // + // Precondition: either a is 'less than' b, or b is an empty slice. + // In the latter case, empty means 'positive infinity', and appending any + // x such that a <= x will be valid. + // + // An implementation may simply be "return append(dst, a...)" but appending + // fewer bytes will result in smaller tables. + // + // For example, if dst, a and b are the []byte equivalents of the strings + // "aqua", "black" and "blue", then the result may be "aquablb". + // Similarly, if the arguments were "aqua", "green" and "", then the result + // may be "aquah". + AppendSeparator(dst, a, b []byte) []byte +} + +// DefaultComparer is the default implementation of the Comparer interface. +// It uses the natural ordering, consistent with bytes.Compare. +var DefaultComparer Comparer = defCmp{} + +type defCmp struct{} + +func (defCmp) Compare(a, b []byte) int { + return bytes.Compare(a, b) +} + +func (defCmp) AppendSeparator(dst, a, b []byte) []byte { + i, n := SharedPrefixLen(a, b), len(dst) + dst = append(dst, a...) + if len(b) > 0 { + if i == len(a) { + return dst + } + if i == len(b) { + panic("a < b is a precondition, but b is a prefix of a") + } + if a[i] == 0xff || a[i]+1 >= b[i] { + // This isn't optimal, but it matches the C++ Level-DB implementation, and + // it's good enough. For example, if a is "1357" and b is "2", then the + // optimal (i.e. shortest) result is appending "14", but we append "1357". + return dst + } + } + i += n + for ; i < len(dst); i++ { + if dst[i] != 0xff { + dst[i]++ + return dst[:i+1] + } + } + return dst +} + +// SharedPrefixLen returns the largest i such that a[:i] equals b[:i]. +// This function can be useful in implementing the Comparer interface. +func SharedPrefixLen(a, b []byte) int { + i, n := 0, len(a) + if n > len(b) { + n = len(b) + } + for i < n && a[i] == b[i] { + i++ + } + return i +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/comparer_test.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/comparer_test.go new file mode 100644 index 00000000..65ed049c --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/comparer_test.go @@ -0,0 +1,50 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package db + +import ( + "testing" +) + +func TestDefCmp(t *testing.T) { + testCases := []struct { + a, b, want string + }{ + // Examples from the doc comments. + {"black", "blue", "blb"}, + {"green", "", "h"}, + // Non-empty b values. The C++ Level-DB code calls these separators. + {"", "2", ""}, + {"1", "2", "1"}, + {"1", "29", "1"}, + {"13", "19", "14"}, + {"13", "99", "2"}, + {"135", "19", "14"}, + {"1357", "19", "14"}, + {"1357", "2", "1357"}, + {"13\xff", "14", "13\xff"}, + {"13\xff", "19", "14"}, + {"1\xff\xff", "19", "1\xff\xff"}, + {"1\xff\xff", "2", "1\xff\xff"}, + {"1\xff\xff", "9", "2"}, + // Empty b values. The C++ Level-DB code calls these successors. + {"", "", ""}, + {"1", "", "2"}, + {"11", "", "2"}, + {"11\xff", "", "2"}, + {"1\xff", "", "2"}, + {"1\xff\xff", "", "2"}, + {"\xff", "", "\xff"}, + {"\xff\xff", "", "\xff\xff"}, + {"\xff\xff\xff", "", "\xff\xff\xff"}, + } + for _, tc := range testCases { + const s = "pqrs" + got := string(DefaultComparer.AppendSeparator([]byte(s), []byte(tc.a), []byte(tc.b))) + if got != s+tc.want { + t.Errorf("a, b = %q, %q: got %q, want %q", tc.a, tc.b, got, s+tc.want) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/db.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/db.go new file mode 100644 index 00000000..e591d77d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/db.go @@ -0,0 +1,121 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package db defines the interfaces for a key/value store. +// +// A DB's basic operations (Get, Set, Delete) should be self-explanatory. Get +// and Delete will return ErrNotFound if the requested key is not in the store. +// Callers are free to ignore this error. +// +// A DB also allows for iterating over the key/value pairs in key order. If d +// is a DB, the code below prints all key/value pairs whose keys are 'greater +// than or equal to' k: +// +// iter := d.Find(k) +// for iter.Next() { +// fmt.Printf("key=%q value=%q\n", iter.Key(), iter.Value()) +// } +// return iter.Close() +// +// Other leveldb packages provide implementations of these interfaces. The +// Options struct in this package holds the optional parameters for these +// implementations, including a Comparer to define a 'less than' relationship +// over keys. It is always valid to pass a nil *Options, which means to use +// the default parameter values. Any zero field of a non-nil *Options also +// means to use the default value for that parameter. Thus, the code below +// uses a custom Comparer, but the default values for every other parameter: +// +// db := memdb.New(&db.Options{ +// Comparer: myComparer, +// }) +package db + +import ( + "errors" +) + +// ErrNotFound means that a get or delete call did not find the requested key. +var ErrNotFound = errors.New("leveldb/db: not found") + +// Iterator iterates over a DB's key/value pairs in key order. +// +// An iterator must be closed after use, but it is not necessary to read an +// iterator until exhaustion. +// +// An iterator is not necessarily goroutine-safe, but it is safe to use +// multiple iterators concurrently, with each in a dedicated goroutine. +// +// It is also safe to use an iterator concurrently with modifying its +// underlying DB, if that DB permits modification. However, the resultant +// key/value pairs are not guaranteed to be a consistent snapshot of that DB +// at a particular point in time. +type Iterator interface { + // Next moves the iterator to the next key/value pair. + // It returns whether the iterator is exhausted. + Next() bool + + // Key returns the key of the current key/value pair, or nil if done. + // The caller should not modify the contents of the returned slice, and + // its contents may change on the next call to Next. + Key() []byte + + // Value returns the value of the current key/value pair, or nil if done. + // The caller should not modify the contents of the returned slice, and + // its contents may change on the next call to Next. + Value() []byte + + // Close closes the iterator and returns any accumulated error. Exhausting + // all the key/value pairs in a table is not considered to be an error. + // It is valid to call Close multiple times. Other methods should not be + // called after the iterator has been closed. + Close() error +} + +// DB is a key/value store. +// +// It is safe to call Get and Find from concurrent goroutines. It is not +// necessarily safe to do so for Set and Delete. +// +// Some implementations may impose additional restrictions. For example: +// - Set calls may need to be in increasing key order. +// - a DB may be read-only or write-only. +type DB interface { + // Get gets the value for the given key. It returns ErrNotFound if the DB + // does not contain the key. + // + // The caller should not modify the contents of the returned slice, but + // it is safe to modify the contents of the argument after Get returns. + Get(key []byte, o *ReadOptions) (value []byte, err error) + + // Set sets the value for the given key. It overwrites any previous value + // for that key; a DB is not a multi-map. + // + // It is safe to modify the contents of the arguments after Set returns. + Set(key, value []byte, o *WriteOptions) error + + // Delete deletes the value for the given key. It returns ErrNotFound if + // the DB does not contain the key. + // + // It is safe to modify the contents of the arguments after Delete returns. + Delete(key []byte, o *WriteOptions) error + + // Find returns an iterator positioned before the first key/value pair + // whose key is 'greater than or equal to' the given key. There may be no + // such pair, in which case the iterator will return false on Next. + // + // Any error encountered will be implicitly returned via the iterator. An + // error-iterator will yield no key/value pairs and closing that iterator + // will return that error. + // + // It is safe to modify the contents of the argument after Find returns. + Find(key []byte, o *ReadOptions) Iterator + + // Close closes the DB. It may or may not close any underlying io.Reader + // or io.Writer, depending on how the DB was created. + // + // It is not safe to close a DB until all outstanding iterators are closed. + // It is valid to call Close multiple times. Other methods should not be + // called after the DB has been closed. + Close() error +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/options.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/options.go new file mode 100644 index 00000000..5018b6ee --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/db/options.go @@ -0,0 +1,132 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package db + +// Compression is the per-block compression algorithm to use. +type Compression int + +const ( + DefaultCompression Compression = iota + NoCompression + SnappyCompression + nCompression +) + +// Options holds the optional parameters for leveldb's DB implementations. +// These options apply to the DB at large; per-query options are defined by +// the ReadOptions and WriteOptions types. +// +// Options are typically passed to a constructor function as a struct literal. +// The GetXxx methods are used inside the DB implementations; they return the +// default parameter value if the *Options receiver is nil or the field value +// is zero. +// +// Read/Write options: +// - Comparer +// Read options: +// - VerifyChecksums +// Write options: +// - BlockRestartInterval +// - BlockSize +// - Compression +type Options struct { + // BlockRestartInterval is the number of keys between restart points + // for delta encoding of keys. + // + // The default value is 16. + BlockRestartInterval int + + // BlockSize is the minimum uncompressed size in bytes of each table block. + // + // The default value is 4096. + BlockSize int + + // Comparer defines a total ordering over the space of []byte keys: a 'less + // than' relationship. The same comparison algorithm must be used for reads + // and writes over the lifetime of the DB. + // + // The default value uses the same ordering as bytes.Compare. + Comparer Comparer + + // Compression defines the per-block compression to use. + // + // The default value (DefaultCompression) uses snappy compression. + Compression Compression + + // VerifyChecksums is whether to verify the per-block checksums in a DB. + // + // The default value is false. + VerifyChecksums bool +} + +func (o *Options) GetBlockRestartInterval() int { + if o == nil || o.BlockRestartInterval <= 0 { + return 16 + } + return o.BlockRestartInterval +} + +func (o *Options) GetBlockSize() int { + if o == nil || o.BlockSize <= 0 { + return 4096 + } + return o.BlockSize +} + +func (o *Options) GetComparer() Comparer { + if o == nil || o.Comparer == nil { + return DefaultComparer + } + return o.Comparer +} + +func (o *Options) GetCompression() Compression { + if o == nil || o.Compression <= DefaultCompression || o.Compression >= nCompression { + // Default to SnappyCompression. + return SnappyCompression + } + return o.Compression +} + +func (o *Options) GetVerifyChecksums() bool { + if o == nil { + return false + } + return o.VerifyChecksums +} + +// ReadOptions hold the optional per-query parameters for Get and Find +// operations. +// +// Like Options, a nil *ReadOptions is valid and means to use the default +// values. +type ReadOptions struct { + // No fields so far. +} + +// WriteOptions hold the optional per-query parameters for Set and Delete +// operations. +// +// Like Options, a nil *WriteOptions is valid and means to use the default +// values. +type WriteOptions struct { + // Sync is whether to sync underlying writes from the OS buffer cache + // through to actual disk, if applicable. Setting Sync can result in + // slower writes. + // + // If false, and the machine crashes, then some recent writes may be lost. + // Note that if it is just the process that crashes (and the machine does + // not) then no writes will be lost. + // + // In other words, Sync being false has the same semantics as a write + // system call. Sync being true means write followed by fsync. + // + // The default value is false. + Sync bool +} + +func (o *WriteOptions) GetSync() bool { + return o != nil && o.Sync +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/leveldb.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/leveldb.go new file mode 100644 index 00000000..16c04c2b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/leveldb.go @@ -0,0 +1,18 @@ +// Copyright 2012 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package leveldb provides an ordered key/value store. +// +// BUG: This package is incomplete. +package leveldb + +// This file is a placeholder for listing import dependencies. + +import ( + _ "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/crc" + _ "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/db" + _ "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/memdb" + _ "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/record" + _ "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/table" +) diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/memdb/memdb.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/memdb/memdb.go new file mode 100644 index 00000000..3e41c047 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/memdb/memdb.go @@ -0,0 +1,318 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package memdb provides a memory-backed implementation of the db.DB +// interface. +// +// A MemDB's memory consumption increases monotonically, even if keys are +// deleted or values are updated with shorter slices. Callers of the package +// are responsible for explicitly compacting a MemDB into a separate DB +// (whether in-memory or on-disk) when appropriate. +package memdb + +import ( + "encoding/binary" + "math/rand" + "sync" + + "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/db" +) + +// maxHeight is the maximum height of a MemDB's skiplist. +const maxHeight = 12 + +// A MemDB's skiplist consists of a number of nodes, and each node is +// represented by a variable number of ints: a key-offset, a value-offset, and +// between 1 and maxHeight next nodes. The key-offset and value-offset encode +// the node's key/value pair and are offsets into a MemDB's kvData slice. +// The remaining ints, for the next nodes in the skiplist's linked lists, are +// offsets into a MemDB's nodeData slice. +// +// The fXxx constants represent how to find the Xxx field of a node in the +// nodeData. For example, given an int 30 representing a node, and given +// nodeData[30:36] that looked like [60, 71, 82, 83, 84, 85], then +// nodeData[30 + fKey] = 60 would be the node's key-offset, +// nodeData[30 + fVal] = 71 would be the node's value-offset, and +// nodeData[30 + fNxt + 0] = 82 would be the next node at the height-0 list, +// nodeData[30 + fNxt + 1] = 83 would be the next node at the height-1 list, +// and so on. A node's height is implied by the skiplist construction: a node +// of height x appears in the height-h list iff 0 <= h && h < x. +const ( + fKey = iota + fVal + fNxt +) + +const ( + // zeroNode represents the end of a linked list. + zeroNode = 0 + // headNode represents the start of the linked list. It is equal to -fNxt + // so that the next nodes at height-h are at nodeData[h]. + // The head node is an artificial node and has no key or value. + headNode = -fNxt +) + +// A node's key-offset and value-offset fields are offsets into a MemDB's +// kvData slice that stores varint-prefixed strings: the node's key and value. +// A negative offset means a zero-length string, whether explicitly set to +// empty or implicitly set by deletion. +const ( + kvOffsetEmptySlice = -1 + kvOffsetDeletedNode = -2 +) + +// MemDB is a memory-backed implementation of the db.DB interface. +// +// It is safe to call Get, Set, Delete and Find concurrently. +type MemDB struct { + mutex sync.RWMutex + // height is the number of such lists, which can increase over time. + height int + // cmp defines an ordering on keys. + cmp db.Comparer + // kvData is an append-only buffer that holds varint-prefixed strings. + kvData []byte + // nodeData is an append-only buffer that holds a node's fields. + nodeData []int +} + +// MemDB implements the db.DB interface. +var _ db.DB = &MemDB{} + +// load loads a []byte from m.kvData. +func (m *MemDB) load(kvOffset int) (b []byte) { + if kvOffset < 0 { + return nil + } + bLen, n := binary.Uvarint(m.kvData[kvOffset:]) + return m.kvData[kvOffset+n : kvOffset+n+int(bLen)] +} + +// save saves a []byte to m.kvData. +func (m *MemDB) save(b []byte) (kvOffset int) { + if len(b) == 0 { + return kvOffsetEmptySlice + } + kvOffset = len(m.kvData) + var buf [binary.MaxVarintLen64]byte + length := binary.PutUvarint(buf[:], uint64(len(b))) + m.kvData = append(m.kvData, buf[:length]...) + m.kvData = append(m.kvData, b...) + return kvOffset +} + +// findNode returns the first node n whose key is >= the given key (or nil if +// there is no such node) and whether n's key equals key. The search is based +// solely on the contents of a node's key. Whether or not that key was +// previously deleted from the MemDB is not relevant. +// +// If prev is non-nil, it also sets the first m.height elements of prev to the +// preceding node at each height. +func (m *MemDB) findNode(key []byte, prev *[maxHeight]int) (n int, exactMatch bool) { + for h, p := m.height-1, headNode; h >= 0; h-- { + // Walk the skiplist at height h until we find either a zero node + // or one whose key is >= the given key. + n = m.nodeData[p+fNxt+h] + for { + if n == zeroNode { + exactMatch = false + break + } + kOff := m.nodeData[n+fKey] + if c := m.cmp.Compare(m.load(kOff), key); c >= 0 { + exactMatch = c == 0 + break + } + p, n = n, m.nodeData[n+fNxt+h] + } + if prev != nil { + (*prev)[h] = p + } + } + return n, exactMatch +} + +// Get implements DB.Get, as documented in the leveldb/db package. +func (m *MemDB) Get(key []byte, o *db.ReadOptions) (value []byte, err error) { + m.mutex.RLock() + defer m.mutex.RUnlock() + n, exactMatch := m.findNode(key, nil) + vOff := m.nodeData[n+fVal] + if !exactMatch || vOff == kvOffsetDeletedNode { + return nil, db.ErrNotFound + } + return m.load(vOff), nil +} + +// Set implements DB.Set, as documented in the leveldb/db package. +func (m *MemDB) Set(key, value []byte, o *db.WriteOptions) error { + m.mutex.Lock() + defer m.mutex.Unlock() + // Find the node, and its predecessors at all heights. + var prev [maxHeight]int + n, exactMatch := m.findNode(key, &prev) + if exactMatch { + m.nodeData[n+fVal] = m.save(value) + return nil + } + // Choose the new node's height, branching with 25% probability. + h := 1 + for h < maxHeight && rand.Intn(4) == 0 { + h++ + } + // Raise the skiplist's height to the node's height, if necessary. + if m.height < h { + for i := m.height; i < h; i++ { + prev[i] = headNode + } + m.height = h + } + // Insert the new node. + var x [fNxt + maxHeight]int + n1 := len(m.nodeData) + x[fKey] = m.save(key) + x[fVal] = m.save(value) + for i := 0; i < h; i++ { + j := prev[i] + fNxt + i + x[fNxt+i] = m.nodeData[j] + m.nodeData[j] = n1 + } + m.nodeData = append(m.nodeData, x[:fNxt+h]...) + return nil +} + +// Delete implements DB.Delete, as documented in the leveldb/db package. +func (m *MemDB) Delete(key []byte, o *db.WriteOptions) error { + m.mutex.Lock() + defer m.mutex.Unlock() + n, exactMatch := m.findNode(key, nil) + if !exactMatch || m.nodeData[n+fVal] == kvOffsetDeletedNode { + return db.ErrNotFound + } + m.nodeData[n+fVal] = kvOffsetDeletedNode + return nil +} + +// Find implements DB.Find, as documented in the leveldb/db package. +func (m *MemDB) Find(key []byte, o *db.ReadOptions) db.Iterator { + m.mutex.RLock() + defer m.mutex.RUnlock() + n, _ := m.findNode(key, nil) + for n != zeroNode && m.nodeData[n+fVal] == kvOffsetDeletedNode { + n = m.nodeData[n+fNxt] + } + t := &iterator{ + m: m, + restartNode: n, + } + t.fill() + // The iterator is positioned at the first node >= key. The iterator API + // requires that the caller the Next first, so we set t.i0 to -1. + t.i0 = -1 + return t +} + +// Close implements DB.Close, as documented in the leveldb/db package. +func (m *MemDB) Close() error { + return nil +} + +// ApproximateMemoryUsage returns the approximate memory usage of the MemDB. +func (m *MemDB) ApproximateMemoryUsage() int { + m.mutex.RLock() + defer m.mutex.RUnlock() + return len(m.kvData) +} + +// New returns a new MemDB. +func New(o *db.Options) *MemDB { + return &MemDB{ + height: 1, + cmp: o.GetComparer(), + kvData: make([]byte, 0, 4096), + // The first maxHeight values of nodeData are the next nodes after the + // head node at each possible height. Their initial value is zeroNode. + nodeData: make([]int, maxHeight, 256), + } +} + +// iterator is a MemDB iterator that buffers upcoming results, so that it does +// not have to acquire the MemDB's mutex on each Next call. +type iterator struct { + m *MemDB + // restartNode is the node to start refilling the buffer from. + restartNode int + // i0 is the current iterator position with respect to buf. A value of -1 + // means that the iterator is at the start, end or both of the iteration. + // i1 is the number of buffered entries. + // Invariant: -1 <= i0 && i0 < i1 && i1 <= len(buf). + i0, i1 int + // buf buffers up to 32 key/value pairs. + buf [32][2][]byte +} + +// iterator implements the db.Iterator interface. +var _ db.Iterator = &iterator{} + +// fill fills the iterator's buffer with key/value pairs from the MemDB. +// +// Precondition: t.m.mutex is locked for reading. +func (t *iterator) fill() { + i, n := 0, t.restartNode + for i < len(t.buf) && n != zeroNode { + if t.m.nodeData[n+fVal] != kvOffsetDeletedNode { + t.buf[i][fKey] = t.m.load(t.m.nodeData[n+fKey]) + t.buf[i][fVal] = t.m.load(t.m.nodeData[n+fVal]) + i++ + } + n = t.m.nodeData[n+fNxt] + } + if i == 0 { + // There were no non-deleted nodes on or after t.restartNode. + // The iterator is exhausted. + t.i0 = -1 + } else { + t.i0 = 0 + } + t.i1 = i + t.restartNode = n +} + +// Next implements Iterator.Next, as documented in the leveldb/db package. +func (t *iterator) Next() bool { + t.i0++ + if t.i0 < t.i1 { + return true + } + if t.restartNode == zeroNode { + t.i0 = -1 + t.i1 = 0 + return false + } + t.m.mutex.RLock() + defer t.m.mutex.RUnlock() + t.fill() + return true +} + +// Key implements Iterator.Key, as documented in the leveldb/db package. +func (t *iterator) Key() []byte { + if t.i0 < 0 { + return nil + } + return t.buf[t.i0][fKey] +} + +// Value implements Iterator.Value, as documented in the leveldb/db package. +func (t *iterator) Value() []byte { + if t.i0 < 0 { + return nil + } + return t.buf[t.i0][fVal] +} + +// Close implements Iterator.Close, as documented in the leveldb/db package. +func (t *iterator) Close() error { + return nil +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/memdb/memdb_test.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/memdb/memdb_test.go new file mode 100644 index 00000000..ba3416e3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/memdb/memdb_test.go @@ -0,0 +1,222 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package memdb + +import ( + "fmt" + "math/rand" + "strconv" + "strings" + "testing" + + "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/db" +) + +// count returns the number of entries in a DB. +func count(d db.DB) (n int) { + x := d.Find(nil, nil) + for x.Next() { + n++ + } + if x.Close() != nil { + return -1 + } + return n +} + +// compact compacts a MemDB. +func compact(m *MemDB) (*MemDB, error) { + n, x := New(nil), m.Find(nil, nil) + for x.Next() { + if err := n.Set(x.Key(), x.Value(), nil); err != nil { + return nil, err + } + } + if err := x.Close(); err != nil { + return nil, err + } + return n, nil +} + +func TestBasic(t *testing.T) { + // Check the empty DB. + m := New(nil) + if got, want := count(m), 0; got != want { + t.Fatalf("0.count: got %v, want %v", got, want) + } + v, err := m.Get([]byte("cherry"), nil) + if string(v) != "" || err != db.ErrNotFound { + t.Fatalf("1.get: got (%q, %v), want (%q, %v)", v, err, "", db.ErrNotFound) + } + // Add some key/value pairs. + m.Set([]byte("cherry"), []byte("red"), nil) + m.Set([]byte("peach"), []byte("yellow"), nil) + m.Set([]byte("grape"), []byte("red"), nil) + m.Set([]byte("grape"), []byte("green"), nil) + m.Set([]byte("plum"), []byte("purple"), nil) + if got, want := count(m), 4; got != want { + t.Fatalf("2.count: got %v, want %v", got, want) + } + // Delete a key twice. + if got, want := m.Delete([]byte("grape"), nil), error(nil); got != want { + t.Fatalf("3.delete: got %v, want %v", got, want) + } + if got, want := m.Delete([]byte("grape"), nil), db.ErrNotFound; got != want { + t.Fatalf("4.delete: got %v, want %v", got, want) + } + if got, want := count(m), 3; got != want { + t.Fatalf("5.count: got %v, want %v", got, want) + } + // Get keys that are and aren't in the DB. + v, err = m.Get([]byte("plum"), nil) + if string(v) != "purple" || err != nil { + t.Fatalf("6.get: got (%q, %v), want (%q, %v)", v, err, "purple", error(nil)) + } + v, err = m.Get([]byte("lychee"), nil) + if string(v) != "" || err != db.ErrNotFound { + t.Fatalf("7.get: got (%q, %v), want (%q, %v)", v, err, "", db.ErrNotFound) + } + // Check an iterator. + s, x := "", m.Find([]byte("mango"), nil) + for x.Next() { + s += fmt.Sprintf("%s/%s.", x.Key(), x.Value()) + } + if want := "peach/yellow.plum/purple."; s != want { + t.Fatalf("8.iter: got %q, want %q", s, want) + } + if err = x.Close(); err != nil { + t.Fatalf("9.close: %v", err) + } + // Check some more sets and deletes. + if got, want := m.Delete([]byte("cherry"), nil), error(nil); got != want { + t.Fatalf("10.delete: got %v, want %v", got, want) + } + if got, want := count(m), 2; got != want { + t.Fatalf("11.count: got %v, want %v", got, want) + } + if err := m.Set([]byte("apricot"), []byte("orange"), nil); err != nil { + t.Fatalf("12.set: %v", err) + } + if got, want := count(m), 3; got != want { + t.Fatalf("13.count: got %v, want %v", got, want) + } + // Clean up. + if err := m.Close(); err != nil { + t.Fatalf("14.close: %v", err) + } +} + +func TestCount(t *testing.T) { + m := New(nil) + for i := 0; i < 200; i++ { + if j := count(m); j != i { + t.Fatalf("count: got %d, want %d", j, i) + } + m.Set([]byte{byte(i)}, nil, nil) + } + if err := m.Close(); err != nil { + t.Fatal(err) + } +} + +func Test1000Entries(t *testing.T) { + // Initialize the DB. + const N = 1000 + m0 := New(nil) + for i := 0; i < N; i++ { + k := []byte(strconv.Itoa(i)) + v := []byte(strings.Repeat("x", i)) + m0.Set(k, v, nil) + } + // Delete one third of the entries, update another third, + // and leave the last third alone. + for i := 0; i < N; i++ { + switch i % 3 { + case 0: + k := []byte(strconv.Itoa(i)) + m0.Delete(k, nil) + case 1: + k := []byte(strconv.Itoa(i)) + v := []byte(strings.Repeat("y", i)) + m0.Set(k, v, nil) + case 2: + // No-op. + } + } + // Check the DB count. + if got, want := count(m0), 666; got != want { + t.Fatalf("count: got %v, want %v", got, want) + } + // Check random-access lookup. + r := rand.New(rand.NewSource(0)) + for i := 0; i < 3*N; i++ { + j := r.Intn(N) + k := []byte(strconv.Itoa(j)) + v, err := m0.Get(k, nil) + var c uint8 + if len(v) != 0 { + c = v[0] + } + switch j % 3 { + case 0: + if err != db.ErrNotFound { + t.Fatalf("get: j=%d, got err=%v, want %v", j, err, db.ErrNotFound) + } + case 1: + if len(v) != j || c != 'y' { + t.Fatalf("get: j=%d, got len(v),c=%d,%c, want %d,%c", j, len(v), c, j, 'y') + } + case 2: + if len(v) != j || c != 'x' { + t.Fatalf("get: j=%d, got len(v),c=%d,%c, want %d,%c", j, len(v), c, j, 'x') + } + } + } + // Check that iterating through the middle of the DB looks OK. + // Keys are in lexicographic order, not numerical order. + // Multiples of 3 are not present. + wants := []string{ + "499", + "5", + "50", + "500", + "502", + "503", + "505", + "506", + "508", + "509", + "511", + } + x := m0.Find([]byte(wants[0]), nil) + for _, want := range wants { + if !x.Next() { + t.Fatalf("iter: next failed, want=%q", want) + } + if got := string(x.Key()); got != want { + t.Fatalf("iter: got %q, want %q", got, want) + } + } + if err := x.Close(); err != nil { + t.Fatalf("close: %v", err) + } + // Check that compaction reduces memory usage by at least one third. + amu0 := m0.ApproximateMemoryUsage() + if amu0 == 0 { + t.Fatalf("compact: memory usage is zero") + } + m1, err := compact(m0) + if err != nil { + t.Fatalf("compact: %v", err) + } + amu1 := m1.ApproximateMemoryUsage() + if ratio := float64(amu1) / float64(amu0); ratio > 0.667 { + t.Fatalf("compact: memory usage before=%d, after=%d, ratio=%f", amu0, amu1, ratio) + } + // Clean up. + if err := m0.Close(); err != nil { + t.Fatalf("close: %v", err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/record/record.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/record/record.go new file mode 100644 index 00000000..cc5fed95 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/record/record.go @@ -0,0 +1,377 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package record reads and writes sequences of records. Each record is a stream +// of bytes that completes before the next record starts. +// +// When reading, call Next to obtain an io.Reader for the next record. Next will +// return io.EOF when there are no more records. It is valid to call Next +// without reading the current record to exhaustion. +// +// When writing, call Next to obtain an io.Writer for the next record. Calling +// Next finishes the current record. Call Close to finish the final record. +// +// Optionally, call Flush to finish the current record and flush the underlying +// writer without starting a new record. To start a new record after flushing, +// call Next. +// +// Neither Readers or Writers are safe to use concurrently. +// +// Example code: +// func read(r io.Reader) ([]string, error) { +// var ss []string +// sr := record.NewReader(r) +// for { +// err := sr.Next() +// if err == io.EOF { +// break +// } +// if err != nil { +// return nil, err +// } +// s, err := ioutil.ReadAll(sr) +// if err != nil { +// return nil, err +// } +// ss = append(ss, string(s)) +// } +// return ss, nil +// } +// +// func write(w io.Writer, ss []string) error { +// sw := record.NewWriter(w) +// for _, s := range ss { +// sw.Next() +// if _, err := sw.Write([]byte(s)), err != nil { +// return err +// } +// } +// return sw.Close() +// } +// +// The wire format is that the stream is divided into 32KiB blocks, and each +// block contains a number of tightly packed chunks. Chunks cannot cross block +// boundaries. The last block may be shorter than 32 KiB. Any unused bytes in a +// block must be zero. +// +// A record maps to one or more chunks. Each chunk has a 7 byte header (a 4 +// byte checksum, a 2 byte little-endian uint16 length, and a 1 byte chunk type) +// followed by a payload. The checksum is over the chunk type and the payload. +// +// There are four chunk types: whether the chunk is the full record, or the +// first, middle or last chunk of a multi-chunk record. A multi-chunk record +// has one first chunk, zero or more middle chunks, and one last chunk. +// +// The wire format allows for limited recovery in the face of data corruption: +// on a format error (such as a checksum mismatch), the reader moves to the +// next block and looks for the next full or first chunk. +package record + +// TODO: implement the recovery algorithm. + +// The C++ Level-DB code calls this the log, but it has been renamed to record +// to avoid clashing with the standard log package, and because it is generally +// useful outside of logging. The C++ code also uses the term "physical record" +// instead of "chunk", but "chunk" is shorter and less confusing. + +import ( + "encoding/binary" + "errors" + "io" + + "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/crc" +) + +// These constants are part of the wire format and should not be changed. +const ( + fullChunkType = 1 + firstChunkType = 2 + middleChunkType = 3 + lastChunkType = 4 +) + +const ( + blockSize = 32 * 1024 + headerSize = 7 +) + +type flusher interface { + Flush() error +} + +// Reader reads records from an underlying io.Reader. +type Reader struct { + // r is the underlying reader. + r io.Reader + // seq is the sequence number of the current record. + seq int + // buf[i:j] is the unread portion of the current chunk's payload. + // The low bound, i, excludes the chunk header. + i, j int + // n is the number of bytes of buf that are valid. Once reading has started, + // only the final block can have n < blockSize. + n int + // started is whether Next has been called at all. + started bool + // last is whether the current chunk is the last chunk of the record. + last bool + // err is any accumulated error. + err error + // buf is the buffer. + buf [blockSize]byte +} + +// NewReader returns a new reader. +func NewReader(r io.Reader) *Reader { + return &Reader{ + r: r, + } +} + +// nextChunk sets r.buf[r.i:r.j] to hold the next chunk's payload, reading the +// next block into the buffer if necessary. +func (r *Reader) nextChunk(wantFirst bool) error { + for { + if r.j+headerSize <= r.n { + checksum := binary.LittleEndian.Uint32(r.buf[r.j+0 : r.j+4]) + length := binary.LittleEndian.Uint16(r.buf[r.j+4 : r.j+6]) + chunkType := int(r.buf[r.j+6]) + + r.i = r.j + headerSize + r.j = r.j + headerSize + int(length) + if r.j > r.n { + return errors.New("leveldb/record: invalid chunk (length overflows block)") + } + if checksum != crc.New(r.buf[r.i-1:r.j]).Value() { + return errors.New("leveldb/record: invalid chunk (checksum mismatch)") + } + if wantFirst { + if chunkType != fullChunkType && chunkType != firstChunkType { + continue + } + } + r.last = chunkType == fullChunkType || chunkType == lastChunkType + return nil + } + if r.n < blockSize && r.started { + if r.j != r.n { + return io.ErrUnexpectedEOF + } + return io.EOF + } + n, err := io.ReadFull(r.r, r.buf[:]) + if err != nil && err != io.ErrUnexpectedEOF { + return err + } + r.i, r.j, r.n = 0, 0, n + } + panic("unreachable") +} + +// Next returns a reader for the next record. It returns io.EOF if there are no +// more records. The reader returned becomes stale after the next Next call, +// and should no longer be used. +func (r *Reader) Next() (io.Reader, error) { + r.seq++ + if r.err != nil { + return nil, r.err + } + r.i = r.j + r.err = r.nextChunk(true) + if r.err != nil { + return nil, r.err + } + r.started = true + return singleReader{r, r.seq}, nil +} + +type singleReader struct { + r *Reader + seq int +} + +func (x singleReader) Read(p []byte) (int, error) { + r := x.r + if r.seq != x.seq { + return 0, errors.New("leveldb/record: stale reader") + } + if r.err != nil { + return 0, r.err + } + for r.i == r.j { + if r.last { + return 0, io.EOF + } + if r.err = r.nextChunk(false); r.err != nil { + return 0, r.err + } + } + n := copy(p, r.buf[r.i:r.j]) + r.i += n + return n, nil +} + +// Writer writes records to an underlying io.Writer. +type Writer struct { + // w is the underlying writer. + w io.Writer + // seq is the sequence number of the current record. + seq int + // f is w as a flusher. + f flusher + // buf[i:j] is the bytes that will become the current chunk. + // The low bound, i, includes the chunk header. + i, j int + // buf[:written] has already been written to w. + // written is zero unless Flush has been called. + written int + // first is whether the current chunk is the first chunk of the record. + first bool + // pending is whether a chunk is buffered but not yet written. + pending bool + // err is any accumulated error. + err error + // buf is the buffer. + buf [blockSize]byte +} + +// NewWriter returns a new Writer. +func NewWriter(w io.Writer) *Writer { + f, _ := w.(flusher) + return &Writer{ + w: w, + f: f, + } +} + +// fillHeader fills in the header for the pending chunk. +func (w *Writer) fillHeader(last bool) { + if w.i+headerSize > w.j || w.j > blockSize { + panic("leveldb/record: bad writer state") + } + if last { + if w.first { + w.buf[w.i+6] = fullChunkType + } else { + w.buf[w.i+6] = lastChunkType + } + } else { + if w.first { + w.buf[w.i+6] = firstChunkType + } else { + w.buf[w.i+6] = middleChunkType + } + } + binary.LittleEndian.PutUint32(w.buf[w.i+0:w.i+4], crc.New(w.buf[w.i+6:w.j]).Value()) + binary.LittleEndian.PutUint16(w.buf[w.i+4:w.i+6], uint16(w.j-w.i-headerSize)) +} + +// writeBlock writes the buffered block to the underlying writer, and reserves +// space for the next chunk's header. +func (w *Writer) writeBlock() { + _, w.err = w.w.Write(w.buf[w.written:]) + w.i = 0 + w.j = headerSize + w.written = 0 +} + +// writePending finishes the current record and writes the buffer to the +// underlying writer. +func (w *Writer) writePending() { + if w.err != nil { + return + } + if w.pending { + w.fillHeader(true) + w.pending = false + } + _, w.err = w.w.Write(w.buf[w.written:w.j]) + w.written = w.j +} + +// Close finishes the current record and closes the writer. +func (w *Writer) Close() error { + w.seq++ + w.writePending() + if w.err != nil { + return w.err + } + w.err = errors.New("leveldb/record: closed Writer") + return nil +} + +// Flush finishes the current record, writes to the underlying writer, and +// flushes it if that writer implements interface{ Flush() error }. +func (w *Writer) Flush() error { + w.seq++ + w.writePending() + if w.err != nil { + return w.err + } + if w.f != nil { + w.err = w.f.Flush() + return w.err + } + return nil +} + +// Next returns a writer for the next record. The writer returned becomes stale +// after the next Close, Flush or Next call, and should no longer be used. +func (w *Writer) Next() (io.Writer, error) { + w.seq++ + if w.err != nil { + return nil, w.err + } + if w.pending { + w.fillHeader(true) + } + w.i = w.j + w.j = w.j + headerSize + // Check if there is room in the block for the header. + if w.j > blockSize { + // Fill in the rest of the block with zeroes. + for k := w.i; k < blockSize; k++ { + w.buf[k] = 0 + } + w.writeBlock() + if w.err != nil { + return nil, w.err + } + } + w.first = true + w.pending = true + return singleWriter{w, w.seq}, nil +} + +type singleWriter struct { + w *Writer + seq int +} + +func (x singleWriter) Write(p []byte) (int, error) { + w := x.w + if w.seq != x.seq { + return 0, errors.New("leveldb/record: stale writer") + } + if w.err != nil { + return 0, w.err + } + n0 := len(p) + for len(p) > 0 { + // Write a block, if it is full. + if w.j == blockSize { + w.fillHeader(false) + w.writeBlock() + if w.err != nil { + return 0, w.err + } + w.first = false + } + // Copy bytes into the buffer. + n := copy(w.buf[w.j:], p) + w.j += n + p = p[n:] + } + return n0, nil +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/record/record_test.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/record/record_test.go new file mode 100644 index 00000000..af034e75 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/record/record_test.go @@ -0,0 +1,314 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package record + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "math/rand" + "strings" + "testing" +) + +func short(s string) string { + if len(s) < 64 { + return s + } + return fmt.Sprintf("%s...(skipping %d bytes)...%s", s[:20], len(s)-40, s[len(s)-20:]) +} + +// big returns a string of length n, composed of repetitions of partial. +func big(partial string, n int) string { + return strings.Repeat(partial, n/len(partial)+1)[:n] +} + +func TestEmpty(t *testing.T) { + buf := new(bytes.Buffer) + r := NewReader(buf) + if _, err := r.Next(); err != io.EOF { + t.Fatalf("got %v, want %v", err, io.EOF) + } +} + +func testGenerator(t *testing.T, reset func(), gen func() (string, bool)) { + buf := new(bytes.Buffer) + + reset() + w := NewWriter(buf) + for { + s, ok := gen() + if !ok { + break + } + ww, err := w.Next() + if err != nil { + t.Fatal(err) + } + if _, err := ww.Write([]byte(s)); err != nil { + t.Fatal(err) + } + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + + reset() + r := NewReader(buf) + for { + s, ok := gen() + if !ok { + break + } + rr, err := r.Next() + if err != nil { + t.Fatal(err) + } + x, err := ioutil.ReadAll(rr) + if err != nil { + t.Fatal(err) + } + if string(x) != s { + t.Fatalf("got %q, want %q", short(string(x)), short(s)) + } + } + if _, err := r.Next(); err != io.EOF { + t.Fatalf("got %v, want %v", err, io.EOF) + } +} + +func testLiterals(t *testing.T, s []string) { + var i int + reset := func() { + i = 0 + } + gen := func() (string, bool) { + if i == len(s) { + return "", false + } + i++ + return s[i-1], true + } + testGenerator(t, reset, gen) +} + +func TestMany(t *testing.T) { + const n = 1e5 + var i int + reset := func() { + i = 0 + } + gen := func() (string, bool) { + if i == n { + return "", false + } + i++ + return fmt.Sprintf("%d.", i-1), true + } + testGenerator(t, reset, gen) +} + +func TestRandom(t *testing.T) { + const n = 1e2 + var ( + i int + r *rand.Rand + ) + reset := func() { + i, r = 0, rand.New(rand.NewSource(0)) + } + gen := func() (string, bool) { + if i == n { + return "", false + } + i++ + return strings.Repeat(string(uint8(i)), r.Intn(2*blockSize+16)), true + } + testGenerator(t, reset, gen) +} + +func TestBasic(t *testing.T) { + testLiterals(t, []string{ + strings.Repeat("a", 1000), + strings.Repeat("b", 97270), + strings.Repeat("c", 8000), + }) +} + +func TestBoundary(t *testing.T) { + for i := blockSize - 16; i < blockSize+16; i++ { + s0 := big("abcd", i) + for j := blockSize - 16; j < blockSize+16; j++ { + s1 := big("ABCDE", j) + testLiterals(t, []string{s0, s1}) + testLiterals(t, []string{s0, "", s1}) + testLiterals(t, []string{s0, "x", s1}) + } + } +} + +func TestFlush(t *testing.T) { + buf := new(bytes.Buffer) + w := NewWriter(buf) + // Write a couple of records. Everything should still be held + // in the record.Writer buffer, so that buf.Len should be 0. + w0, _ := w.Next() + w0.Write([]byte("0")) + w1, _ := w.Next() + w1.Write([]byte("11")) + if got, want := buf.Len(), 0; got != want { + t.Fatalf("buffer length #0: got %d want %d", got, want) + } + // Flush the record.Writer buffer, which should yield 17 bytes. + // 17 = 2*7 + 1 + 2, which is two headers and 1 + 2 payload bytes. + if err := w.Flush(); err != nil { + t.Fatal(err) + } + if got, want := buf.Len(), 17; got != want { + t.Fatalf("buffer length #1: got %d want %d", got, want) + } + // Do another write, one that isn't large enough to complete the block. + // The write should not have flowed through to buf. + w2, _ := w.Next() + w2.Write(bytes.Repeat([]byte("2"), 10000)) + if got, want := buf.Len(), 17; got != want { + t.Fatalf("buffer length #2: got %d want %d", got, want) + } + // Flushing should get us up to 10024 bytes written. + // 10024 = 17 + 7 + 10000. + if err := w.Flush(); err != nil { + t.Fatal(err) + } + if got, want := buf.Len(), 10024; got != want { + t.Fatalf("buffer length #3: got %d want %d", got, want) + } + // Do a bigger write, one that completes the current block. + // We should now have 32768 bytes (a complete block), without + // an explicit flush. + w3, _ := w.Next() + w3.Write(bytes.Repeat([]byte("3"), 40000)) + if got, want := buf.Len(), 32768; got != want { + t.Fatalf("buffer length #4: got %d want %d", got, want) + } + // Flushing should get us up to 50038 bytes written. + // 50038 = 10024 + 2*7 + 40000. There are two headers because + // the one record was split into two chunks. + if err := w.Flush(); err != nil { + t.Fatal(err) + } + if got, want := buf.Len(), 50038; got != want { + t.Fatalf("buffer length #5: got %d want %d", got, want) + } + // Check that reading those records give the right lengths. + r := NewReader(buf) + wants := []int64{1, 2, 10000, 40000} + for i, want := range wants { + rr, _ := r.Next() + n, err := io.Copy(ioutil.Discard, rr) + if err != nil { + t.Fatalf("read #%d: %v", i, err) + } + if n != want { + t.Fatalf("read #%d: got %d bytes want %d", i, n, want) + } + } +} + +func TestNonExhaustiveRead(t *testing.T) { + const n = 100 + buf := new(bytes.Buffer) + p := make([]byte, 10) + rnd := rand.New(rand.NewSource(1)) + + w := NewWriter(buf) + for i := 0; i < n; i++ { + length := len(p) + rnd.Intn(3*blockSize) + s := string(uint8(i)) + "123456789abcdefgh" + ww, _ := w.Next() + ww.Write([]byte(big(s, length))) + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + + r := NewReader(buf) + for i := 0; i < n; i++ { + rr, _ := r.Next() + _, err := io.ReadFull(rr, p) + if err != nil { + t.Fatal(err) + } + want := string(uint8(i)) + "123456789" + if got := string(p); got != want { + t.Fatalf("read #%d: got %q want %q", i, got, want) + } + } +} + +func TestStaleReader(t *testing.T) { + buf := new(bytes.Buffer) + + w := NewWriter(buf) + w0, err := w.Next() + if err != nil { + t.Fatal(err) + } + w0.Write([]byte("0")) + w1, err := w.Next() + if err != nil { + t.Fatal(err) + } + w1.Write([]byte("11")) + if err := w.Close(); err != nil { + t.Fatal(err) + } + + r := NewReader(buf) + r0, err := r.Next() + if err != nil { + t.Fatal(err) + } + r1, err := r.Next() + if err != nil { + t.Fatal(err) + } + p := make([]byte, 1) + if _, err := r0.Read(p); err == nil || !strings.Contains(err.Error(), "stale") { + t.Fatalf("stale read #0: unexpected error: %v", err) + } + if _, err := r1.Read(p); err != nil { + t.Fatalf("fresh read #1: got %v want nil error", err) + } + if p[0] != '1' { + t.Fatalf("fresh read #1: byte contents: got '%c' want '1'", p[0]) + } +} + +func TestStaleWriter(t *testing.T) { + buf := new(bytes.Buffer) + + w := NewWriter(buf) + w0, err := w.Next() + if err != nil { + t.Fatal(err) + } + w1, err := w.Next() + if err != nil { + t.Fatal(err) + } + if _, err := w0.Write([]byte("0")); err == nil || !strings.Contains(err.Error(), "stale") { + t.Fatalf("stale write #0: unexpected error: %v", err) + } + if _, err := w1.Write([]byte("11")); err != nil { + t.Fatalf("fresh write #1: got %v want nil error", err) + } + if err := w.Flush(); err != nil { + t.Fatalf("flush: %v", err) + } + if _, err := w1.Write([]byte("0")); err == nil || !strings.Contains(err.Error(), "stale") { + t.Fatalf("stale write #1: unexpected error: %v", err) + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/reader.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/reader.go new file mode 100644 index 00000000..163a38d6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/reader.go @@ -0,0 +1,403 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package table + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "os" + "sort" + + "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/crc" + "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/db" + "camlistore.org/third_party/code.google.com/p/snappy-go/snappy" +) + +// blockHandle is the file offset and length of a block. +type blockHandle struct { + offset, length uint64 +} + +// decodeBlockHandle returns the block handle encoded at the start of src, as +// well as the number of bytes it occupies. It returns zero if given invalid +// input. +func decodeBlockHandle(src []byte) (blockHandle, int) { + offset, n := binary.Uvarint(src) + length, m := binary.Uvarint(src[n:]) + if n == 0 || m == 0 { + return blockHandle{}, 0 + } + return blockHandle{offset, length}, n + m +} + +func encodeBlockHandle(dst []byte, b blockHandle) int { + n := binary.PutUvarint(dst, b.offset) + m := binary.PutUvarint(dst[n:], b.length) + return n + m +} + +// block is a []byte that holds a sequence of key/value pairs plus an index +// over those pairs. +type block []byte + +// seek returns a blockIter positioned at the first key/value pair whose key is +// >= the given key. If there is no such key, the blockIter returned is done. +func (b block) seek(c db.Comparer, key []byte) (*blockIter, error) { + numRestarts := int(binary.LittleEndian.Uint32(b[len(b)-4:])) + if numRestarts == 0 { + return nil, errors.New("leveldb/table: invalid table (block has no restart points)") + } + n := len(b) - 4*(1+numRestarts) + var offset int + if len(key) > 0 { + // Find the index of the smallest restart point whose key is > the key + // sought; index will be numRestarts if there is no such restart point. + index := sort.Search(numRestarts, func(i int) bool { + o := int(binary.LittleEndian.Uint32(b[n+4*i:])) + // For a restart point, there are 0 bytes shared with the previous key. + // The varint encoding of 0 occupies 1 byte. + o++ + // Decode the key at that restart point, and compare it to the key sought. + v1, n1 := binary.Uvarint(b[o:]) + _, n2 := binary.Uvarint(b[o+n1:]) + m := o + n1 + n2 + s := b[m : m+int(v1)] + return c.Compare(s, key) > 0 + }) + // Since keys are strictly increasing, if index > 0 then the restart + // point at index-1 will be the largest whose key is <= the key sought. + // If index == 0, then all keys in this block are larger than the key + // sought, and offset remains at zero. + if index > 0 { + offset = int(binary.LittleEndian.Uint32(b[n+4*(index-1):])) + } + } + // Initialize the blockIter to the restart point. + i := &blockIter{ + data: b[offset:n], + key: make([]byte, 0, 256), + } + // Iterate from that restart point to somewhere >= the key sought. + for i.Next() && c.Compare(i.key, key) < 0 { + } + if i.err != nil { + return nil, i.err + } + i.soi = !i.eoi + return i, nil +} + +// blockIter is an iterator over a single block of data. +type blockIter struct { + data []byte + key, val []byte + err error + // soi and eoi mark the start and end of iteration. + // Both cannot simultaneously be true. + soi, eoi bool +} + +// blockIter implements the db.Iterator interface. +var _ db.Iterator = (*blockIter)(nil) + +// Next implements Iterator.Next, as documented in the leveldb/db package. +func (i *blockIter) Next() bool { + if i.eoi || i.err != nil { + return false + } + if i.soi { + i.soi = false + return true + } + if len(i.data) == 0 { + i.Close() + return false + } + v0, n0 := binary.Uvarint(i.data) + v1, n1 := binary.Uvarint(i.data[n0:]) + v2, n2 := binary.Uvarint(i.data[n0+n1:]) + n := n0 + n1 + n2 + i.key = append(i.key[:v0], i.data[n:n+int(v1)]...) + i.val = i.data[n+int(v1) : n+int(v1+v2)] + i.data = i.data[n+int(v1+v2):] + return true +} + +// Key implements Iterator.Key, as documented in the leveldb/db package. +func (i *blockIter) Key() []byte { + if i.soi { + return nil + } + return i.key +} + +// Value implements Iterator.Value, as documented in the leveldb/db package. +func (i *blockIter) Value() []byte { + if i.soi { + return nil + } + return i.val +} + +// Close implements Iterator.Close, as documented in the leveldb/db package. +func (i *blockIter) Close() error { + i.key = nil + i.val = nil + i.eoi = true + return i.err +} + +// tableIter is an iterator over an entire table of data. It is a two-level +// iterator: to seek for a given key, it first looks in the index for the +// block that contains that key, and then looks inside that block. +type tableIter struct { + reader *Reader + data *blockIter + index *blockIter + err error +} + +// tableIter implements the db.Iterator interface. +var _ db.Iterator = (*tableIter)(nil) + +// nextBlock loads the next block and positions i.data at the first key in that +// block which is >= the given key. If unsuccessful, it sets i.err to any error +// encountered, which may be nil if we have simply exhausted the entire table. +func (i *tableIter) nextBlock(key []byte) bool { + if !i.index.Next() { + i.err = i.index.err + return false + } + // Load the next block. + v := i.index.Value() + h, n := decodeBlockHandle(v) + if n == 0 || n != len(v) { + i.err = errors.New("leveldb/table: corrupt index entry") + return false + } + k, err := i.reader.readBlock(h) + if err != nil { + i.err = err + return false + } + // Look for the key inside that block. + data, err := k.seek(i.reader.comparer, key) + if err != nil { + i.err = err + return false + } + i.data = data + return true +} + +// Next implements Iterator.Next, as documented in the leveldb/db package. +func (i *tableIter) Next() bool { + if i.data == nil { + return false + } + for { + if i.data.Next() { + return true + } + if i.data.err != nil { + i.err = i.data.err + break + } + if !i.nextBlock(nil) { + break + } + } + i.Close() + return false +} + +// Key implements Iterator.Key, as documented in the leveldb/db package. +func (i *tableIter) Key() []byte { + if i.data == nil { + return nil + } + return i.data.Key() +} + +// Value implements Iterator.Value, as documented in the leveldb/db package. +func (i *tableIter) Value() []byte { + if i.data == nil { + return nil + } + return i.data.Value() +} + +// Close implements Iterator.Close, as documented in the leveldb/db package. +func (i *tableIter) Close() error { + i.data = nil + return i.err +} + +// Reader is a table reader. It implements the DB interface, as documented +// in the leveldb/db package. +type Reader struct { + file File + err error + index block + comparer db.Comparer + verifyChecksums bool + // TODO: add a (goroutine-safe) LRU block cache. +} + +// Reader implements the db.DB interface. +var _ db.DB = (*Reader)(nil) + +// Close implements DB.Close, as documented in the leveldb/db package. +func (r *Reader) Close() error { + if r.err != nil { + if r.file != nil { + r.file.Close() + r.file = nil + } + return r.err + } + if r.file != nil { + r.err = r.file.Close() + r.file = nil + if r.err != nil { + return r.err + } + } + // Make any future calls to Get, Find or Close return an error. + r.err = errors.New("leveldb/table: reader is closed") + return nil +} + +// Get implements DB.Get, as documented in the leveldb/db package. +func (r *Reader) Get(key []byte, o *db.ReadOptions) (value []byte, err error) { + if r.err != nil { + return nil, r.err + } + i := r.Find(key, o) + if !i.Next() || !bytes.Equal(key, i.Key()) { + err := i.Close() + if err == nil { + err = db.ErrNotFound + } + return nil, err + } + return i.Value(), i.Close() +} + +// Set is provided to implement the DB interface, but returns an error, as a +// Reader cannot write to a table. +func (r *Reader) Set(key, value []byte, o *db.WriteOptions) error { + return errors.New("leveldb/table: cannot Set into a read-only table") +} + +// Delete is provided to implement the DB interface, but returns an error, as a +// Reader cannot write to a table. +func (r *Reader) Delete(key []byte, o *db.WriteOptions) error { + return errors.New("leveldb/table: cannot Delete from a read-only table") +} + +// Find implements DB.Find, as documented in the leveldb/db package. +func (r *Reader) Find(key []byte, o *db.ReadOptions) db.Iterator { + if r.err != nil { + return &tableIter{err: r.err} + } + index, err := r.index.seek(r.comparer, key) + if err != nil { + return &tableIter{err: err} + } + i := &tableIter{ + reader: r, + index: index, + } + i.nextBlock(key) + return i +} + +// readBlock reads and decompresses a block from disk into memory. +func (r *Reader) readBlock(bh blockHandle) (block, error) { + b := make([]byte, bh.length+blockTrailerLen) + if _, err := r.file.ReadAt(b, int64(bh.offset)); err != nil { + return nil, err + } + if r.verifyChecksums { + checksum0 := binary.LittleEndian.Uint32(b[bh.length+1:]) + checksum1 := crc.New(b[:bh.length+1]).Value() + if checksum0 != checksum1 { + return nil, errors.New("leveldb/table: invalid table (checksum mismatch)") + } + } + switch b[bh.length] { + case noCompressionBlockType: + return b[:bh.length], nil + case snappyCompressionBlockType: + b, err := snappy.Decode(nil, b[:bh.length]) + if err != nil { + return nil, err + } + return b, nil + } + return nil, fmt.Errorf("leveldb/table: unknown block compression: %d", b[bh.length]) +} + +// TODO(nigeltao): move the File interface to the standard package library? +// Package http already defines something similar. + +// File holds the raw bytes for a table. +type File interface { + io.Closer + io.ReaderAt + io.Writer + Stat() (os.FileInfo, error) +} + +// NewReader returns a new table reader for the file. Closing the reader will +// close the file. +func NewReader(f File, o *db.Options) *Reader { + r := &Reader{ + file: f, + comparer: o.GetComparer(), + verifyChecksums: o.GetVerifyChecksums(), + } + if f == nil { + r.err = errors.New("leveldb/table: nil file") + return r + } + stat, err := f.Stat() + if err != nil { + r.err = fmt.Errorf("leveldb/table: invalid table (could not stat file): %v", err) + return r + } + var footer [footerLen]byte + if stat.Size() < int64(len(footer)) { + r.err = errors.New("leveldb/table: invalid table (file size is too small)") + return r + } + _, err = f.ReadAt(footer[:], stat.Size()-int64(len(footer))) + if err != nil && err != io.EOF { + r.err = fmt.Errorf("leveldb/table: invalid table (could not read footer): %v", err) + return r + } + if string(footer[footerLen-len(magic):footerLen]) != magic { + r.err = errors.New("leveldb/table: invalid table (bad magic number)") + return r + } + // Ignore the metaindex. + _, n := decodeBlockHandle(footer[:]) + if n == 0 { + r.err = errors.New("leveldb/table: invalid table (bad metaindex block handle)") + return r + } + // Read the index into memory. + indexBH, n := decodeBlockHandle(footer[n:]) + if n == 0 { + r.err = errors.New("leveldb/table: invalid table (bad index block handle)") + return r + } + r.index, r.err = r.readBlock(indexBH) + return r +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/table.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/table.go new file mode 100644 index 00000000..0beb6ab3 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/table.go @@ -0,0 +1,137 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package table implements readers and writers of leveldb tables. + +Tables are either opened for reading or created for writing but not both. + +A reader can create iterators, which yield all key/value pairs whose keys +are 'greater than or equal' to a starting key. There may be multiple key/ +value pairs that have the same key. + +A reader can be used concurrently. Multiple goroutines can call Find +concurrently, and each iterator can run concurrently with other iterators. +However, any particular iterator should not be used concurrently, and +iterators should not be used once a reader is closed. + +A writer writes key/value pairs in increasing key order, and cannot be used +concurrently. A table cannot be read until the writer has finished. + +Readers and writers can be created with various options. Passing a nil +Options pointer is valid and means to use the default values. + +One such option is to define the 'less than' ordering for keys. The default +Comparer uses the natural ordering consistent with bytes.Compare. The same +ordering should be used for reading and writing a table. + +To return the value for a key: + + r := table.NewReader(file, options) + defer r.Close() + return r.Get(key) + +To count the number of entries in a table: + + i, n := r.Find(nil), 0 + for i.Next() { + n++ + } + if err := i.Close(); err != nil { + return 0, err + } + return n, nil + +To write a table with three entries: + + w := table.NewWriter(file, options) + if err := w.Set([]byte("apple"), []byte("red")); err != nil { + w.Close() + return err + } + if err := w.Set([]byte("banana"), []byte("yellow")); err != nil { + w.Close() + return err + } + if err := w.Set([]byte("cherry"), []byte("red")); err != nil { + w.Close() + return err + } + return w.Close() +*/ +package table + +/* +The table file format looks like: + + +[data block 0] +[data block 1] +... +[data block N-1] +[meta block 0] +[meta block 1] +... +[meta block K-1] +[metaindex block] +[index block] +[footer] + + +Each block consists of some data and a 5 byte trailer: a 1 byte block type and +a 4 byte checksum of the compressed data. The block type gives the per-block +compression used; each block is compressed independently. The checksum +algorithm is described in the leveldb/crc package. + +The decompressed block data consists of a sequence of key/value entries +followed by a trailer. Each key is encoded as a shared prefix length and a +remainder string. For example, if two adjacent keys are "tweedledee" and +"tweedledum", then the second key would be encoded as {8, "um"}. The shared +prefix length is varint encoded. The remainder string and the value are +encoded as a varint-encoded length followed by the literal contents. To +continue the example, suppose that the key "tweedledum" mapped to the value +"socks". The encoded key/value entry would be: "\x08\x02\x05umsocks". + +Every block has a restart interval I. Every I'th key/value entry in that block +is called a restart point, and shares no key prefix with the previous entry. +Continuing the example above, if the key after "tweedledum" was "two", but was +part of a restart point, then that key would be encoded as {0, "two"} instead +of {2, "o"}. If a block has P restart points, then the block trailer consists +of (P+1)*4 bytes: (P+1) little-endian uint32 values. The first P of these +uint32 values are the block offsets of each restart point. The final uint32 +value is P itself. Thus, when seeking for a particular key, one can use binary +search to find the largest restart point whose key is <= the key sought. + +An index block is a block with N key/value entries. The i'th value is the +encoded block handle of the i'th data block. The i'th key is a separator for +i < N-1, and a successor for i == N-1. The separator between blocks i and i+1 +is a key that is >= every key in block i and is < every key i block i+1. The +successor for the final block is a key that is >= every key in block N-1. The +index block restart interval is 1: every entry is a restart point. + +The table footer is exactly 48 bytes long: + - the block handle for the metaindex block, + - the block handle for the index block, + - padding to take the two items above up to 40 bytes, + - an 8-byte magic string. + +A block handle is an offset and a length; the length does not include the 5 +byte trailer. Both numbers are varint-encoded, with no padding between the two +values. The maximum size of an encoded block handle is therefore 20 bytes. +*/ + +const ( + blockTrailerLen = 5 + footerLen = 48 + + magic = "\x57\xfb\x80\x8b\x24\x75\x47\xdb" + + // The block type gives the per-block compression format. + // These constants are part of the file format and should not be changed. + // They are different from the db.Compression constants because the latter + // are designed so that the zero value of the db.Compression type means to + // use the default compression (which is snappy). + noCompressionBlockType = 0 + snappyCompressionBlockType = 1 +) diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/table_test.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/table_test.go new file mode 100644 index 00000000..71925707 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/table_test.go @@ -0,0 +1,279 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package table + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "sort" + "strings" + "testing" + "time" + + "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/db" +) + +type memFile []byte + +func (f *memFile) Close() error { + return nil +} + +func (f *memFile) ReadAt(p []byte, off int64) (int, error) { + return copy(p, (*f)[off:]), nil +} + +func (f *memFile) Stat() (os.FileInfo, error) { + return f, nil +} + +func (f *memFile) Write(p []byte) (int, error) { + *f = append(*f, p...) + return len(p), nil +} + +func (f *memFile) Size() int64 { + return int64(len(*f)) +} + +func (f *memFile) Sys() interface{} { + return nil +} + +func (f *memFile) IsDir() bool { + return false +} + +func (f *memFile) ModTime() time.Time { + return time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) +} + +func (f *memFile) Mode() os.FileMode { + return os.FileMode(0755) +} + +func (f *memFile) Name() string { + return "testdata" +} + +var wordCount = map[string]string{} + +func init() { + f, err := os.Open("../../testdata/h.txt") + if err != nil { + panic(err) + } + defer f.Close() + r := bufio.NewReader(f) + for { + s, err := r.ReadBytes('\n') + if err == io.EOF { + break + } + if err != nil { + panic(err) + } + k := strings.TrimSpace(string(s[8:])) + v := strings.TrimSpace(string(s[:8])) + wordCount[k] = v + } + if len(wordCount) != 1710 { + panic(fmt.Sprintf("h.txt entry count: got %d, want %d", len(wordCount), 1710)) + } +} + +func check(f File) error { + r := NewReader(f, &db.Options{ + VerifyChecksums: true, + }) + // Check that each key/value pair in wordCount is also in the table. + for k, v := range wordCount { + // Check using Get. + if v1, err := r.Get([]byte(k), nil); string(v1) != string(v) || err != nil { + return fmt.Errorf("Get %q: got (%q, %v), want (%q, %v)", k, v1, err, v, error(nil)) + } + + // Check using Find. + i := r.Find([]byte(k), nil) + if !i.Next() || string(i.Key()) != k { + return fmt.Errorf("Find %q: key was not in the table", k) + } + if string(i.Value()) != v { + return fmt.Errorf("Find %q: got value %q, want %q", k, i.Value(), v) + } + if err := i.Close(); err != nil { + return err + } + } + + // Check that nonsense words are not in the table. + var nonsenseWords = []string{ + "", + "\x00", + "kwyjibo", + "\xff", + } + for _, s := range nonsenseWords { + // Check using Get. + if _, err := r.Get([]byte(s), nil); err != db.ErrNotFound { + return fmt.Errorf("Get %q: got %v, want ErrNotFound", s, err) + } + + // Check using Find. + i := r.Find([]byte(s), nil) + if i.Next() && s == string(i.Key()) { + return fmt.Errorf("Find %q: unexpectedly found key in the table", s) + } + if err := i.Close(); err != nil { + return err + } + } + + // Check that the number of keys >= a given start key matches the expected number. + var countTests = []struct { + count int + start string + }{ + // cat h.txt | cut -c 9- | wc -l gives 1710. + {1710, ""}, + // cat h.txt | cut -c 9- | grep -v "^[a-b]" | wc -l gives 1522. + {1522, "c"}, + // cat h.txt | cut -c 9- | grep -v "^[a-j]" | wc -l gives 940. + {940, "k"}, + // cat h.txt | cut -c 9- | grep -v "^[a-x]" | wc -l gives 12. + {12, "y"}, + // cat h.txt | cut -c 9- | grep -v "^[a-z]" | wc -l gives 0. + {0, "~"}, + } + for _, ct := range countTests { + n, i := 0, r.Find([]byte(ct.start), nil) + for i.Next() { + n++ + } + if err := i.Close(); err != nil { + return err + } + if n != ct.count { + return fmt.Errorf("count %q: got %d, want %d", ct.start, n, ct.count) + } + } + + return r.Close() +} + +func build(compression db.Compression) (*memFile, error) { + // Create a sorted list of wordCount's keys. + keys := make([]string, len(wordCount)) + i := 0 + for k := range wordCount { + keys[i] = k + i++ + } + sort.Strings(keys) + + // Write the key/value pairs to a new table, in increasing key order. + f := new(memFile) + w := NewWriter(f, &db.Options{ + Compression: compression, + }) + for _, k := range keys { + v := wordCount[k] + if err := w.Set([]byte(k), []byte(v), nil); err != nil { + return nil, err + } + } + if err := w.Close(); err != nil { + return nil, err + } + return f, nil +} + +func TestReader(t *testing.T) { + // Check that we can read a pre-made table. + f, err := os.Open("../../testdata/h.sst") + if err != nil { + t.Fatal(err) + } + err = check(f) + if err != nil { + t.Fatal(err) + } +} + +func TestWriter(t *testing.T) { + // Check that we can read a freshly made table. + f, err := build(db.DefaultCompression) + if err != nil { + t.Fatal(err) + } + err = check(f) + if err != nil { + t.Fatal(err) + } +} + +func TestNoCompressionOutput(t *testing.T) { + // Check that a freshly made NoCompression table is byte-for-byte equal + // to a pre-made table. + a, err := ioutil.ReadFile("../../testdata/h.no-compression.sst") + if err != nil { + t.Fatal(err) + } + b, err := build(db.NoCompression) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(a, []byte(*b)) { + t.Fatal("built table does not match pre-made table") + } +} + +func TestBlockIter(t *testing.T) { + // k is a block that maps three keys "apple", "apricot", "banana" to empty strings. + k := block([]byte("\x00\x05\x00apple\x02\x05\x00ricot\x00\x06\x00banana\x00\x00\x00\x00\x01\x00\x00\x00")) + var testcases = []struct { + index int + key string + }{ + {0, ""}, + {0, "a"}, + {0, "aaaaaaaaaaaaaaa"}, + {0, "app"}, + {0, "apple"}, + {1, "appliance"}, + {1, "apricos"}, + {1, "apricot"}, + {2, "azzzzzzzzzzzzzz"}, + {2, "b"}, + {2, "banan"}, + {2, "banana"}, + {3, "banana\x00"}, + {3, "c"}, + } + for _, tc := range testcases { + i, err := k.seek(db.DefaultComparer, []byte(tc.key)) + if err != nil { + t.Fatal(err) + } + for j, kWant := range []string{"apple", "apricot", "banana"}[tc.index:] { + if !i.Next() { + t.Fatalf("key=%q, index=%d, j=%d: Next got false, want true", tc.key, tc.index, j) + } + if kGot := string(i.Key()); kGot != kWant { + t.Fatalf("key=%q, index=%d, j=%d: got %q, want %q", tc.key, tc.index, j, kGot, kWant) + } + } + if i.Next() { + t.Fatalf("key=%q, index=%d: Next got true, want false", tc.key, tc.index) + } + if err := i.Close(); err != nil { + t.Fatalf("key=%q, index=%d: got err=%v", tc.key, tc.index, err) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/writer.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/writer.go new file mode 100644 index 00000000..d105991b --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/leveldb/table/writer.go @@ -0,0 +1,309 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package table + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + + "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/crc" + "camlistore.org/third_party/code.google.com/p/leveldb-go/leveldb/db" + "camlistore.org/third_party/code.google.com/p/snappy-go/snappy" +) + +// indexEntry is a block handle and the length of the separator key. +type indexEntry struct { + bh blockHandle + keyLen int +} + +// Writer is a table writer. It implements the DB interface, as documented +// in the leveldb/db package. +type Writer struct { + writer io.Writer + bufWriter *bufio.Writer + closer io.Closer + err error + // The next four fields are copied from a db.Options. + blockRestartInterval int + blockSize int + cmp db.Comparer + compression db.Compression + // A table is a series of blocks and a block's index entry contains a + // separator key between one block and the next. Thus, a finished block + // cannot be written until the first key in the next block is seen. + // pendingBH is the blockHandle of a finished block that is waiting for + // the next call to Set. If the writer is not in this state, pendingBH + // is zero. + pendingBH blockHandle + // offset is the offset (relative to the table start) of the next block + // to be written. + offset uint64 + // prevKey is a copy of the key most recently passed to Set. + prevKey []byte + // indexKeys and indexEntries hold the separator keys between each block + // and the successor key for the final block. indexKeys contains the key's + // bytes concatenated together. The keyLen field of each indexEntries + // element is the length of the respective separator key. + indexKeys []byte + indexEntries []indexEntry + // The next three fields hold data for the current block: + // - buf is the accumulated uncompressed bytes, + // - nEntries is the number of entries, + // - restarts are the offsets (relative to the block start) of each + // restart point. + buf bytes.Buffer + nEntries int + restarts []uint32 + // compressedBuf is the destination buffer for snappy compression. It is + // re-used over the lifetime of the writer, avoiding the allocation of a + // temporary buffer for each block. + compressedBuf []byte + // tmp is a scratch buffer, large enough to hold either footerLen bytes, + // blockTrailerLen bytes, or (5 * binary.MaxVarintLen64) bytes. + tmp [50]byte +} + +// Writer implements the db.DB interface. +var _ db.DB = (*Writer)(nil) + +// Get is provided to implement the DB interface, but returns an error, as a +// Writer cannot read from a table. +func (w *Writer) Get(key []byte, o *db.ReadOptions) ([]byte, error) { + return nil, errors.New("leveldb/table: cannot Get from a write-only table") +} + +// Delete is provided to implement the DB interface, but returns an error, as a +// Writer can only append key/value pairs. +func (w *Writer) Delete(key []byte, o *db.WriteOptions) error { + return errors.New("leveldb/table: cannot Delete from a table") +} + +// Find is provided to implement the DB interface, but returns an error, as a +// Writer cannot read from a table. +func (w *Writer) Find(key []byte, o *db.ReadOptions) db.Iterator { + return &tableIter{ + err: errors.New("leveldb/table: cannot Find from a write-only table"), + } +} + +// Set implements DB.Set, as documented in the leveldb/db package. For a given +// Writer, the keys passed to Set must be in increasing order. +func (w *Writer) Set(key, value []byte, o *db.WriteOptions) error { + if w.err != nil { + return w.err + } + if w.cmp.Compare(w.prevKey, key) >= 0 { + w.err = fmt.Errorf("leveldb/table: Set called in non-increasing key order: %q, %q", w.prevKey, key) + return w.err + } + w.flushPendingBH(key) + w.append(key, value, w.nEntries%w.blockRestartInterval == 0) + // If the estimated block size is sufficiently large, finish the current block. + if w.buf.Len()+4*(len(w.restarts)+1) >= w.blockSize { + bh, err := w.finishBlock() + if err != nil { + w.err = err + return w.err + } + w.pendingBH = bh + } + return nil +} + +// flushPendingBH adds any pending block handle to the index entries. +func (w *Writer) flushPendingBH(key []byte) { + if w.pendingBH.length == 0 { + // A valid blockHandle must be non-zero. + // In particular, it must have a non-zero length. + return + } + n0 := len(w.indexKeys) + w.indexKeys = w.cmp.AppendSeparator(w.indexKeys, w.prevKey, key) + n1 := len(w.indexKeys) + w.indexEntries = append(w.indexEntries, indexEntry{w.pendingBH, n1 - n0}) + w.pendingBH = blockHandle{} +} + +// append appends a key/value pair, which may also be a restart point. +func (w *Writer) append(key, value []byte, restart bool) { + nShared := 0 + if restart { + w.restarts = append(w.restarts, uint32(w.buf.Len())) + } else { + nShared = db.SharedPrefixLen(w.prevKey, key) + } + w.prevKey = append(w.prevKey[:0], key...) + w.nEntries++ + n := binary.PutUvarint(w.tmp[0:], uint64(nShared)) + n += binary.PutUvarint(w.tmp[n:], uint64(len(key)-nShared)) + n += binary.PutUvarint(w.tmp[n:], uint64(len(value))) + w.buf.Write(w.tmp[:n]) + w.buf.Write(key[nShared:]) + w.buf.Write(value) +} + +// finishBlock finishes the current block and returns its block handle, which is +// its offset and length in the table. +func (w *Writer) finishBlock() (blockHandle, error) { + // Write the restart points to the buffer. + if w.nEntries == 0 { + // Every block must have at least one restart point. + w.restarts = w.restarts[:1] + w.restarts[0] = 0 + } + tmp4 := w.tmp[:4] + for _, x := range w.restarts { + binary.LittleEndian.PutUint32(tmp4, x) + w.buf.Write(tmp4) + } + binary.LittleEndian.PutUint32(tmp4, uint32(len(w.restarts))) + w.buf.Write(tmp4) + + // Compress the buffer, discarding the result if the improvement + // isn't at least 12.5%. + b := w.buf.Bytes() + w.tmp[0] = noCompressionBlockType + if w.compression == db.SnappyCompression { + compressed, err := snappy.Encode(w.compressedBuf, b) + if err != nil { + return blockHandle{}, err + } + w.compressedBuf = compressed[:cap(compressed)] + if len(compressed) < len(b)-len(b)/8 { + w.tmp[0] = snappyCompressionBlockType + b = compressed + } + } + + // Calculate the checksum. + checksum := crc.New(b).Update(w.tmp[:1]).Value() + binary.LittleEndian.PutUint32(w.tmp[1:5], checksum) + + // Write the bytes to the file. + if _, err := w.writer.Write(b); err != nil { + return blockHandle{}, err + } + if _, err := w.writer.Write(w.tmp[:5]); err != nil { + return blockHandle{}, err + } + bh := blockHandle{w.offset, uint64(len(b))} + w.offset += uint64(len(b)) + blockTrailerLen + + // Reset the per-block state. + w.buf.Reset() + w.nEntries = 0 + w.restarts = w.restarts[:0] + return bh, nil +} + +// Close implements DB.Close, as documented in the leveldb/db package. +func (w *Writer) Close() (err error) { + defer func() { + if w.closer == nil { + return + } + err1 := w.closer.Close() + if err == nil { + err = err1 + } + w.closer = nil + }() + if w.err != nil { + return w.err + } + + // Finish the last data block, or force an empty data block if there + // aren't any data blocks at all. + if w.nEntries > 0 || len(w.indexEntries) == 0 { + bh, err := w.finishBlock() + if err != nil { + w.err = err + return w.err + } + w.pendingBH = bh + w.flushPendingBH(nil) + } + + // Write the (empty) metaindex block. + metaindexBlockHandle, err := w.finishBlock() + if err != nil { + w.err = err + return w.err + } + + // Write the index block. + // writer.append uses w.tmp[:3*binary.MaxVarintLen64]. + i0, tmp := 0, w.tmp[3*binary.MaxVarintLen64:5*binary.MaxVarintLen64] + for _, ie := range w.indexEntries { + n := encodeBlockHandle(tmp, ie.bh) + i1 := i0 + ie.keyLen + w.append(w.indexKeys[i0:i1], tmp[:n], true) + i0 = i1 + } + indexBlockHandle, err := w.finishBlock() + if err != nil { + w.err = err + return w.err + } + + // Write the table footer. + footer := w.tmp[:footerLen] + for i := range footer { + footer[i] = 0 + } + n := encodeBlockHandle(footer, metaindexBlockHandle) + encodeBlockHandle(footer[n:], indexBlockHandle) + copy(footer[footerLen-len(magic):], magic) + if _, err := w.writer.Write(footer); err != nil { + w.err = err + return w.err + } + + // Flush the buffer. + if w.bufWriter != nil { + if err := w.bufWriter.Flush(); err != nil { + w.err = err + return err + } + } + + // Make any future calls to Set or Close return an error. + w.err = errors.New("leveldb/table: writer is closed") + return nil +} + +// NewWriter returns a new table writer for the file. Closing the writer will +// close the file. +func NewWriter(f File, o *db.Options) *Writer { + w := &Writer{ + closer: f, + blockRestartInterval: o.GetBlockRestartInterval(), + blockSize: o.GetBlockSize(), + cmp: o.GetComparer(), + compression: o.GetCompression(), + prevKey: make([]byte, 0, 256), + restarts: make([]uint32, 0, 256), + } + if f == nil { + w.err = errors.New("leveldb/table: nil file") + return w + } + // If f does not have a Flush method, do our own buffering. + type flusher interface { + Flush() error + } + if _, ok := f.(flusher); ok { + w.writer = f + } else { + w.bufWriter = bufio.NewWriter(f) + w.writer = w.bufWriter + } + return w +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/lib/codereview/codereview.cfg b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/lib/codereview/codereview.cfg new file mode 100644 index 00000000..93b55c0a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/lib/codereview/codereview.cfg @@ -0,0 +1 @@ +defaultcc: golang-dev@googlegroups.com diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/000003.log b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/000003.log new file mode 100644 index 00000000..e69de29b diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/CURRENT b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/CURRENT new file mode 100644 index 00000000..1a848522 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/CURRENT @@ -0,0 +1 @@ +MANIFEST-000002 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/LOCK b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/LOCK new file mode 100644 index 00000000..e69de29b diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/LOG b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/LOG new file mode 100644 index 00000000..ed125ed2 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/LOG @@ -0,0 +1 @@ +2012/02/03-18:31:28.752463 7ff183bca740 Delete type=3 #1 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/MANIFEST-000002 b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-1/MANIFEST-000002 new file mode 100644 index 0000000000000000000000000000000000000000..dbf594eac71015b1981f1a3bae1a920c98281dcd GIT binary patch literal 65536 zcmeIuu?>Pi006*$q0kkGGdP6qV2|Mo5)1~3gc(?Y(jlmvOFH%-cO%?=UH<0~W6`8f zYO2TOI`(O(-|5yqUu9SJZTFq0-!a5Pb_!XZ2Mic6V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA HV2=YEsSXfR literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/000003.log b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/000003.log new file mode 100644 index 0000000000000000000000000000000000000000..147db71a52d32c61cabc3c20d8b57b4d7e507260 GIT binary patch literal 65536 zcmeIuF%AJi6oBCwy9%WOz0P425}m|`WJSXy8zRv-gia~0;8^Zpg~Gak=9}ha^1r;| zTc2jvUWhTg-)qKfl~R_rWf(s0b!#=+a#3ZwLz-^p=Rv*t(~j-kx+=@@ttk2-=8alk z%~xq(bw-m*H^lZ=3|W2l^EU_(AV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk w1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK;Rz&Po=&UX#fBK literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/CURRENT b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/CURRENT new file mode 100644 index 00000000..1a848522 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/CURRENT @@ -0,0 +1 @@ +MANIFEST-000002 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/LOCK b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/LOCK new file mode 100644 index 00000000..e69de29b diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/LOG b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/LOG new file mode 100644 index 00000000..7fd79744 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/LOG @@ -0,0 +1 @@ +2012/02/03-18:32:06.283846 7fa954064740 Delete type=3 #1 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/MANIFEST-000002 b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-2/MANIFEST-000002 new file mode 100644 index 0000000000000000000000000000000000000000..dbf594eac71015b1981f1a3bae1a920c98281dcd GIT binary patch literal 65536 zcmeIuu?>Pi006*$q0kkGGdP6qV2|Mo5)1~3gc(?Y(jlmvOFH%-cO%?=UH<0~W6`8f zYO2TOI`(O(-|5yqUu9SJZTFq0-!a5Pb_!XZ2Mic6V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA HV2=YEsSXfR literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/000005.sst b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/000005.sst new file mode 100644 index 0000000000000000000000000000000000000000..f4a60d282db41844ba8b19c82c038b9543bb7c05 GIT binary patch literal 165 zcmWGhVBls*N-SbvWng6#VCG$Pa1i(9h!O({ P5CY-98@g3W-ERW`jy)aL literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/000006.log b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/000006.log new file mode 100644 index 00000000..e69de29b diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/CURRENT b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/CURRENT new file mode 100644 index 00000000..cacca757 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/CURRENT @@ -0,0 +1 @@ +MANIFEST-000004 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/LOCK b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/LOCK new file mode 100644 index 00000000..e69de29b diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/LOG b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/LOG new file mode 100644 index 00000000..9b8ff68d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/LOG @@ -0,0 +1,5 @@ +2012/02/03-18:32:34.790995 7f8f2d339740 Recovering log #3 +2012/02/03-18:32:34.791037 7f8f2d339740 Level-0 table #5: started +2012/02/03-18:32:34.850300 7f8f2d339740 Level-0 table #5: 165 bytes OK +2012/02/03-18:32:34.917482 7f8f2d339740 Delete type=3 #2 +2012/02/03-18:32:34.917520 7f8f2d339740 Delete type=0 #3 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/LOG.old b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/LOG.old new file mode 100644 index 00000000..de536958 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/LOG.old @@ -0,0 +1 @@ +2012/02/03-18:32:34.790486 7f8f2d339740 Delete type=3 #1 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/MANIFEST-000004 b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-3/MANIFEST-000004 new file mode 100644 index 0000000000000000000000000000000000000000..a1fd797b44c56642a5f50740cf1898d9562c60d0 GIT binary patch literal 65536 zcmeIuK?(s;0LJn6Fec5`O18ER!4+gblcy9tjfot{9pol%!cK7t_4^k8zV&bYzQ3j+ z#>qaN(|*&;7uPObcBiz=hhyE=U2eyZ)pHhNZ#WA5a!{0^xW%fe+fan>s%_3O{=O0* zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U dAV7cs0RjXF5FkK+009C72oNAZfWV&$d;l3!6Ey$; literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-4/000005.sst b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-4/000005.sst new file mode 100644 index 0000000000000000000000000000000000000000..f4a60d282db41844ba8b19c82c038b9543bb7c05 GIT binary patch literal 165 zcmWGhVBls*N-SbvWng6#VCG$Pa1i(9h!O({ P5CY-98@g3W-ERW`jy)aL literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-4/000006.log b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/db-stage-4/000006.log new file mode 100644 index 0000000000000000000000000000000000000000..1638b770622f9269e859c1132723d61162ae975d GIT binary patch literal 65536 zcmeIuF$%&!6h+Y)CTLn4%ivbTl^94MRggFmY}|vjWLepZodGSf1>yYW@wwf3qaN(|*&;7uPObcBiz=hhyE=U2eyZ)pHhNZ#WA5a!{0^xW%fe+fan>s%_3O{=O0* zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U dAV7cs0RjXF5FkK+009C72oNAZfWV&$d;l3!6Ey$; literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/h.no-compression.sst b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/h.no-compression.sst new file mode 100644 index 0000000000000000000000000000000000000000..f9c8f09253210e282d31c7d4bdff6e052b6621ad GIT binary patch literal 13105 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 WKJK^w|KG3v*Kd8{qsO2Br~d`4un)Wd literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/h.sst b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/h.sst new file mode 100644 index 0000000000000000000000000000000000000000..2e7a9d60c649a66f98637210f87975aa808d8497 GIT binary patch literal 11028 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/h.txt b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/h.txt new file mode 100644 index 00000000..ed8f750a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/h.txt @@ -0,0 +1,1710 @@ + 97 a + 2 aboard + 2 about + 1 above + 1 abroad + 1 absurd + 1 abused + 1 accord + 1 account + 1 achievements + 1 acquaint + 5 act + 1 action + 1 actions + 1 addition + 1 address + 4 adieu + 1 admiration + 1 adoption + 1 adulterate + 1 advantage + 1 advice + 2 affair + 3 affection + 1 after + 1 afternoon + 13 again + 5 against + 2 ah + 5 air + 1 airs + 1 alas + 36 all + 1 alleys + 1 allow + 4 almost + 4 alone + 2 along + 1 already + 1 always + 9 am + 1 amazed + 1 ambiguous + 1 ambitious + 13 an + 227 and + 1 angel + 1 angels + 1 anger + 1 angry + 1 another + 4 answer + 1 antic + 6 any + 1 apparel + 2 apparition + 4 appear + 1 appears + 1 appetite + 1 approve + 1 apt + 21 are + 2 arm + 2 armed + 1 armour + 2 arms + 1 arrant + 6 art + 1 artery + 1 article + 1 articles + 56 as + 1 aside + 1 asking + 1 assail + 1 assistant + 2 assume + 18 at + 1 attendants + 1 attent + 1 attribute + 1 audience + 2 aught + 1 auspicious + 1 avoid + 1 avouch + 1 awake + 7 away + 2 awhile + 7 ay + 1 baby + 1 back + 1 baked + 1 bark + 1 barr + 1 base + 1 baser + 1 bawds + 42 be + 5 bear + 1 beard + 1 bearers + 1 bears + 2 beast + 1 beating + 1 beauty + 1 beaver + 2 beckons + 4 bed + 4 been + 1 beetles + 1 befitted + 6 before + 1 beg + 1 beguile + 1 behold + 1 behoves + 4 being + 1 belief + 6 believe + 1 bell + 2 bend + 5 beneath + 1 benefit + 30 bernardo + 2 beseech + 1 besmirch + 5 best + 1 beteem + 1 bethought + 2 better + 2 between + 2 beware + 1 beyond + 2 bid + 2 bird + 3 birth + 1 bites + 1 bitter + 1 black + 1 blast + 1 blastments + 1 blasts + 1 blazes + 1 blazon + 3 blessing + 7 blood + 1 blossoms + 1 blows + 1 bodes + 5 body + 1 bonds + 1 bones + 1 book + 1 books + 2 born + 1 borrower + 1 borrowing + 1 bosom + 3 both + 2 bound + 1 bounteous + 1 bow + 2 boy + 2 brain + 1 bray + 1 brazen + 1 breach + 3 break + 1 breaking + 1 breath + 1 breathing + 1 brief + 1 bring + 1 brokers + 6 brother + 1 brow + 1 bruit + 1 bulk + 1 buried + 2 burns + 1 burnt + 2 burst + 4 business + 58 but + 1 buttons + 1 buy + 31 by + 4 call + 1 calumnious + 2 came + 5 can + 1 canker + 2 cannon + 3 cannot + 1 canon + 1 canonized + 2 canst + 1 cap + 1 carefully + 1 carriage + 1 carrying + 1 carve + 3 cast + 2 castle + 1 catch + 1 cautel + 1 caution + 1 celebrated + 1 celestial + 1 cellarage + 2 censure + 1 cerements + 1 certain + 1 chances + 1 change + 1 character + 3 charge + 1 chariest + 1 charitable + 1 charm + 1 chaste + 1 cheer + 2 chief + 1 chiefest + 2 choice + 1 choose + 1 circumscribed + 2 circumstance + 1 clad + 8 claudius + 1 clearly + 1 clepe + 1 cliff + 1 climatures + 1 cloak + 2 clouds + 5 cock + 2 cold + 1 coldly + 1 colleagued + 1 colour + 1 combat + 1 combated + 1 combined + 17 come + 7 comes + 1 comest + 1 comfort + 1 coming + 1 command + 1 commandment + 2 commend + 1 commendable + 4 common + 1 compact + 1 competent + 1 complete + 1 complexion + 1 compulsatory + 1 comrade + 1 conceal + 1 condolement + 1 confess + 1 confine + 1 confined + 1 conqueror + 3 consent + 1 constantly + 1 contagious + 1 contracted + 1 contrive + 1 conveniently + 1 convoy + 1 copied + 4 cornelius + 1 coronation + 1 corruption + 2 corse + 1 costly + 1 couch + 2 could + 2 countenance + 1 country + 1 countrymen + 1 couple + 2 course + 1 courses + 1 court + 1 courteous + 1 courtier + 2 cousin + 1 covenant + 1 crack + 1 credent + 1 crescent + 2 crew + 1 cried + 1 cries + 1 crimes + 1 cross + 1 crowing + 2 crown + 1 crows + 1 crust + 1 curd + 2 cursed + 3 custom + 1 customary + 1 cut + 54 d + 1 daily + 1 dalliance + 1 damn + 2 damned + 3 dane + 1 danger + 1 dared + 1 dares + 2 daughter + 1 dawning + 8 day + 1 days + 7 dead + 4 dear + 2 dearest + 1 dearly + 6 death + 1 decline + 1 deed + 1 deeds + 1 deep + 1 defeated + 1 defect + 1 defend + 1 dejected + 1 delated + 1 delight + 2 deliver + 1 demonstrated + 13 denmark + 1 denote + 1 depart + 1 depends + 1 deprive + 1 design + 6 desire + 1 desperate + 1 desperation + 3 dew + 1 dews + 1 dexterity + 14 did + 1 didst + 1 die + 1 died + 1 diet + 1 dignity + 1 direct + 1 dirge + 1 disappointed + 1 disasters + 1 disclosed + 1 discourse + 1 discretion + 1 disjoint + 2 dispatch + 3 disposition + 1 distilled + 1 distilment + 1 distracted + 1 divide + 36 do + 3 does + 1 dole + 3 done + 1 doom + 1 doomsday + 7 doth + 2 double + 4 doubt + 1 doubtful + 7 down + 1 drains + 1 dram + 1 draughts + 1 draw + 1 draws + 1 dread + 1 dreaded + 2 dreadful + 1 dream + 1 dreamt + 1 drink + 1 drinks + 1 dropping + 1 droppings + 1 drum + 1 drunkards + 1 dull + 1 duller + 1 dulls + 2 dumb + 1 dust + 1 duties + 7 duty + 1 dwelling + 1 dye + 1 e + 5 each + 2 eager + 1 eale + 5 ear + 3 ears + 9 earth + 1 earthly + 2 ease + 1 east + 1 eastward + 1 eclipse + 1 edge + 2 effect + 1 eleven + 4 else + 2 elsinore + 1 embark + 1 empire + 1 emulate + 2 en + 1 encounter + 1 encumber + 1 end + 1 enemy + 1 enmity + 1 enough + 12 enter + 1 enterprise + 1 entertainment + 1 entrance + 1 entreated + 1 entreatments + 1 equal + 5 er + 4 ere + 1 ergrowth + 1 ermaster + 1 erring + 1 eruption + 1 erwhelm + 1 esteem + 1 et + 1 eternal + 1 eternity + 8 even + 1 events + 6 ever + 1 everlasting + 3 every + 1 exactly + 1 excellent + 8 exeunt + 6 exit + 2 express + 1 extinct + 1 extorted + 1 extravagant + 6 eye + 7 eyes + 2 face + 1 faded + 1 fail + 3 fair + 1 fairy + 4 faith + 1 falling + 1 false + 1 familiar + 1 fancy + 2 fantasy + 2 far + 2 fare + 8 farewell + 3 fashion + 2 fast + 1 fat + 2 fate + 1 fates + 28 father + 1 fathers + 1 fathoms + 4 fault + 2 favour + 9 fear + 1 fearful + 1 fed + 1 fee + 2 fell + 2 fellow + 3 few + 4 fie + 1 fierce + 3 figure + 1 filial + 2 find + 1 fingers + 4 fire + 1 fires + 1 first + 2 fit + 1 fits + 1 fitting + 2 fix + 1 flames + 1 flat + 2 flesh + 1 flood + 1 flourish + 1 flushing + 1 foe + 9 follow + 1 follows + 1 fond + 1 food + 1 fool + 1 fools + 1 foot + 45 for + 1 forbid + 1 forced + 1 foreign + 1 foreknowing + 1 foresaid + 1 forfeit + 1 forged + 1 forget + 4 form + 2 forms + 3 forth + 1 fortified + 6 fortinbras + 1 forts + 1 fortune + 1 forward + 1 fought + 6 foul + 1 frailty + 1 frame + 3 france + 10 francisco + 1 free + 1 freely + 1 freeze + 1 fretful + 3 friend + 1 friending + 5 friends + 21 from + 1 frown + 1 frowningly + 1 fruitful + 2 full + 3 funeral + 1 furnish + 4 further + 1 gaged + 2 gainst + 1 gait + 1 galled + 1 galls + 1 gape + 1 garbage + 1 garden + 1 gates + 1 gaudy + 1 general + 1 generous + 1 gentle + 5 gentlemen + 4 gertrude + 1 get + 26 ghost + 1 gibber + 3 gifts + 1 gins + 1 girl + 13 give + 4 given + 3 giving + 2 glad + 1 glimpses + 1 globe + 1 glow + 15 go + 1 goblin + 8 god + 3 goes + 1 going + 4 gone + 20 good + 1 goodly + 6 grace + 1 graces + 2 gracious + 1 grapple + 1 grave + 1 graves + 1 great + 1 greatness + 2 green + 1 greeting + 3 grief + 1 grizzled + 2 gross + 3 ground + 2 grow + 1 grown + 2 grows + 1 guard + 2 guilty + 1 ha + 2 habit + 13 had + 1 hail + 1 hair + 1 hallow + 100 hamlet + 5 hand + 4 hands + 2 hang + 1 hap + 1 happily + 1 harbingers + 2 hard + 1 hardy + 1 harrow + 1 harrows + 3 has + 4 hast + 7 haste + 1 hatch + 15 hath + 31 have + 1 havior + 34 he + 6 head + 1 headed + 1 headshake + 3 health + 9 hear + 4 heard + 1 hearing + 2 hears + 1 hearsed + 10 heart + 3 heartily + 1 hearts + 1 heat + 21 heaven + 1 heavens + 1 heavy + 1 hebenon + 1 height + 1 held + 3 hell + 2 help + 8 her + 1 heraldry + 1 hercules + 11 here + 1 hereafter + 3 herein + 1 hic + 1 hideous + 1 hies + 2 high + 1 higher + 1 hill + 2 hillo + 21 him + 3 himself + 57 his + 1 hither + 1 hitherto + 5 ho + 9 hold + 1 holding + 2 holds + 1 holla + 1 holy + 2 honest + 5 honour + 1 honourable + 1 hoops + 85 horatio + 4 horrible + 1 horridly + 1 host + 1 hot + 6 hour + 2 house + 7 how + 1 howsoever + 1 humbly + 1 hundred + 1 husbandry + 1 hyperion + 124 i + 1 ice + 22 if + 1 ignorance + 1 ii + 1 iii + 1 illume + 1 illusion + 1 image + 1 imagination + 1 immediate + 1 imminent + 1 immortal + 3 impart + 1 impartment + 1 impatient + 1 imperfections + 1 imperial + 1 impious + 1 implements + 1 implorators + 1 importing + 1 importuned + 1 importunity + 1 impotent + 1 impress + 118 in + 1 incest + 2 incestuous + 1 incorrect + 1 increase + 8 indeed + 1 infants + 1 infinite + 1 influence + 1 inform + 1 inheritance + 1 inky + 2 instant + 1 instrumental + 1 intent + 1 intents + 5 into + 1 inurn + 1 investments + 1 invites + 1 invulnerable + 1 inward + 62 is + 1 issue + 126 it + 1 its + 9 itself + 1 iv + 1 jaws + 1 jelly + 1 jocund + 2 joint + 1 jointress + 1 joy + 1 judgment + 1 juice + 1 julius + 1 jump + 3 keep + 1 keeps + 1 kept + 1 kettle + 1 key + 1 kin + 1 kind + 23 king + 1 kingdom + 1 knave + 1 knew + 1 knotted + 17 know + 2 known + 1 knows + 1 labourer + 1 laboursome + 1 lack + 1 lacks + 16 laertes + 2 land + 3 lands + 1 larger + 3 last + 1 lasting + 3 late + 2 law + 1 lawless + 1 lay + 1 lazar + 1 lead + 2 least + 8 leave + 1 leavens + 1 left + 1 leisure + 1 lend + 1 lender + 1 lends + 1 length + 1 leperous + 2 less + 1 lesson + 23 let + 1 lethe + 1 lets + 1 levies + 1 lewdness + 1 libertine + 1 lids + 1 liegemen + 1 lies + 7 life + 1 lifted + 1 light + 1 lightest + 23 like + 1 link + 1 lion + 1 lips + 1 liquid + 6 list + 1 lists + 3 little + 3 live + 1 livery + 1 lives + 18 ll + 1 lo + 1 loan + 1 loathsome + 1 lock + 1 locks + 1 lodge + 1 lofty + 4 long + 3 longer + 10 look + 2 looks + 1 loose + 60 lord + 1 lords + 1 lordship + 2 lose + 1 loses + 1 loss + 5 lost + 1 loud + 8 love + 5 loves + 2 loving + 2 lust + 1 luxury + 2 m + 4 madam + 8 made + 1 madness + 1 maid + 1 maiden + 2 main + 1 majestical + 1 majesty + 8 make + 2 makes + 2 making + 1 malicious + 11 man + 1 manner + 1 manners + 1 mantle + 2 many + 1 marble + 46 marcellus + 2 march + 2 mark + 3 marriage + 2 married + 1 marrow + 3 marry + 1 mart + 1 martial + 1 marvel + 1 matin + 1 matter + 19 may + 47 me + 2 mean + 2 means + 1 meats + 1 meditation + 3 meet + 1 meeting + 1 melt + 5 memory + 3 men + 2 mercy + 1 mere + 1 merely + 1 message + 1 met + 2 methinks + 1 methought + 1 mettle + 1 middle + 7 might + 1 mightiest + 1 milk + 6 mind + 6 mine + 1 ministers + 1 minute + 1 minutes + 1 mirth + 1 mock + 1 mockery + 1 moderate + 1 moiety + 1 moist + 2 mole + 1 moment + 3 month + 1 months + 1 moods + 2 moon + 19 more + 3 morn + 3 morning + 28 most + 1 mote + 5 mother + 1 motion + 2 motive + 1 mourn + 1 mourning + 1 mouse + 1 mouth + 1 moved + 9 much + 3 murder + 14 must + 126 my + 4 myself + 2 name + 1 nations + 2 native + 2 natural + 13 nature + 7 nay + 1 ne + 3 near + 1 necessaries + 1 need + 1 needful + 1 needs + 1 neither + 1 nemean + 1 nephew + 1 neptune + 1 nerve + 6 never + 1 new + 2 news + 22 night + 1 nighted + 1 nightly + 3 nights + 1 niobe + 1 nipping + 28 no + 1 nobility + 5 noble + 2 none + 14 nor + 5 norway + 80 not + 2 note + 2 nothing + 19 now + 30 o + 1 oath + 3 obey + 1 object + 1 obligation + 1 obsequious + 1 observance + 1 observant + 1 observation + 1 obstinate + 1 occasion + 1 odd + 176 of + 6 off + 2 offence + 1 offend + 1 offended + 2 offer + 7 oft + 4 old + 1 omen + 25 on + 8 once + 6 one + 1 oped + 1 open + 15 ophelia + 1 opinion + 1 opposed + 1 opposition + 1 oppress + 28 or + 2 orchard + 1 ordnance + 1 origin + 4 other + 45 our + 2 ourself + 1 ourselves + 8 out + 6 own + 1 ownself + 4 pale + 1 pales + 1 palm + 1 palmy + 1 pardon + 1 parle + 1 parley + 6 part + 6 particular + 1 partisan + 1 passeth + 1 passing + 1 past + 1 pastors + 1 path + 1 patrick + 1 pay + 1 pe + 2 peace + 1 peevish + 2 perchance + 1 perform + 1 perfume + 1 perhaps + 1 perilous + 1 permanent + 1 pernicious + 1 persever + 1 person + 1 personal + 1 persons + 1 perturbed + 1 pester + 1 petition + 1 petty + 1 philosophy + 3 phrase + 1 piece + 1 pin + 1 pioner + 1 pious + 1 pith + 1 pity + 3 place + 1 plain + 1 planets + 5 platform + 1 plausive + 2 play + 1 please + 1 pledge + 2 point + 1 polacks + 1 pole + 13 polonius + 1 ponderous + 1 pooh + 9 poor + 1 porches + 1 porpentine + 1 portentous + 1 possess + 1 posset + 3 post + 1 pour + 3 power + 7 pray + 1 prayers + 1 preceding + 1 precepts + 1 precurse + 1 preparations + 1 presence + 1 present + 1 pressures + 1 prey + 2 prick + 1 pride + 1 primrose + 1 primy + 1 prince + 1 prison + 1 private + 1 privy + 1 probation + 1 process + 1 proclaims + 2 prodigal + 1 prologue + 1 promise + 1 pronouncing + 1 prophetic + 1 proportions + 1 propose + 1 puff + 1 pure + 1 purged + 1 purpose + 1 purse + 1 pursuest + 2 put + 1 puts + 1 quarrel + 7 queen + 2 question + 1 questionable + 1 quicksilver + 1 quiet + 1 quietly + 1 quills + 1 radiant + 2 rank + 1 rankly + 1 rate + 1 ratified + 2 re + 1 reaches + 1 rear + 5 reason + 1 rebels + 1 reckless + 1 reckoning + 1 recks + 1 records + 1 recover + 1 red + 1 rede + 1 reels + 1 relief + 1 relieved + 1 remain + 6 remember + 1 remembrance + 1 remove + 1 removed + 1 render + 1 reply + 1 report + 1 request + 1 requite + 1 reserve + 1 resolutes + 1 resolve + 2 rest + 1 retrograde + 2 return + 1 reveal + 1 revel + 3 revenge + 1 revisit + 1 rhenish + 1 rich + 1 rid + 3 right + 1 rise + 1 rivals + 1 river + 1 roar + 1 romage + 1 roman + 1 rome + 2 room + 1 roots + 1 rotten + 1 roughly + 2 rouse + 2 royal + 1 ruled + 1 running + 1 russet + 39 s + 1 sable + 2 safety + 3 said + 1 sail + 1 saint + 1 salt + 5 same + 1 sanctified + 1 sate + 1 satyr + 1 saviour + 6 saw + 1 saws + 11 say + 1 saying + 3 says + 1 scale + 1 scandal + 1 scanter + 1 scapes + 1 scarcely + 5 scene + 1 scent + 1 scholar + 1 scholars + 1 school + 2 scope + 3 sea + 2 seal + 4 season + 1 seat + 1 second + 1 secrecy + 1 secret + 1 secrets + 2 secure + 1 seduce + 7 see + 1 seed + 1 seeing + 1 seek + 2 seem + 1 seeming + 3 seems + 8 seen + 1 seized + 1 select + 1 self + 1 sense + 1 sensible + 1 sent + 1 sepulchre + 1 serious + 2 serpent + 1 servant + 1 servants + 1 service + 4 set + 2 shake + 22 shall + 1 shalt + 1 shame + 1 shameful + 2 shape + 1 shapes + 1 shark + 6 she + 1 sheeted + 1 sheets + 1 shift + 1 shipwrights + 1 shoes + 2 shot + 6 should + 1 shoulder + 1 shouldst + 6 show + 2 shows + 1 shrewdly + 1 shrill + 1 shrunk + 2 sick + 1 side + 3 sight + 1 silence + 1 silver + 1 simple + 1 sin + 1 since + 1 sinews + 1 singeth + 3 sir + 1 sirs + 3 sister + 4 sit + 2 sits + 1 skirts + 1 slander + 1 slaughter + 1 slay + 1 sledded + 1 sleep + 3 sleeping + 2 slow + 2 smile + 1 smiles + 2 smiling + 1 smooth + 1 smote + 48 so + 1 soe + 2 soft + 2 soil + 1 soldier + 1 soldiers + 2 solemn + 1 solid + 13 some + 3 something + 1 sometime + 1 sometimes + 1 somewhat + 3 son + 1 songs + 1 sore + 3 sorrow + 1 sorry + 1 sort + 8 soul + 1 souls + 2 sound + 1 sounding + 1 source + 1 sovereignty + 27 speak + 1 speaking + 1 speech + 1 speed + 1 spend + 1 spheres + 8 spirit + 1 spirits + 1 spite + 1 spoke + 2 spring + 1 springes + 1 squeak + 4 st + 1 stale + 1 stalk + 1 stalks + 1 stamp + 5 stand + 1 stands + 3 star + 2 stars + 1 start + 1 started + 8 state + 1 stately + 1 station + 7 stay + 2 steel + 1 steep + 1 sterling + 1 stiffly + 8 still + 2 sting + 2 stir + 1 stirring + 1 stole + 1 stomach + 2 stood + 1 stop + 1 story + 6 strange + 1 stranger + 1 streets + 1 strict + 2 strike + 1 strokes + 1 strong + 2 struck + 1 stubbornness + 1 student + 1 stung + 3 subject + 1 substance + 10 such + 1 sudden + 1 suit + 3 suits + 1 sulphurous + 1 summit + 1 summons + 2 sun + 1 sunday + 1 suppliance + 1 supposal + 1 suppress + 1 sure + 1 surprised + 1 surrender + 1 survivor + 1 suspiration + 1 sustain + 1 swaggering + 10 swear + 1 sweaty + 1 sweep + 2 sweet + 2 swift + 1 swinish + 5 sword + 2 sworn + 18 t + 1 ta + 1 table + 2 tables + 1 taint + 10 take + 1 taken + 3 takes + 1 tale + 1 talk + 1 task + 1 tax + 2 teach + 2 tears + 9 tell + 1 temple + 1 tempt + 1 tenable + 1 tenantless + 1 tend + 2 tender + 3 tenders + 2 term + 2 terms + 1 tether + 1 tetter + 15 than + 2 thanks + 83 that + 1 thaw + 237 the + 23 thee + 10 their + 10 them + 1 theme + 15 then + 18 there + 4 therefore + 1 thereto + 13 these + 1 thews + 14 they + 1 thin + 3 thine + 6 thing + 3 things + 16 think + 1 thinking + 1 third + 67 this + 1 thorns + 1 thorny + 7 those + 28 thou + 10 though + 2 thought + 4 thoughts + 1 thrice + 2 thrift + 1 throat + 2 throne + 3 through + 1 throw + 1 thunder + 9 thus + 36 thy + 1 thyself + 4 till + 10 time + 1 times + 22 tis + 192 to + 1 toe + 7 together + 1 toils + 2 told + 4 tongue + 9 too + 1 top + 1 tormenting + 3 touching + 4 toward + 1 toy + 1 toys + 1 traduced + 1 tragedy + 1 trains + 1 traitorous + 1 trappings + 1 treads + 2 treasure + 1 tremble + 1 tried + 1 trifling + 1 triumph + 1 trivial + 1 trouble + 1 troubles + 2 truant + 5 true + 1 truepenny + 1 truly + 2 trumpet + 1 trumpets + 1 truncheon + 1 truster + 2 truth + 2 tush + 3 twelve + 1 twere + 2 twice + 2 twill + 1 twixt + 5 two + 1 ubique + 1 unanel + 5 uncle + 1 undergo + 1 understand + 2 understanding + 1 uneffectual + 1 unfledged + 3 unfold + 1 unforced + 1 unfortified + 1 ungracious + 1 unhand + 1 unholy + 1 unhousel + 1 unimproved + 1 unmanly + 1 unmask + 1 unmaster + 1 unmix + 2 unnatural + 1 unprevailing + 1 unprofitable + 1 unproportioned + 1 unrighteous + 1 unschool + 1 unsifted + 4 unto + 1 unvalued + 1 unweeded + 10 up + 1 uphoarded + 18 upon + 19 us + 1 use + 1 uses + 1 usurp + 1 v + 1 vailed + 1 vain + 2 valiant + 1 vanish + 1 vanquisher + 1 vast + 9 very + 1 vial + 1 vicious + 1 vigour + 1 vile + 5 villain + 2 violence + 1 violet + 3 virtue + 1 virtues + 1 virtuous + 1 visage + 1 vision + 2 visit + 5 voice + 4 voltimand + 1 volume + 1 vow + 3 vows + 2 vulgar + 1 wake + 6 walk + 1 walks + 1 wants + 1 war + 2 warlike + 1 warning + 1 warrant + 1 wars + 1 wary + 17 was + 1 wassail + 12 watch + 1 watchman + 3 waves + 2 waxes + 2 way + 1 ways + 34 we + 1 weak + 1 wears + 1 weary + 1 wedding + 1 weed + 1 week + 2 weigh + 1 weighing + 3 welcome + 14 well + 1 went + 3 were + 1 west + 1 westward + 1 wharf + 42 what + 1 whatsoever + 8 when + 1 whence + 9 where + 1 wherefore + 4 wherein + 2 whereof + 1 whether + 16 which + 2 while + 1 whiles + 1 whilst + 1 whirling + 1 whisper + 8 who + 3 whole + 2 wholesome + 8 whose + 13 why + 3 wicked + 1 wide + 1 wife + 1 wild + 25 will + 1 willing + 1 willingly + 1 wilt + 2 wind + 2 winds + 1 windy + 1 wings + 1 wipe + 1 wisdom + 1 wisdoms + 1 wisest + 1 wishes + 2 wit + 1 witch + 1 witchcraft + 65 with + 2 withal + 11 within + 3 without + 1 witness + 4 wittenberg + 3 woe + 2 woman + 1 womb + 1 won + 1 wonder + 1 wonderful + 1 wondrous + 1 wont + 1 woodcocks + 3 word + 2 words + 1 wore + 2 work + 3 world + 1 worm + 1 worth + 1 worthy + 14 would + 3 wouldst + 1 wretch + 2 writ + 1 writing + 1 wrong + 1 wrung + 1 yea + 4 yes + 1 yesternight + 7 yet + 1 yielding + 1 yon + 1 yond + 110 you + 6 young + 49 your + 7 yourself + 5 youth diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/hamlet-act-1.txt b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/hamlet-act-1.txt new file mode 100644 index 00000000..2491678e --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/hamlet-act-1.txt @@ -0,0 +1,1234 @@ +The Tragedy of Hamlet, Prince of Denmark + +ACT I + +SCENE I. Elsinore. A platform before the castle. + +FRANCISCO at his post. Enter to him BERNARDO +BERNARDO +Who's there? +FRANCISCO +Nay, answer me: stand, and unfold yourself. +BERNARDO +Long live the king! +FRANCISCO +Bernardo? +BERNARDO +He. +FRANCISCO +You come most carefully upon your hour. +BERNARDO +'Tis now struck twelve; get thee to bed, Francisco. +FRANCISCO +For this relief much thanks: 'tis bitter cold, +And I am sick at heart. +BERNARDO +Have you had quiet guard? +FRANCISCO +Not a mouse stirring. +BERNARDO +Well, good night. +If you do meet Horatio and Marcellus, +The rivals of my watch, bid them make haste. +FRANCISCO +I think I hear them. Stand, ho! Who's there? +Enter HORATIO and MARCELLUS + +HORATIO +Friends to this ground. +MARCELLUS +And liegemen to the Dane. +FRANCISCO +Give you good night. +MARCELLUS +O, farewell, honest soldier: +Who hath relieved you? +FRANCISCO +Bernardo has my place. +Give you good night. +Exit + +MARCELLUS +Holla! Bernardo! +BERNARDO +Say, +What, is Horatio there? +HORATIO +A piece of him. +BERNARDO +Welcome, Horatio: welcome, good Marcellus. +MARCELLUS +What, has this thing appear'd again to-night? +BERNARDO +I have seen nothing. +MARCELLUS +Horatio says 'tis but our fantasy, +And will not let belief take hold of him +Touching this dreaded sight, twice seen of us: +Therefore I have entreated him along +With us to watch the minutes of this night; +That if again this apparition come, +He may approve our eyes and speak to it. +HORATIO +Tush, tush, 'twill not appear. +BERNARDO +Sit down awhile; +And let us once again assail your ears, +That are so fortified against our story +What we have two nights seen. +HORATIO +Well, sit we down, +And let us hear Bernardo speak of this. +BERNARDO +Last night of all, +When yond same star that's westward from the pole +Had made his course to illume that part of heaven +Where now it burns, Marcellus and myself, +The bell then beating one,-- +Enter Ghost + +MARCELLUS +Peace, break thee off; look, where it comes again! +BERNARDO +In the same figure, like the king that's dead. +MARCELLUS +Thou art a scholar; speak to it, Horatio. +BERNARDO +Looks it not like the king? mark it, Horatio. +HORATIO +Most like: it harrows me with fear and wonder. +BERNARDO +It would be spoke to. +MARCELLUS +Question it, Horatio. +HORATIO +What art thou that usurp'st this time of night, +Together with that fair and warlike form +In which the majesty of buried Denmark +Did sometimes march? by heaven I charge thee, speak! +MARCELLUS +It is offended. +BERNARDO +See, it stalks away! +HORATIO +Stay! speak, speak! I charge thee, speak! +Exit Ghost + +MARCELLUS +'Tis gone, and will not answer. +BERNARDO +How now, Horatio! you tremble and look pale: +Is not this something more than fantasy? +What think you on't? +HORATIO +Before my God, I might not this believe +Without the sensible and true avouch +Of mine own eyes. +MARCELLUS +Is it not like the king? +HORATIO +As thou art to thyself: +Such was the very armour he had on +When he the ambitious Norway combated; +So frown'd he once, when, in an angry parle, +He smote the sledded Polacks on the ice. +'Tis strange. +MARCELLUS +Thus twice before, and jump at this dead hour, +With martial stalk hath he gone by our watch. +HORATIO +In what particular thought to work I know not; +But in the gross and scope of my opinion, +This bodes some strange eruption to our state. +MARCELLUS +Good now, sit down, and tell me, he that knows, +Why this same strict and most observant watch +So nightly toils the subject of the land, +And why such daily cast of brazen cannon, +And foreign mart for implements of war; +Why such impress of shipwrights, whose sore task +Does not divide the Sunday from the week; +What might be toward, that this sweaty haste +Doth make the night joint-labourer with the day: +Who is't that can inform me? +HORATIO +That can I; +At least, the whisper goes so. Our last king, +Whose image even but now appear'd to us, +Was, as you know, by Fortinbras of Norway, +Thereto prick'd on by a most emulate pride, +Dared to the combat; in which our valiant Hamlet-- +For so this side of our known world esteem'd him-- +Did slay this Fortinbras; who by a seal'd compact, +Well ratified by law and heraldry, +Did forfeit, with his life, all those his lands +Which he stood seized of, to the conqueror: +Against the which, a moiety competent +Was gaged by our king; which had return'd +To the inheritance of Fortinbras, +Had he been vanquisher; as, by the same covenant, +And carriage of the article design'd, +His fell to Hamlet. Now, sir, young Fortinbras, +Of unimproved mettle hot and full, +Hath in the skirts of Norway here and there +Shark'd up a list of lawless resolutes, +For food and diet, to some enterprise +That hath a stomach in't; which is no other-- +As it doth well appear unto our state-- +But to recover of us, by strong hand +And terms compulsatory, those foresaid lands +So by his father lost: and this, I take it, +Is the main motive of our preparations, +The source of this our watch and the chief head +Of this post-haste and romage in the land. +BERNARDO +I think it be no other but e'en so: +Well may it sort that this portentous figure +Comes armed through our watch; so like the king +That was and is the question of these wars. +HORATIO +A mote it is to trouble the mind's eye. +In the most high and palmy state of Rome, +A little ere the mightiest Julius fell, +The graves stood tenantless and the sheeted dead +Did squeak and gibber in the Roman streets: +As stars with trains of fire and dews of blood, +Disasters in the sun; and the moist star +Upon whose influence Neptune's empire stands +Was sick almost to doomsday with eclipse: +And even the like precurse of fierce events, +As harbingers preceding still the fates +And prologue to the omen coming on, +Have heaven and earth together demonstrated +Unto our climatures and countrymen.-- +But soft, behold! lo, where it comes again! +Re-enter Ghost + +I'll cross it, though it blast me. Stay, illusion! +If thou hast any sound, or use of voice, +Speak to me: +If there be any good thing to be done, +That may to thee do ease and grace to me, +Speak to me: +Cock crows + +If thou art privy to thy country's fate, +Which, happily, foreknowing may avoid, O, speak! +Or if thou hast uphoarded in thy life +Extorted treasure in the womb of earth, +For which, they say, you spirits oft walk in death, +Speak of it: stay, and speak! Stop it, Marcellus. +MARCELLUS +Shall I strike at it with my partisan? +HORATIO +Do, if it will not stand. +BERNARDO +'Tis here! +HORATIO +'Tis here! +MARCELLUS +'Tis gone! +Exit Ghost + +We do it wrong, being so majestical, +To offer it the show of violence; +For it is, as the air, invulnerable, +And our vain blows malicious mockery. +BERNARDO +It was about to speak, when the cock crew. +HORATIO +And then it started like a guilty thing +Upon a fearful summons. I have heard, +The cock, that is the trumpet to the morn, +Doth with his lofty and shrill-sounding throat +Awake the god of day; and, at his warning, +Whether in sea or fire, in earth or air, +The extravagant and erring spirit hies +To his confine: and of the truth herein +This present object made probation. +MARCELLUS +It faded on the crowing of the cock. +Some say that ever 'gainst that season comes +Wherein our Saviour's birth is celebrated, +The bird of dawning singeth all night long: +And then, they say, no spirit dares stir abroad; +The nights are wholesome; then no planets strike, +No fairy takes, nor witch hath power to charm, +So hallow'd and so gracious is the time. +HORATIO +So have I heard and do in part believe it. +But, look, the morn, in russet mantle clad, +Walks o'er the dew of yon high eastward hill: +Break we our watch up; and by my advice, +Let us impart what we have seen to-night +Unto young Hamlet; for, upon my life, +This spirit, dumb to us, will speak to him. +Do you consent we shall acquaint him with it, +As needful in our loves, fitting our duty? +MARCELLUS +Let's do't, I pray; and I this morning know +Where we shall find him most conveniently. +Exeunt + +SCENE II. A room of state in the castle. + +Enter KING CLAUDIUS, QUEEN GERTRUDE, HAMLET, POLONIUS, LAERTES, VOLTIMAND, CORNELIUS, Lords, and Attendants +KING CLAUDIUS +Though yet of Hamlet our dear brother's death +The memory be green, and that it us befitted +To bear our hearts in grief and our whole kingdom +To be contracted in one brow of woe, +Yet so far hath discretion fought with nature +That we with wisest sorrow think on him, +Together with remembrance of ourselves. +Therefore our sometime sister, now our queen, +The imperial jointress to this warlike state, +Have we, as 'twere with a defeated joy,-- +With an auspicious and a dropping eye, +With mirth in funeral and with dirge in marriage, +In equal scale weighing delight and dole,-- +Taken to wife: nor have we herein barr'd +Your better wisdoms, which have freely gone +With this affair along. For all, our thanks. +Now follows, that you know, young Fortinbras, +Holding a weak supposal of our worth, +Or thinking by our late dear brother's death +Our state to be disjoint and out of frame, +Colleagued with the dream of his advantage, +He hath not fail'd to pester us with message, +Importing the surrender of those lands +Lost by his father, with all bonds of law, +To our most valiant brother. So much for him. +Now for ourself and for this time of meeting: +Thus much the business is: we have here writ +To Norway, uncle of young Fortinbras,-- +Who, impotent and bed-rid, scarcely hears +Of this his nephew's purpose,--to suppress +His further gait herein; in that the levies, +The lists and full proportions, are all made +Out of his subject: and we here dispatch +You, good Cornelius, and you, Voltimand, +For bearers of this greeting to old Norway; +Giving to you no further personal power +To business with the king, more than the scope +Of these delated articles allow. +Farewell, and let your haste commend your duty. +CORNELIUS VOLTIMAND +In that and all things will we show our duty. +KING CLAUDIUS +We doubt it nothing: heartily farewell. +Exeunt VOLTIMAND and CORNELIUS + +And now, Laertes, what's the news with you? +You told us of some suit; what is't, Laertes? +You cannot speak of reason to the Dane, +And loose your voice: what wouldst thou beg, Laertes, +That shall not be my offer, not thy asking? +The head is not more native to the heart, +The hand more instrumental to the mouth, +Than is the throne of Denmark to thy father. +What wouldst thou have, Laertes? +LAERTES +My dread lord, +Your leave and favour to return to France; +From whence though willingly I came to Denmark, +To show my duty in your coronation, +Yet now, I must confess, that duty done, +My thoughts and wishes bend again toward France +And bow them to your gracious leave and pardon. +KING CLAUDIUS +Have you your father's leave? What says Polonius? +LORD POLONIUS +He hath, my lord, wrung from me my slow leave +By laboursome petition, and at last +Upon his will I seal'd my hard consent: +I do beseech you, give him leave to go. +KING CLAUDIUS +Take thy fair hour, Laertes; time be thine, +And thy best graces spend it at thy will! +But now, my cousin Hamlet, and my son,-- +HAMLET +[Aside] A little more than kin, and less than kind. +KING CLAUDIUS +How is it that the clouds still hang on you? +HAMLET +Not so, my lord; I am too much i' the sun. +QUEEN GERTRUDE +Good Hamlet, cast thy nighted colour off, +And let thine eye look like a friend on Denmark. +Do not for ever with thy vailed lids +Seek for thy noble father in the dust: +Thou know'st 'tis common; all that lives must die, +Passing through nature to eternity. +HAMLET +Ay, madam, it is common. +QUEEN GERTRUDE +If it be, +Why seems it so particular with thee? +HAMLET +Seems, madam! nay it is; I know not 'seems.' +'Tis not alone my inky cloak, good mother, +Nor customary suits of solemn black, +Nor windy suspiration of forced breath, +No, nor the fruitful river in the eye, +Nor the dejected 'havior of the visage, +Together with all forms, moods, shapes of grief, +That can denote me truly: these indeed seem, +For they are actions that a man might play: +But I have that within which passeth show; +These but the trappings and the suits of woe. +KING CLAUDIUS +'Tis sweet and commendable in your nature, Hamlet, +To give these mourning duties to your father: +But, you must know, your father lost a father; +That father lost, lost his, and the survivor bound +In filial obligation for some term +To do obsequious sorrow: but to persever +In obstinate condolement is a course +Of impious stubbornness; 'tis unmanly grief; +It shows a will most incorrect to heaven, +A heart unfortified, a mind impatient, +An understanding simple and unschool'd: +For what we know must be and is as common +As any the most vulgar thing to sense, +Why should we in our peevish opposition +Take it to heart? Fie! 'tis a fault to heaven, +A fault against the dead, a fault to nature, +To reason most absurd: whose common theme +Is death of fathers, and who still hath cried, +From the first corse till he that died to-day, +'This must be so.' We pray you, throw to earth +This unprevailing woe, and think of us +As of a father: for let the world take note, +You are the most immediate to our throne; +And with no less nobility of love +Than that which dearest father bears his son, +Do I impart toward you. For your intent +In going back to school in Wittenberg, +It is most retrograde to our desire: +And we beseech you, bend you to remain +Here, in the cheer and comfort of our eye, +Our chiefest courtier, cousin, and our son. +QUEEN GERTRUDE +Let not thy mother lose her prayers, Hamlet: +I pray thee, stay with us; go not to Wittenberg. +HAMLET +I shall in all my best obey you, madam. +KING CLAUDIUS +Why, 'tis a loving and a fair reply: +Be as ourself in Denmark. Madam, come; +This gentle and unforced accord of Hamlet +Sits smiling to my heart: in grace whereof, +No jocund health that Denmark drinks to-day, +But the great cannon to the clouds shall tell, +And the king's rouse the heavens all bruit again, +Re-speaking earthly thunder. Come away. +Exeunt all but HAMLET + +HAMLET +O, that this too too solid flesh would melt +Thaw and resolve itself into a dew! +Or that the Everlasting had not fix'd +His canon 'gainst self-slaughter! O God! God! +How weary, stale, flat and unprofitable, +Seem to me all the uses of this world! +Fie on't! ah fie! 'tis an unweeded garden, +That grows to seed; things rank and gross in nature +Possess it merely. That it should come to this! +But two months dead: nay, not so much, not two: +So excellent a king; that was, to this, +Hyperion to a satyr; so loving to my mother +That he might not beteem the winds of heaven +Visit her face too roughly. Heaven and earth! +Must I remember? why, she would hang on him, +As if increase of appetite had grown +By what it fed on: and yet, within a month-- +Let me not think on't--Frailty, thy name is woman!-- +A little month, or ere those shoes were old +With which she follow'd my poor father's body, +Like Niobe, all tears:--why she, even she-- +O, God! a beast, that wants discourse of reason, +Would have mourn'd longer--married with my uncle, +My father's brother, but no more like my father +Than I to Hercules: within a month: +Ere yet the salt of most unrighteous tears +Had left the flushing in her galled eyes, +She married. O, most wicked speed, to post +With such dexterity to incestuous sheets! +It is not nor it cannot come to good: +But break, my heart; for I must hold my tongue. +Enter HORATIO, MARCELLUS, and BERNARDO + +HORATIO +Hail to your lordship! +HAMLET +I am glad to see you well: +Horatio,--or I do forget myself. +HORATIO +The same, my lord, and your poor servant ever. +HAMLET +Sir, my good friend; I'll change that name with you: +And what make you from Wittenberg, Horatio? Marcellus? +MARCELLUS +My good lord-- +HAMLET +I am very glad to see you. Good even, sir. +But what, in faith, make you from Wittenberg? +HORATIO +A truant disposition, good my lord. +HAMLET +I would not hear your enemy say so, +Nor shall you do mine ear that violence, +To make it truster of your own report +Against yourself: I know you are no truant. +But what is your affair in Elsinore? +We'll teach you to drink deep ere you depart. +HORATIO +My lord, I came to see your father's funeral. +HAMLET +I pray thee, do not mock me, fellow-student; +I think it was to see my mother's wedding. +HORATIO +Indeed, my lord, it follow'd hard upon. +HAMLET +Thrift, thrift, Horatio! the funeral baked meats +Did coldly furnish forth the marriage tables. +Would I had met my dearest foe in heaven +Or ever I had seen that day, Horatio! +My father!--methinks I see my father. +HORATIO +Where, my lord? +HAMLET +In my mind's eye, Horatio. +HORATIO +I saw him once; he was a goodly king. +HAMLET +He was a man, take him for all in all, +I shall not look upon his like again. +HORATIO +My lord, I think I saw him yesternight. +HAMLET +Saw? who? +HORATIO +My lord, the king your father. +HAMLET +The king my father! +HORATIO +Season your admiration for awhile +With an attent ear, till I may deliver, +Upon the witness of these gentlemen, +This marvel to you. +HAMLET +For God's love, let me hear. +HORATIO +Two nights together had these gentlemen, +Marcellus and Bernardo, on their watch, +In the dead vast and middle of the night, +Been thus encounter'd. A figure like your father, +Armed at point exactly, cap-a-pe, +Appears before them, and with solemn march +Goes slow and stately by them: thrice he walk'd +By their oppress'd and fear-surprised eyes, +Within his truncheon's length; whilst they, distilled +Almost to jelly with the act of fear, +Stand dumb and speak not to him. This to me +In dreadful secrecy impart they did; +And I with them the third night kept the watch; +Where, as they had deliver'd, both in time, +Form of the thing, each word made true and good, +The apparition comes: I knew your father; +These hands are not more like. +HAMLET +But where was this? +MARCELLUS +My lord, upon the platform where we watch'd. +HAMLET +Did you not speak to it? +HORATIO +My lord, I did; +But answer made it none: yet once methought +It lifted up its head and did address +Itself to motion, like as it would speak; +But even then the morning cock crew loud, +And at the sound it shrunk in haste away, +And vanish'd from our sight. +HAMLET +'Tis very strange. +HORATIO +As I do live, my honour'd lord, 'tis true; +And we did think it writ down in our duty +To let you know of it. +HAMLET +Indeed, indeed, sirs, but this troubles me. +Hold you the watch to-night? +MARCELLUS BERNARDO +We do, my lord. +HAMLET +Arm'd, say you? +MARCELLUS BERNARDO +Arm'd, my lord. +HAMLET +From top to toe? +MARCELLUS BERNARDO +My lord, from head to foot. +HAMLET +Then saw you not his face? +HORATIO +O, yes, my lord; he wore his beaver up. +HAMLET +What, look'd he frowningly? +HORATIO +A countenance more in sorrow than in anger. +HAMLET +Pale or red? +HORATIO +Nay, very pale. +HAMLET +And fix'd his eyes upon you? +HORATIO +Most constantly. +HAMLET +I would I had been there. +HORATIO +It would have much amazed you. +HAMLET +Very like, very like. Stay'd it long? +HORATIO +While one with moderate haste might tell a hundred. +MARCELLUS BERNARDO +Longer, longer. +HORATIO +Not when I saw't. +HAMLET +His beard was grizzled--no? +HORATIO +It was, as I have seen it in his life, +A sable silver'd. +HAMLET +I will watch to-night; +Perchance 'twill walk again. +HORATIO +I warrant it will. +HAMLET +If it assume my noble father's person, +I'll speak to it, though hell itself should gape +And bid me hold my peace. I pray you all, +If you have hitherto conceal'd this sight, +Let it be tenable in your silence still; +And whatsoever else shall hap to-night, +Give it an understanding, but no tongue: +I will requite your loves. So, fare you well: +Upon the platform, 'twixt eleven and twelve, +I'll visit you. +All +Our duty to your honour. +HAMLET +Your loves, as mine to you: farewell. +Exeunt all but HAMLET + +My father's spirit in arms! all is not well; +I doubt some foul play: would the night were come! +Till then sit still, my soul: foul deeds will rise, +Though all the earth o'erwhelm them, to men's eyes. +Exit + +SCENE III. A room in Polonius' house. + +Enter LAERTES and OPHELIA +LAERTES +My necessaries are embark'd: farewell: +And, sister, as the winds give benefit +And convoy is assistant, do not sleep, +But let me hear from you. +OPHELIA +Do you doubt that? +LAERTES +For Hamlet and the trifling of his favour, +Hold it a fashion and a toy in blood, +A violet in the youth of primy nature, +Forward, not permanent, sweet, not lasting, +The perfume and suppliance of a minute; No more. +OPHELIA +No more but so? +LAERTES +Think it no more; +For nature, crescent, does not grow alone +In thews and bulk, but, as this temple waxes, +The inward service of the mind and soul +Grows wide withal. Perhaps he loves you now, +And now no soil nor cautel doth besmirch +The virtue of his will: but you must fear, +His greatness weigh'd, his will is not his own; +For he himself is subject to his birth: +He may not, as unvalued persons do, +Carve for himself; for on his choice depends +The safety and health of this whole state; +And therefore must his choice be circumscribed +Unto the voice and yielding of that body +Whereof he is the head. Then if he says he loves you, +It fits your wisdom so far to believe it +As he in his particular act and place +May give his saying deed; which is no further +Than the main voice of Denmark goes withal. +Then weigh what loss your honour may sustain, +If with too credent ear you list his songs, +Or lose your heart, or your chaste treasure open +To his unmaster'd importunity. +Fear it, Ophelia, fear it, my dear sister, +And keep you in the rear of your affection, +Out of the shot and danger of desire. +The chariest maid is prodigal enough, +If she unmask her beauty to the moon: +Virtue itself 'scapes not calumnious strokes: +The canker galls the infants of the spring, +Too oft before their buttons be disclosed, +And in the morn and liquid dew of youth +Contagious blastments are most imminent. +Be wary then; best safety lies in fear: +Youth to itself rebels, though none else near. +OPHELIA +I shall the effect of this good lesson keep, +As watchman to my heart. But, good my brother, +Do not, as some ungracious pastors do, +Show me the steep and thorny way to heaven; +Whiles, like a puff'd and reckless libertine, +Himself the primrose path of dalliance treads, +And recks not his own rede. +LAERTES +O, fear me not. +I stay too long: but here my father comes. +Enter POLONIUS + +A double blessing is a double grace, +Occasion smiles upon a second leave. +LORD POLONIUS +Yet here, Laertes! aboard, aboard, for shame! +The wind sits in the shoulder of your sail, +And you are stay'd for. There; my blessing with thee! +And these few precepts in thy memory +See thou character. Give thy thoughts no tongue, +Nor any unproportioned thought his act. +Be thou familiar, but by no means vulgar. +Those friends thou hast, and their adoption tried, +Grapple them to thy soul with hoops of steel; +But do not dull thy palm with entertainment +Of each new-hatch'd, unfledged comrade. Beware +Of entrance to a quarrel, but being in, +Bear't that the opposed may beware of thee. +Give every man thy ear, but few thy voice; +Take each man's censure, but reserve thy judgment. +Costly thy habit as thy purse can buy, +But not express'd in fancy; rich, not gaudy; +For the apparel oft proclaims the man, +And they in France of the best rank and station +Are of a most select and generous chief in that. +Neither a borrower nor a lender be; +For loan oft loses both itself and friend, +And borrowing dulls the edge of husbandry. +This above all: to thine ownself be true, +And it must follow, as the night the day, +Thou canst not then be false to any man. +Farewell: my blessing season this in thee! +LAERTES +Most humbly do I take my leave, my lord. +LORD POLONIUS +The time invites you; go; your servants tend. +LAERTES +Farewell, Ophelia; and remember well +What I have said to you. +OPHELIA +'Tis in my memory lock'd, +And you yourself shall keep the key of it. +LAERTES +Farewell. +Exit + +LORD POLONIUS +What is't, Ophelia, be hath said to you? +OPHELIA +So please you, something touching the Lord Hamlet. +LORD POLONIUS +Marry, well bethought: +'Tis told me, he hath very oft of late +Given private time to you; and you yourself +Have of your audience been most free and bounteous: +If it be so, as so 'tis put on me, +And that in way of caution, I must tell you, +You do not understand yourself so clearly +As it behoves my daughter and your honour. +What is between you? give me up the truth. +OPHELIA +He hath, my lord, of late made many tenders +Of his affection to me. +LORD POLONIUS +Affection! pooh! you speak like a green girl, +Unsifted in such perilous circumstance. +Do you believe his tenders, as you call them? +OPHELIA +I do not know, my lord, what I should think. +LORD POLONIUS +Marry, I'll teach you: think yourself a baby; +That you have ta'en these tenders for true pay, +Which are not sterling. Tender yourself more dearly; +Or--not to crack the wind of the poor phrase, +Running it thus--you'll tender me a fool. +OPHELIA +My lord, he hath importuned me with love +In honourable fashion. +LORD POLONIUS +Ay, fashion you may call it; go to, go to. +OPHELIA +And hath given countenance to his speech, my lord, +With almost all the holy vows of heaven. +LORD POLONIUS +Ay, springes to catch woodcocks. I do know, +When the blood burns, how prodigal the soul +Lends the tongue vows: these blazes, daughter, +Giving more light than heat, extinct in both, +Even in their promise, as it is a-making, +You must not take for fire. From this time +Be somewhat scanter of your maiden presence; +Set your entreatments at a higher rate +Than a command to parley. For Lord Hamlet, +Believe so much in him, that he is young +And with a larger tether may he walk +Than may be given you: in few, Ophelia, +Do not believe his vows; for they are brokers, +Not of that dye which their investments show, +But mere implorators of unholy suits, +Breathing like sanctified and pious bawds, +The better to beguile. This is for all: +I would not, in plain terms, from this time forth, +Have you so slander any moment leisure, +As to give words or talk with the Lord Hamlet. +Look to't, I charge you: come your ways. +OPHELIA +I shall obey, my lord. +Exeunt + +SCENE IV. The platform. + +Enter HAMLET, HORATIO, and MARCELLUS +HAMLET +The air bites shrewdly; it is very cold. +HORATIO +It is a nipping and an eager air. +HAMLET +What hour now? +HORATIO +I think it lacks of twelve. +HAMLET +No, it is struck. +HORATIO +Indeed? I heard it not: then it draws near the season +Wherein the spirit held his wont to walk. +A flourish of trumpets, and ordnance shot off, within + +What does this mean, my lord? +HAMLET +The king doth wake to-night and takes his rouse, +Keeps wassail, and the swaggering up-spring reels; +And, as he drains his draughts of Rhenish down, +The kettle-drum and trumpet thus bray out +The triumph of his pledge. +HORATIO +Is it a custom? +HAMLET +Ay, marry, is't: +But to my mind, though I am native here +And to the manner born, it is a custom +More honour'd in the breach than the observance. +This heavy-headed revel east and west +Makes us traduced and tax'd of other nations: +They clepe us drunkards, and with swinish phrase +Soil our addition; and indeed it takes +From our achievements, though perform'd at height, +The pith and marrow of our attribute. +So, oft it chances in particular men, +That for some vicious mole of nature in them, +As, in their birth--wherein they are not guilty, +Since nature cannot choose his origin-- +By the o'ergrowth of some complexion, +Oft breaking down the pales and forts of reason, +Or by some habit that too much o'er-leavens +The form of plausive manners, that these men, +Carrying, I say, the stamp of one defect, +Being nature's livery, or fortune's star,-- +Their virtues else--be they as pure as grace, +As infinite as man may undergo-- +Shall in the general censure take corruption +From that particular fault: the dram of eale +Doth all the noble substance of a doubt +To his own scandal. +HORATIO +Look, my lord, it comes! +Enter Ghost + +HAMLET +Angels and ministers of grace defend us! +Be thou a spirit of health or goblin damn'd, +Bring with thee airs from heaven or blasts from hell, +Be thy intents wicked or charitable, +Thou comest in such a questionable shape +That I will speak to thee: I'll call thee Hamlet, +King, father, royal Dane: O, answer me! +Let me not burst in ignorance; but tell +Why thy canonized bones, hearsed in death, +Have burst their cerements; why the sepulchre, +Wherein we saw thee quietly inurn'd, +Hath oped his ponderous and marble jaws, +To cast thee up again. What may this mean, +That thou, dead corse, again in complete steel +Revisit'st thus the glimpses of the moon, +Making night hideous; and we fools of nature +So horridly to shake our disposition +With thoughts beyond the reaches of our souls? +Say, why is this? wherefore? what should we do? +Ghost beckons HAMLET + +HORATIO +It beckons you to go away with it, +As if it some impartment did desire +To you alone. +MARCELLUS +Look, with what courteous action +It waves you to a more removed ground: +But do not go with it. +HORATIO +No, by no means. +HAMLET +It will not speak; then I will follow it. +HORATIO +Do not, my lord. +HAMLET +Why, what should be the fear? +I do not set my life in a pin's fee; +And for my soul, what can it do to that, +Being a thing immortal as itself? +It waves me forth again: I'll follow it. +HORATIO +What if it tempt you toward the flood, my lord, +Or to the dreadful summit of the cliff +That beetles o'er his base into the sea, +And there assume some other horrible form, +Which might deprive your sovereignty of reason +And draw you into madness? think of it: +The very place puts toys of desperation, +Without more motive, into every brain +That looks so many fathoms to the sea +And hears it roar beneath. +HAMLET +It waves me still. +Go on; I'll follow thee. +MARCELLUS +You shall not go, my lord. +HAMLET +Hold off your hands. +HORATIO +Be ruled; you shall not go. +HAMLET +My fate cries out, +And makes each petty artery in this body +As hardy as the Nemean lion's nerve. +Still am I call'd. Unhand me, gentlemen. +By heaven, I'll make a ghost of him that lets me! +I say, away! Go on; I'll follow thee. +Exeunt Ghost and HAMLET + +HORATIO +He waxes desperate with imagination. +MARCELLUS +Let's follow; 'tis not fit thus to obey him. +HORATIO +Have after. To what issue will this come? +MARCELLUS +Something is rotten in the state of Denmark. +HORATIO +Heaven will direct it. +MARCELLUS +Nay, let's follow him. +Exeunt + +SCENE V. Another part of the platform. + +Enter GHOST and HAMLET +HAMLET +Where wilt thou lead me? speak; I'll go no further. +Ghost +Mark me. +HAMLET +I will. +Ghost +My hour is almost come, +When I to sulphurous and tormenting flames +Must render up myself. +HAMLET +Alas, poor ghost! +Ghost +Pity me not, but lend thy serious hearing +To what I shall unfold. +HAMLET +Speak; I am bound to hear. +Ghost +So art thou to revenge, when thou shalt hear. +HAMLET +What? +Ghost +I am thy father's spirit, +Doom'd for a certain term to walk the night, +And for the day confined to fast in fires, +Till the foul crimes done in my days of nature +Are burnt and purged away. But that I am forbid +To tell the secrets of my prison-house, +I could a tale unfold whose lightest word +Would harrow up thy soul, freeze thy young blood, +Make thy two eyes, like stars, start from their spheres, +Thy knotted and combined locks to part +And each particular hair to stand on end, +Like quills upon the fretful porpentine: +But this eternal blazon must not be +To ears of flesh and blood. List, list, O, list! +If thou didst ever thy dear father love-- +HAMLET +O God! +Ghost +Revenge his foul and most unnatural murder. +HAMLET +Murder! +Ghost +Murder most foul, as in the best it is; +But this most foul, strange and unnatural. +HAMLET +Haste me to know't, that I, with wings as swift +As meditation or the thoughts of love, +May sweep to my revenge. +Ghost +I find thee apt; +And duller shouldst thou be than the fat weed +That roots itself in ease on Lethe wharf, +Wouldst thou not stir in this. Now, Hamlet, hear: +'Tis given out that, sleeping in my orchard, +A serpent stung me; so the whole ear of Denmark +Is by a forged process of my death +Rankly abused: but know, thou noble youth, +The serpent that did sting thy father's life +Now wears his crown. +HAMLET +O my prophetic soul! My uncle! +Ghost +Ay, that incestuous, that adulterate beast, +With witchcraft of his wit, with traitorous gifts,-- +O wicked wit and gifts, that have the power +So to seduce!--won to his shameful lust +The will of my most seeming-virtuous queen: +O Hamlet, what a falling-off was there! +From me, whose love was of that dignity +That it went hand in hand even with the vow +I made to her in marriage, and to decline +Upon a wretch whose natural gifts were poor +To those of mine! +But virtue, as it never will be moved, +Though lewdness court it in a shape of heaven, +So lust, though to a radiant angel link'd, +Will sate itself in a celestial bed, +And prey on garbage. +But, soft! methinks I scent the morning air; +Brief let me be. Sleeping within my orchard, +My custom always of the afternoon, +Upon my secure hour thy uncle stole, +With juice of cursed hebenon in a vial, +And in the porches of my ears did pour +The leperous distilment; whose effect +Holds such an enmity with blood of man +That swift as quicksilver it courses through +The natural gates and alleys of the body, +And with a sudden vigour doth posset +And curd, like eager droppings into milk, +The thin and wholesome blood: so did it mine; +And a most instant tetter bark'd about, +Most lazar-like, with vile and loathsome crust, +All my smooth body. +Thus was I, sleeping, by a brother's hand +Of life, of crown, of queen, at once dispatch'd: +Cut off even in the blossoms of my sin, +Unhousel'd, disappointed, unanel'd, +No reckoning made, but sent to my account +With all my imperfections on my head: +O, horrible! O, horrible! most horrible! +If thou hast nature in thee, bear it not; +Let not the royal bed of Denmark be +A couch for luxury and damned incest. +But, howsoever thou pursuest this act, +Taint not thy mind, nor let thy soul contrive +Against thy mother aught: leave her to heaven +And to those thorns that in her bosom lodge, +To prick and sting her. Fare thee well at once! +The glow-worm shows the matin to be near, +And 'gins to pale his uneffectual fire: +Adieu, adieu! Hamlet, remember me. +Exit + +HAMLET +O all you host of heaven! O earth! what else? +And shall I couple hell? O, fie! Hold, hold, my heart; +And you, my sinews, grow not instant old, +But bear me stiffly up. Remember thee! +Ay, thou poor ghost, while memory holds a seat +In this distracted globe. Remember thee! +Yea, from the table of my memory +I'll wipe away all trivial fond records, +All saws of books, all forms, all pressures past, +That youth and observation copied there; +And thy commandment all alone shall live +Within the book and volume of my brain, +Unmix'd with baser matter: yes, by heaven! +O most pernicious woman! +O villain, villain, smiling, damned villain! +My tables,--meet it is I set it down, +That one may smile, and smile, and be a villain; +At least I'm sure it may be so in Denmark: +Writing + +So, uncle, there you are. Now to my word; +It is 'Adieu, adieu! remember me.' +I have sworn 't. +MARCELLUS HORATIO +[Within] My lord, my lord,-- +MARCELLUS +[Within] Lord Hamlet,-- +HORATIO +[Within] Heaven secure him! +HAMLET +So be it! +HORATIO +[Within] Hillo, ho, ho, my lord! +HAMLET +Hillo, ho, ho, boy! come, bird, come. +Enter HORATIO and MARCELLUS + +MARCELLUS +How is't, my noble lord? +HORATIO +What news, my lord? +HAMLET +O, wonderful! +HORATIO +Good my lord, tell it. +HAMLET +No; you'll reveal it. +HORATIO +Not I, my lord, by heaven. +MARCELLUS +Nor I, my lord. +HAMLET +How say you, then; would heart of man once think it? +But you'll be secret? +HORATIO MARCELLUS +Ay, by heaven, my lord. +HAMLET +There's ne'er a villain dwelling in all Denmark +But he's an arrant knave. +HORATIO +There needs no ghost, my lord, come from the grave +To tell us this. +HAMLET +Why, right; you are i' the right; +And so, without more circumstance at all, +I hold it fit that we shake hands and part: +You, as your business and desire shall point you; +For every man has business and desire, +Such as it is; and for mine own poor part, +Look you, I'll go pray. +HORATIO +These are but wild and whirling words, my lord. +HAMLET +I'm sorry they offend you, heartily; +Yes, 'faith heartily. +HORATIO +There's no offence, my lord. +HAMLET +Yes, by Saint Patrick, but there is, Horatio, +And much offence too. Touching this vision here, +It is an honest ghost, that let me tell you: +For your desire to know what is between us, +O'ermaster 't as you may. And now, good friends, +As you are friends, scholars and soldiers, +Give me one poor request. +HORATIO +What is't, my lord? we will. +HAMLET +Never make known what you have seen to-night. +HORATIO MARCELLUS +My lord, we will not. +HAMLET +Nay, but swear't. +HORATIO +In faith, +My lord, not I. +MARCELLUS +Nor I, my lord, in faith. +HAMLET +Upon my sword. +MARCELLUS +We have sworn, my lord, already. +HAMLET +Indeed, upon my sword, indeed. +Ghost +[Beneath] Swear. +HAMLET +Ah, ha, boy! say'st thou so? art thou there, +truepenny? +Come on--you hear this fellow in the cellarage-- +Consent to swear. +HORATIO +Propose the oath, my lord. +HAMLET +Never to speak of this that you have seen, +Swear by my sword. +Ghost +[Beneath] Swear. +HAMLET +Hic et ubique? then we'll shift our ground. +Come hither, gentlemen, +And lay your hands again upon my sword: +Never to speak of this that you have heard, +Swear by my sword. +Ghost +[Beneath] Swear. +HAMLET +Well said, old mole! canst work i' the earth so fast? +A worthy pioner! Once more remove, good friends. +HORATIO +O day and night, but this is wondrous strange! +HAMLET +And therefore as a stranger give it welcome. +There are more things in heaven and earth, Horatio, +Than are dreamt of in your philosophy. But come; +Here, as before, never, so help you mercy, +How strange or odd soe'er I bear myself, +As I perchance hereafter shall think meet +To put an antic disposition on, +That you, at such times seeing me, never shall, +With arms encumber'd thus, or this headshake, +Or by pronouncing of some doubtful phrase, +As 'Well, well, we know,' or 'We could, an if we would,' +Or 'If we list to speak,' or 'There be, an if they might,' +Or such ambiguous giving out, to note +That you know aught of me: this not to do, +So grace and mercy at your most need help you, Swear. +Ghost +[Beneath] Swear. +HAMLET +Rest, rest, perturbed spirit! +They swear + +So, gentlemen, +With all my love I do commend me to you: +And what so poor a man as Hamlet is +May do, to express his love and friending to you, +God willing, shall not lack. Let us go in together; +And still your fingers on your lips, I pray. +The time is out of joint: O cursed spite, +That ever I was born to set it right! +Nay, come, let's go together. +Exeunt diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/make-db.cc b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/make-db.cc new file mode 100644 index 00000000..20ef4f7a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/make-db.cc @@ -0,0 +1,116 @@ +// Copyright 2012 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This program creates a leveldb db at /tmp/db. + +#include + +#include "leveldb/db.h" + +static const char* dbname = "/tmp/db"; + +// The program consists of up to 4 stages. If stage is in the range [1, 4], +// the program will exit after the stage'th stage. +// 1. create an empty DB. +// 2. add some key/value pairs. +// 3. close and re-open the DB, which forces a compaction. +// 4. add some more key/value pairs. +static const int stage = 4; + +int main(int argc, char** argv) { + leveldb::Status status; + leveldb::Options o; + leveldb::WriteOptions wo; + leveldb::DB* db; + + o.create_if_missing = true; + o.error_if_exists = true; + + if (stage < 1) { + return 0; + } + cout << "Stage 1" << endl; + + status = leveldb::DB::Open(o, dbname, &db); + if (!status.ok()) { + cerr << "DB::Open " << status.ToString() << endl; + return 1; + } + + if (stage < 2) { + return 0; + } + cout << "Stage 2" << endl; + + status = db->Put(wo, "foo", "one"); + if (!status.ok()) { + cerr << "DB::Put " << status.ToString() << endl; + return 1; + } + + status = db->Put(wo, "bar", "two"); + if (!status.ok()) { + cerr << "DB::Put " << status.ToString() << endl; + return 1; + } + + status = db->Put(wo, "baz", "three"); + if (!status.ok()) { + cerr << "DB::Put " << status.ToString() << endl; + return 1; + } + + status = db->Put(wo, "foo", "four"); + if (!status.ok()) { + cerr << "DB::Put " << status.ToString() << endl; + return 1; + } + + status = db->Delete(wo, "bar"); + if (!status.ok()) { + cerr << "DB::Delete " << status.ToString() << endl; + return 1; + } + + if (stage < 3) { + return 0; + } + cout << "Stage 3" << endl; + + delete db; + db = NULL; + o.create_if_missing = false; + o.error_if_exists = false; + + status = leveldb::DB::Open(o, dbname, &db); + if (!status.ok()) { + cerr << "DB::Open " << status.ToString() << endl; + return 1; + } + + if (stage < 4) { + return 0; + } + cout << "Stage 4" << endl; + + status = db->Put(wo, "foo", "five"); + if (!status.ok()) { + cerr << "DB::Put " << status.ToString() << endl; + return 1; + } + + status = db->Put(wo, "quux", "six"); + if (!status.ok()) { + cerr << "DB::Put " << status.ToString() << endl; + return 1; + } + + status = db->Delete(wo, "baz"); + if (!status.ok()) { + cerr << "DB::Delete " << status.ToString() << endl; + return 1; + } + + return 0; +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/make-table.cc b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/make-table.cc new file mode 100644 index 00000000..1f6b3f47 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/leveldb-go/testdata/make-table.cc @@ -0,0 +1,106 @@ +// Copyright 2011 The LevelDB-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This program adds N lines from infile to a leveldb table at outfile. +// The h.txt infile was generated via: +// cat hamlet-act-1.txt | tr '[:upper:]' '[:lower:]' | grep -o -E '\w+' | sort | uniq -c > infile + +#include +#include +#include + +#include "leveldb/env.h" +#include "leveldb/table.h" +#include "leveldb/table_builder.h" + +const int N = 1000000; +const char* infile = "h.txt"; +const char* outfile = "h.sst"; + +int write() { + leveldb::Status status; + + leveldb::WritableFile* wf; + status = leveldb::Env::Default()->NewWritableFile(outfile, &wf); + if (!status.ok()) { + cerr << "Env::NewWritableFile: " << status.ToString() << endl; + return 1; + } + + leveldb::Options o; + // o.compression = leveldb::kNoCompression; + leveldb::TableBuilder* tb = new leveldb::TableBuilder(o, wf); + ifstream in(infile); + string s; + for (int i = 0; i < N && getline(in, s); i++) { + string key(s, 8); + string val(s, 0, 7); + val = val.substr(1 + val.rfind(' ')); + tb->Add(key.c_str(), val.c_str()); + } + + status = tb->Finish(); + if (!status.ok()) { + cerr << "TableBuilder::Finish: " << status.ToString() << endl; + return 1; + } + + status = wf->Close(); + if (!status.ok()) { + cerr << "WritableFile::Close: " << status.ToString() << endl; + return 1; + } + + cout << "wrote " << tb->NumEntries() << " entries" << endl; + delete tb; + delete wf; + return 0; +} + +int read() { + leveldb::Status status; + + leveldb::RandomAccessFile* raf; + status = leveldb::Env::Default()->NewRandomAccessFile(outfile, &raf); + if (!status.ok()) { + cerr << "Env::NewRandomAccessFile: " << status.ToString() << endl; + return 1; + } + + uint64_t file_size; + status = leveldb::Env::Default()->GetFileSize(outfile, &file_size); + if (!status.ok()) { + cerr << "Env::GetFileSize: " << status.ToString() << endl; + return 1; + } + + leveldb::Options o; + leveldb::Table* t; + status = leveldb::Table::Open(o, raf, file_size, &t); + if (!status.ok()) { + cerr << "Table::Open: " << status.ToString() << endl; + return 1; + } + + leveldb::ReadOptions ro; + leveldb::Iterator* i = t->NewIterator(ro); + uint64_t n = 0; + for (i->SeekToFirst(); i->Valid(); i->Next()) { + n++; + } + + cout << "read " << n << " entries" << endl; + delete i; + delete t; + delete raf; + return 0; +} + +int main(int argc, char** argv) { + int ret = write(); + if (ret != 0) { + return ret; + } + return read(); +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/gf256/blog_test.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/gf256/blog_test.go new file mode 100644 index 00000000..4fbf7ecb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/gf256/blog_test.go @@ -0,0 +1,85 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains a straightforward implementation of +// Reed-Solomon encoding, along with a benchmark. +// It goes with http://research.swtch.com/field. +// +// For an optimized implementation, see gf256.go. + +package gf256 + +import ( + "bytes" + "fmt" + "testing" +) + +// BlogECC writes to check the error correcting code bytes +// for data using the given Reed-Solomon parameters. +func BlogECC(rs *RSEncoder, m []byte, check []byte) { + if len(check) < rs.c { + panic("gf256: invalid check byte length") + } + if rs.c == 0 { + return + } + + // The check bytes are the remainder after dividing + // data padded with c zeros by the generator polynomial. + + // p = data padded with c zeros. + var p []byte + n := len(m) + rs.c + if len(rs.p) >= n { + p = rs.p + } else { + p = make([]byte, n) + } + copy(p, m) + for i := len(m); i < len(p); i++ { + p[i] = 0 + } + + gen := rs.gen + + // Divide p by gen, leaving the remainder in p[len(data):]. + // p[0] is the most significant term in p, and + // gen[0] is the most significant term in the generator. + for i := 0; i < len(m); i++ { + k := f.Mul(p[i], f.Inv(gen[0])) // k = pi / g0 + // p -= k·g + for j, g := range gen { + p[i+j] = f.Add(p[i+j], f.Mul(k, g)) + } + } + + copy(check, p[len(m):]) + rs.p = p +} + +func BenchmarkBlogECC(b *testing.B) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0x29, 0x41, 0xb3, 0x93, 0x8, 0xe8, 0xa3, 0xe7, 0x63, 0x8f} + out := make([]byte, len(check)) + rs := NewRSEncoder(f, len(check)) + for i := 0; i < b.N; i++ { + BlogECC(rs, data, out) + } + b.SetBytes(int64(len(data))) + if !bytes.Equal(out, check) { + fmt.Printf("have %#v want %#v\n", out, check) + } +} + +func TestBlogECC(t *testing.T) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0xa5, 0x24, 0xd4, 0xc1, 0xed, 0x36, 0xc7, 0x87, 0x2c, 0x55} + out := make([]byte, len(check)) + rs := NewRSEncoder(f, len(check)) + BlogECC(rs, data, out) + if !bytes.Equal(out, check) { + t.Errorf("have %x want %x", out, check) + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/gf256/gf256.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/gf256/gf256.go new file mode 100644 index 00000000..feab9187 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/gf256/gf256.go @@ -0,0 +1,241 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package gf256 implements arithmetic over the Galois Field GF(256). +package gf256 + +import "strconv" + +// A Field represents an instance of GF(256) defined by a specific polynomial. +type Field struct { + log [256]byte // log[0] is unused + exp [510]byte +} + +// NewField returns a new field corresponding to the polynomial poly +// and generator α. The Reed-Solomon encoding in QR codes uses +// polynomial 0x11d with generator 2. +// +// The choice of generator α only affects the Exp and Log operations. +func NewField(poly, α int) *Field { + if poly < 0x100 || poly >= 0x200 || reducible(poly) { + panic("gf256: invalid polynomial: " + strconv.Itoa(poly)) + } + + var f Field + x := 1 + for i := 0; i < 255; i++ { + if x == 1 && i != 0 { + panic("gf256: invalid generator " + strconv.Itoa(α) + + " for polynomial " + strconv.Itoa(poly)) + } + f.exp[i] = byte(x) + f.exp[i+255] = byte(x) + f.log[x] = byte(i) + x = mul(x, α, poly) + } + f.log[0] = 255 + for i := 0; i < 255; i++ { + if f.log[f.exp[i]] != byte(i) { + panic("bad log") + } + if f.log[f.exp[i+255]] != byte(i) { + panic("bad log") + } + } + for i := 1; i < 256; i++ { + if f.exp[f.log[i]] != byte(i) { + panic("bad log") + } + } + + return &f +} + +// nbit returns the number of significant in p. +func nbit(p int) uint { + n := uint(0) + for ; p > 0; p >>= 1 { + n++ + } + return n +} + +// polyDiv divides the polynomial p by q and returns the remainder. +func polyDiv(p, q int) int { + np := nbit(p) + nq := nbit(q) + for ; np >= nq; np-- { + if p&(1<<(np-1)) != 0 { + p ^= q << (np - nq) + } + } + return p +} + +// mul returns the product x*y mod poly, a GF(256) multiplication. +func mul(x, y, poly int) int { + z := 0 + for x > 0 { + if x&1 != 0 { + z ^= y + } + x >>= 1 + y <<= 1 + if y&0x100 != 0 { + y ^= poly + } + } + return z +} + +// reducible reports whether p is reducible. +func reducible(p int) bool { + // Multiplying n-bit * n-bit produces (2n-1)-bit, + // so if p is reducible, one of its factors must be + // of np/2+1 bits or fewer. + np := nbit(p) + for q := 2; q < 1<<(np/2+1); q++ { + if polyDiv(p, q) == 0 { + return true + } + } + return false +} + +// Add returns the sum of x and y in the field. +func (f *Field) Add(x, y byte) byte { + return x ^ y +} + +// Exp returns the the base-α exponential of e in the field. +// If e < 0, Exp returns 0. +func (f *Field) Exp(e int) byte { + if e < 0 { + return 0 + } + return f.exp[e%255] +} + +// Log returns the base-α logarithm of x in the field. +// If x == 0, Log returns -1. +func (f *Field) Log(x byte) int { + if x == 0 { + return -1 + } + return int(f.log[x]) +} + +// Inv returns the multiplicative inverse of x in the field. +// If x == 0, Inv returns 0. +func (f *Field) Inv(x byte) byte { + if x == 0 { + return 0 + } + return f.exp[255-f.log[x]] +} + +// Mul returns the product of x and y in the field. +func (f *Field) Mul(x, y byte) byte { + if x == 0 || y == 0 { + return 0 + } + return f.exp[int(f.log[x])+int(f.log[y])] +} + +// An RSEncoder implements Reed-Solomon encoding +// over a given field using a given number of error correction bytes. +type RSEncoder struct { + f *Field + c int + gen []byte + lgen []byte + p []byte +} + +func (f *Field) gen(e int) (gen, lgen []byte) { + // p = 1 + p := make([]byte, e+1) + p[e] = 1 + + for i := 0; i < e; i++ { + // p *= (x + Exp(i)) + // p[j] = p[j]*Exp(i) + p[j+1]. + c := f.Exp(i) + for j := 0; j < e; j++ { + p[j] = f.Mul(p[j], c) ^ p[j+1] + } + p[e] = f.Mul(p[e], c) + } + + // lp = log p. + lp := make([]byte, e+1) + for i, c := range p { + if c == 0 { + lp[i] = 255 + } else { + lp[i] = byte(f.Log(c)) + } + } + + return p, lp +} + +// NewRSEncoder returns a new Reed-Solomon encoder +// over the given field and number of error correction bytes. +func NewRSEncoder(f *Field, c int) *RSEncoder { + gen, lgen := f.gen(c) + return &RSEncoder{f: f, c: c, gen: gen, lgen: lgen} +} + +// ECC writes to check the error correcting code bytes +// for data using the given Reed-Solomon parameters. +func (rs *RSEncoder) ECC(data []byte, check []byte) { + if len(check) < rs.c { + panic("gf256: invalid check byte length") + } + if rs.c == 0 { + return + } + + // The check bytes are the remainder after dividing + // data padded with c zeros by the generator polynomial. + + // p = data padded with c zeros. + var p []byte + n := len(data) + rs.c + if len(rs.p) >= n { + p = rs.p + } else { + p = make([]byte, n) + } + copy(p, data) + for i := len(data); i < len(p); i++ { + p[i] = 0 + } + + // Divide p by gen, leaving the remainder in p[len(data):]. + // p[0] is the most significant term in p, and + // gen[0] is the most significant term in the generator, + // which is always 1. + // To avoid repeated work, we store various values as + // lv, not v, where lv = log[v]. + f := rs.f + lgen := rs.lgen[1:] + for i := 0; i < len(data); i++ { + c := p[i] + if c == 0 { + continue + } + q := p[i+1:] + exp := f.exp[f.log[c]:] + for j, lg := range lgen { + if lg != 255 { // lgen uses 255 for log 0 + q[j] ^= exp[lg] + } + } + } + copy(check, p[len(data):]) + rs.p = p +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/gf256/gf256_test.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/gf256/gf256_test.go new file mode 100644 index 00000000..f77fa7d6 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/gf256/gf256_test.go @@ -0,0 +1,194 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gf256 + +import ( + "bytes" + "fmt" + "testing" +) + +var f = NewField(0x11d, 2) // x^8 + x^4 + x^3 + x^2 + 1 + +func TestBasic(t *testing.T) { + if f.Exp(0) != 1 || f.Exp(1) != 2 || f.Exp(255) != 1 { + panic("bad Exp") + } +} + +func TestECC(t *testing.T) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0xa5, 0x24, 0xd4, 0xc1, 0xed, 0x36, 0xc7, 0x87, 0x2c, 0x55} + out := make([]byte, len(check)) + rs := NewRSEncoder(f, len(check)) + rs.ECC(data, out) + if !bytes.Equal(out, check) { + t.Errorf("have %x want %x", out, check) + } +} + +func TestLinear(t *testing.T) { + d1 := []byte{0x00, 0x00} + c1 := []byte{0x00, 0x00} + out := make([]byte, len(c1)) + rs := NewRSEncoder(f, len(c1)) + if rs.ECC(d1, out); !bytes.Equal(out, c1) { + t.Errorf("ECBytes(%x, %d) = %x, want 0", d1, len(c1), out) + } + d2 := []byte{0x00, 0x01} + c2 := make([]byte, 2) + rs.ECC(d2, c2) + d3 := []byte{0x00, 0x02} + c3 := make([]byte, 2) + rs.ECC(d3, c3) + cx := make([]byte, 2) + for i := range cx { + cx[i] = c2[i] ^ c3[i] + } + d4 := []byte{0x00, 0x03} + c4 := make([]byte, 2) + rs.ECC(d4, c4) + if !bytes.Equal(cx, c4) { + t.Errorf("ECBytes(%x, 2) = %x\nECBytes(%x, 2) = %x\nxor = %x\nECBytes(%x, 2) = %x", + d2, c2, d3, c3, cx, d4, c4) + } +} + +func TestGaussJordan(t *testing.T) { + rs := NewRSEncoder(f, 2) + m := make([][]byte, 16) + for i := range m { + m[i] = make([]byte, 4) + m[i][i/8] = 1 << uint(i%8) + rs.ECC(m[i][:2], m[i][2:]) + } + if false { + fmt.Printf("---\n") + for _, row := range m { + fmt.Printf("%x\n", row) + } + } + b := []uint{0, 1, 2, 3, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25, 26, 27} + for i := 0; i < 16; i++ { + bi := b[i] + if m[i][bi/8]&(1<<(7-bi%8)) == 0 { + for j := i + 1; ; j++ { + if j >= len(m) { + t.Errorf("lost track for %d", bi) + break + } + if m[j][bi/8]&(1<<(7-bi%8)) != 0 { + m[i], m[j] = m[j], m[i] + break + } + } + } + for j := i + 1; j < len(m); j++ { + if m[j][bi/8]&(1<<(7-bi%8)) != 0 { + for k := range m[j] { + m[j][k] ^= m[i][k] + } + } + } + } + if false { + fmt.Printf("---\n") + for _, row := range m { + fmt.Printf("%x\n", row) + } + } + for i := 15; i >= 0; i-- { + bi := b[i] + for j := i - 1; j >= 0; j-- { + if m[j][bi/8]&(1<<(7-bi%8)) != 0 { + for k := range m[j] { + m[j][k] ^= m[i][k] + } + } + } + } + if false { + fmt.Printf("---\n") + for _, row := range m { + fmt.Printf("%x", row) + out := make([]byte, 2) + if rs.ECC(row[:2], out); !bytes.Equal(out, row[2:]) { + fmt.Printf(" - want %x", out) + } + fmt.Printf("\n") + } + } +} + +func BenchmarkECC(b *testing.B) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0x29, 0x41, 0xb3, 0x93, 0x8, 0xe8, 0xa3, 0xe7, 0x63, 0x8f} + out := make([]byte, len(check)) + rs := NewRSEncoder(f, len(check)) + for i := 0; i < b.N; i++ { + rs.ECC(data, out) + } + b.SetBytes(int64(len(data))) + if !bytes.Equal(out, check) { + fmt.Printf("have %#v want %#v\n", out, check) + } +} + +func TestGen(t *testing.T) { + for i := 0; i < 256; i++ { + _, lg := f.gen(i) + if lg[0] != 0 { + t.Errorf("#%d: %x", i, lg) + } + } +} + +func TestReducible(t *testing.T) { + var count = []int{1, 2, 3, 6, 9, 18, 30, 56, 99, 186} // oeis.org/A1037 + for i, want := range count { + n := 0 + for p := 1 << uint(i+2); p < 1< 0 { + n := nbit + if n > 8 { + n = 8 + } + if b.nbit%8 == 0 { + b.b = append(b.b, 0) + } else { + m := -b.nbit & 7 + if n > m { + n = m + } + } + b.nbit += n + sh := uint(nbit - n) + b.b[len(b.b)-1] |= uint8(v >> sh << uint(-b.nbit&7)) + v -= v >> sh << sh + nbit -= n + } +} + +// Num is the encoding for numeric data. +// The only valid characters are the decimal digits 0 through 9. +type Num string + +func (s Num) String() string { + return fmt.Sprintf("Num(%#q)", string(s)) +} + +func (s Num) Check() error { + for _, c := range s { + if c < '0' || '9' < c { + return fmt.Errorf("non-numeric string %#q", string(s)) + } + } + return nil +} + +var numLen = [3]int{10, 12, 14} + +func (s Num) Bits(v Version) int { + return 4 + numLen[v.sizeClass()] + (10*len(s)+2)/3 +} + +func (s Num) Encode(b *Bits, v Version) { + b.Write(1, 4) + b.Write(uint(len(s)), numLen[v.sizeClass()]) + var i int + for i = 0; i+3 <= len(s); i += 3 { + w := uint(s[i]-'0')*100 + uint(s[i+1]-'0')*10 + uint(s[i+2]-'0') + b.Write(w, 10) + } + switch len(s) - i { + case 1: + w := uint(s[i] - '0') + b.Write(w, 4) + case 2: + w := uint(s[i]-'0')*10 + uint(s[i+1]-'0') + b.Write(w, 7) + } +} + +// Alpha is the encoding for alphanumeric data. +// The valid characters are 0-9A-Z$%*+-./: and space. +type Alpha string + +const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:" + +func (s Alpha) String() string { + return fmt.Sprintf("Alpha(%#q)", string(s)) +} + +func (s Alpha) Check() error { + for _, c := range s { + if strings.IndexRune(alphabet, c) < 0 { + return fmt.Errorf("non-alphanumeric string %#q", string(s)) + } + } + return nil +} + +var alphaLen = [3]int{9, 11, 13} + +func (s Alpha) Bits(v Version) int { + return 4 + alphaLen[v.sizeClass()] + (11*len(s)+1)/2 +} + +func (s Alpha) Encode(b *Bits, v Version) { + b.Write(2, 4) + b.Write(uint(len(s)), alphaLen[v.sizeClass()]) + var i int + for i = 0; i+2 <= len(s); i += 2 { + w := uint(strings.IndexRune(alphabet, rune(s[i])))*45 + + uint(strings.IndexRune(alphabet, rune(s[i+1]))) + b.Write(w, 11) + } + + if i < len(s) { + w := uint(strings.IndexRune(alphabet, rune(s[i]))) + b.Write(w, 6) + } +} + +// String is the encoding for 8-bit data. All bytes are valid. +type String string + +func (s String) String() string { + return fmt.Sprintf("String(%#q)", string(s)) +} + +func (s String) Check() error { + return nil +} + +var stringLen = [3]int{8, 16, 16} + +func (s String) Bits(v Version) int { + return 4 + stringLen[v.sizeClass()] + 8*len(s) +} + +func (s String) Encode(b *Bits, v Version) { + b.Write(4, 4) + b.Write(uint(len(s)), stringLen[v.sizeClass()]) + for i := 0; i < len(s); i++ { + b.Write(uint(s[i]), 8) + } +} + +// A Pixel describes a single pixel in a QR code. +type Pixel uint32 + +const ( + Black Pixel = 1 << iota + Invert +) + +func (p Pixel) Offset() uint { + return uint(p >> 6) +} + +func OffsetPixel(o uint) Pixel { + return Pixel(o << 6) +} + +func (r PixelRole) Pixel() Pixel { + return Pixel(r << 2) +} + +func (p Pixel) Role() PixelRole { + return PixelRole(p>>2) & 15 +} + +func (p Pixel) String() string { + s := p.Role().String() + if p&Black != 0 { + s += "+black" + } + if p&Invert != 0 { + s += "+invert" + } + s += "+" + strconv.FormatUint(uint64(p.Offset()), 10) + return s +} + +// A PixelRole describes the role of a QR pixel. +type PixelRole uint32 + +const ( + _ PixelRole = iota + Position // position squares (large) + Alignment // alignment squares (small) + Timing // timing strip between position squares + Format // format metadata + PVersion // version pattern + Unused // unused pixel + Data // data bit + Check // error correction check bit + Extra +) + +var roles = []string{ + "", + "position", + "alignment", + "timing", + "format", + "pversion", + "unused", + "data", + "check", + "extra", +} + +func (r PixelRole) String() string { + if Position <= r && r <= Check { + return roles[r] + } + return strconv.Itoa(int(r)) +} + +// A Level represents a QR error correction level. +// From least to most tolerant of errors, they are L, M, Q, H. +type Level int + +const ( + L Level = iota + M + Q + H +) + +func (l Level) String() string { + if L <= l && l <= H { + return "LMQH"[l : l+1] + } + return strconv.Itoa(int(l)) +} + +// A Code is a square pixel grid. +type Code struct { + Bitmap []byte // 1 is black, 0 is white + Size int // number of pixels on a side + Stride int // number of bytes per row +} + +func (c *Code) Black(x, y int) bool { + return 0 <= x && x < c.Size && 0 <= y && y < c.Size && + c.Bitmap[y*c.Stride+x/8]&(1<= pad { + break + } + b.Write(0x11, 8) + } + } +} + +func (b *Bits) AddCheckBytes(v Version, l Level) { + nd := v.DataBytes(l) + if b.nbit < nd*8 { + b.Pad(nd*8 - b.nbit) + } + if b.nbit != nd*8 { + panic("qr: too much data") + } + + dat := b.Bytes() + vt := &vtab[v] + lev := &vt.level[l] + db := nd / lev.nblock + extra := nd % lev.nblock + chk := make([]byte, lev.check) + rs := gf256.NewRSEncoder(Field, lev.check) + for i := 0; i < lev.nblock; i++ { + if i == lev.nblock-extra { + db++ + } + rs.ECC(dat[:db], chk) + b.Append(chk) + dat = dat[db:] + } + + if len(b.Bytes()) != vt.bytes { + panic("qr: internal error") + } +} + +func (p *Plan) Encode(text ...Encoding) (*Code, error) { + var b Bits + for _, t := range text { + if err := t.Check(); err != nil { + return nil, err + } + t.Encode(&b, p.Version) + } + if b.Bits() > p.DataBytes*8 { + return nil, fmt.Errorf("cannot encode %d bits into %d-bit code", b.Bits(), p.DataBytes*8) + } + b.AddCheckBytes(p.Version, p.Level) + bytes := b.Bytes() + + // Now we have the checksum bytes and the data bytes. + // Construct the actual code. + c := &Code{Size: len(p.Pixel), Stride: (len(p.Pixel) + 7) &^ 7} + c.Bitmap = make([]byte, c.Stride*c.Size) + crow := c.Bitmap + for _, row := range p.Pixel { + for x, pix := range row { + switch pix.Role() { + case Data, Check: + o := pix.Offset() + if bytes[o/8]&(1< 40 { + return nil, fmt.Errorf("invalid QR version %d", int(v)) + } + siz := 17 + int(v)*4 + m := grid(siz) + p.Pixel = m + + // Timing markers (overwritten by boxes). + const ti = 6 // timing is in row/column 6 (counting from 0) + for i := range m { + p := Timing.Pixel() + if i&1 == 0 { + p |= Black + } + m[i][ti] = p + m[ti][i] = p + } + + // Position boxes. + posBox(m, 0, 0) + posBox(m, siz-7, 0) + posBox(m, 0, siz-7) + + // Alignment boxes. + info := &vtab[v] + for x := 4; x+5 < siz; { + for y := 4; y+5 < siz; { + // don't overwrite timing markers + if (x < 7 && y < 7) || (x < 7 && y+5 >= siz-7) || (x+5 >= siz-7 && y < 7) { + } else { + alignBox(m, x, y) + } + if y == 4 { + y = info.apos + } else { + y += info.astride + } + } + if x == 4 { + x = info.apos + } else { + x += info.astride + } + } + + // Version pattern. + pat := vtab[v].pattern + if pat != 0 { + v := pat + for x := 0; x < 6; x++ { + for y := 0; y < 3; y++ { + p := PVersion.Pixel() + if v&1 != 0 { + p |= Black + } + m[siz-11+y][x] = p + m[x][siz-11+y] = p + v >>= 1 + } + } + } + + // One lonely black pixel + m[siz-8][8] = Unused.Pixel() | Black + + return p, nil +} + +// fplan adds the format pixels +func fplan(l Level, m Mask, p *Plan) error { + // Format pixels. + fb := uint32(l^1) << 13 // level: L=01, M=00, Q=11, H=10 + fb |= uint32(m) << 10 // mask + const formatPoly = 0x537 + rem := fb + for i := 14; i >= 10; i-- { + if rem&(1<>i)&1 == 1 { + pix |= Black + } + if (invert>>i)&1 == 1 { + pix ^= Invert | Black + } + // top left + switch { + case i < 6: + p.Pixel[i][8] = pix + case i < 8: + p.Pixel[i+1][8] = pix + case i < 9: + p.Pixel[8][7] = pix + default: + p.Pixel[8][14-i] = pix + } + // bottom right + switch { + case i < 8: + p.Pixel[8][siz-1-int(i)] = pix + default: + p.Pixel[siz-1-int(14-i)][8] = pix + } + } + return nil +} + +// lplan edits a version-only Plan to add information +// about the error correction levels. +func lplan(v Version, l Level, p *Plan) error { + p.Level = l + + nblock := vtab[v].level[l].nblock + ne := vtab[v].level[l].check + nde := (vtab[v].bytes - ne*nblock) / nblock + extra := (vtab[v].bytes - ne*nblock) % nblock + dataBits := (nde*nblock + extra) * 8 + checkBits := ne * nblock * 8 + + p.DataBytes = vtab[v].bytes - ne*nblock + p.CheckBytes = ne * nblock + p.Blocks = nblock + + // Make data + checksum pixels. + data := make([]Pixel, dataBits) + for i := range data { + data[i] = Data.Pixel() | OffsetPixel(uint(i)) + } + check := make([]Pixel, checkBits) + for i := range check { + check[i] = Check.Pixel() | OffsetPixel(uint(i+dataBits)) + } + + // Split into blocks. + dataList := make([][]Pixel, nblock) + checkList := make([][]Pixel, nblock) + for i := 0; i < nblock; i++ { + // The last few blocks have an extra data byte (8 pixels). + nd := nde + if i >= nblock-extra { + nd++ + } + dataList[i], data = data[0:nd*8], data[nd*8:] + checkList[i], check = check[0:ne*8], check[ne*8:] + } + if len(data) != 0 || len(check) != 0 { + panic("data/check math") + } + + // Build up bit sequence, taking first byte of each block, + // then second byte, and so on. Then checksums. + bits := make([]Pixel, dataBits+checkBits) + dst := bits + for i := 0; i < nde+1; i++ { + for _, b := range dataList { + if i*8 < len(b) { + copy(dst, b[i*8:(i+1)*8]) + dst = dst[8:] + } + } + } + for i := 0; i < ne; i++ { + for _, b := range checkList { + if i*8 < len(b) { + copy(dst, b[i*8:(i+1)*8]) + dst = dst[8:] + } + } + } + if len(dst) != 0 { + panic("dst math") + } + + // Sweep up pair of columns, + // then down, assigning to right then left pixel. + // Repeat. + // See Figure 2 of http://www.pclviewer.com/rs2/qrtopology.htm + siz := len(p.Pixel) + rem := make([]Pixel, 7) + for i := range rem { + rem[i] = Extra.Pixel() + } + src := append(bits, rem...) + for x := siz; x > 0; { + for y := siz - 1; y >= 0; y-- { + if p.Pixel[y][x-1].Role() == 0 { + p.Pixel[y][x-1], src = src[0], src[1:] + } + if p.Pixel[y][x-2].Role() == 0 { + p.Pixel[y][x-2], src = src[0], src[1:] + } + } + x -= 2 + if x == 7 { // vertical timing strip + x-- + } + for y := 0; y < siz; y++ { + if p.Pixel[y][x-1].Role() == 0 { + p.Pixel[y][x-1], src = src[0], src[1:] + } + if p.Pixel[y][x-2].Role() == 0 { + p.Pixel[y][x-2], src = src[0], src[1:] + } + } + x -= 2 + } + return nil +} + +// mplan edits a version+level-only Plan to add the mask. +func mplan(m Mask, p *Plan) error { + p.Mask = m + for y, row := range p.Pixel { + for x, pix := range row { + if r := pix.Role(); (r == Data || r == Check || r == Extra) && p.Mask.Invert(y, x) { + row[x] ^= Black | Invert + } + } + } + return nil +} + +// posBox draws a position (large) box at upper left x, y. +func posBox(m [][]Pixel, x, y int) { + pos := Position.Pixel() + // box + for dy := 0; dy < 7; dy++ { + for dx := 0; dx < 7; dx++ { + p := pos + if dx == 0 || dx == 6 || dy == 0 || dy == 6 || 2 <= dx && dx <= 4 && 2 <= dy && dy <= 4 { + p |= Black + } + m[y+dy][x+dx] = p + } + } + // white border + for dy := -1; dy < 8; dy++ { + if 0 <= y+dy && y+dy < len(m) { + if x > 0 { + m[y+dy][x-1] = pos + } + if x+7 < len(m) { + m[y+dy][x+7] = pos + } + } + } + for dx := -1; dx < 8; dx++ { + if 0 <= x+dx && x+dx < len(m) { + if y > 0 { + m[y-1][x+dx] = pos + } + if y+7 < len(m) { + m[y+7][x+dx] = pos + } + } + } +} + +// alignBox draw an alignment (small) box at upper left x, y. +func alignBox(m [][]Pixel, x, y int) { + // box + align := Alignment.Pixel() + for dy := 0; dy < 5; dy++ { + for dx := 0; dx < 5; dx++ { + p := align + if dx == 0 || dx == 4 || dy == 0 || dy == 4 || dx == 2 && dy == 2 { + p |= Black + } + m[y+dy][x+dx] = p + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/coding/qr_test.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/coding/qr_test.go new file mode 100644 index 00000000..d667a8bb --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/coding/qr_test.go @@ -0,0 +1,133 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package coding + +import ( + "bytes" + "testing" + + "camlistore.org/third_party/code.google.com/p/rsc/gf256" + "camlistore.org/third_party/code.google.com/p/rsc/qr/libqrencode" +) + +func test(t *testing.T, v Version, l Level, text ...Encoding) bool { + s := "" + ty := libqrencode.EightBit + switch x := text[0].(type) { + case String: + s = string(x) + case Alpha: + s = string(x) + ty = libqrencode.Alphanumeric + case Num: + s = string(x) + ty = libqrencode.Numeric + } + key, err := libqrencode.Encode(libqrencode.Version(v), libqrencode.Level(l), ty, s) + if err != nil { + t.Errorf("libqrencode.Encode(%v, %v, %d, %#q): %v", v, l, ty, s, err) + return false + } + mask := (^key.Pixel[8][2]&1)<<2 | (key.Pixel[8][3]&1)<<1 | (^key.Pixel[8][4] & 1) + p, err := NewPlan(v, l, Mask(mask)) + if err != nil { + t.Errorf("NewPlan(%v, L, %d): %v", v, err, mask) + return false + } + if len(p.Pixel) != len(key.Pixel) { + t.Errorf("%v: NewPlan uses %dx%d, libqrencode uses %dx%d", v, len(p.Pixel), len(p.Pixel), len(key.Pixel), len(key.Pixel)) + return false + } + c, err := p.Encode(text...) + if err != nil { + t.Errorf("Encode: %v", err) + return false + } + badpix := 0 +Pixel: + for y, prow := range p.Pixel { + for x, pix := range prow { + pix &^= Black + if c.Black(x, y) { + pix |= Black + } + + keypix := key.Pixel[y][x] + want := Pixel(0) + switch { + case keypix&libqrencode.Finder != 0: + want = Position.Pixel() + case keypix&libqrencode.Alignment != 0: + want = Alignment.Pixel() + case keypix&libqrencode.Timing != 0: + want = Timing.Pixel() + case keypix&libqrencode.Format != 0: + want = Format.Pixel() + want |= OffsetPixel(pix.Offset()) // sic + want |= pix & Invert + case keypix&libqrencode.PVersion != 0: + want = PVersion.Pixel() + case keypix&libqrencode.DataECC != 0: + if pix.Role() == Check || pix.Role() == Extra { + want = pix.Role().Pixel() + } else { + want = Data.Pixel() + } + want |= OffsetPixel(pix.Offset()) + want |= pix & Invert + default: + want = Unused.Pixel() + } + if keypix&libqrencode.Black != 0 { + want |= Black + } + if pix != want { + t.Errorf("%v/%v: Pixel[%d][%d] = %v, want %v %#x", v, mask, y, x, pix, want, keypix) + if badpix++; badpix >= 100 { + t.Errorf("stopping after %d bad pixels", badpix) + break Pixel + } + } + } + } + return badpix == 0 +} + +var input = []Encoding{ + String("hello"), + Num("1"), + Num("12"), + Num("123"), + Alpha("AB"), + Alpha("ABC"), +} + +func TestVersion(t *testing.T) { + badvers := 0 +Version: + for v := Version(1); v <= 40; v++ { + for l := L; l <= H; l++ { + for _, in := range input { + if !test(t, v, l, in) { + if badvers++; badvers >= 10 { + t.Errorf("stopping after %d bad versions", badvers) + break Version + } + } + } + } + } +} + +func TestEncode(t *testing.T) { + data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11} + check := []byte{0xa5, 0x24, 0xd4, 0xc1, 0xed, 0x36, 0xc7, 0x87, 0x2c, 0x55} + rs := gf256.NewRSEncoder(Field, len(check)) + out := make([]byte, len(check)) + rs.ECC(data, out) + if !bytes.Equal(out, check) { + t.Errorf("have %x want %x", out, check) + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/png.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/png.go new file mode 100644 index 00000000..db49d057 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/png.go @@ -0,0 +1,400 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package qr + +// PNG writer for QR codes. + +import ( + "bytes" + "encoding/binary" + "hash" + "hash/crc32" +) + +// PNG returns a PNG image displaying the code. +// +// PNG uses a custom encoder tailored to QR codes. +// Its compressed size is about 2x away from optimal, +// but it runs about 20x faster than calling png.Encode +// on c.Image(). +func (c *Code) PNG() []byte { + var p pngWriter + return p.encode(c) +} + +type pngWriter struct { + tmp [16]byte + wctmp [4]byte + buf bytes.Buffer + zlib bitWriter + crc hash.Hash32 +} + +var pngHeader = []byte("\x89PNG\r\n\x1a\n") + +func (w *pngWriter) encode(c *Code) []byte { + scale := c.Scale + siz := c.Size + + w.buf.Reset() + + // Header + w.buf.Write(pngHeader) + + // Header block + binary.BigEndian.PutUint32(w.tmp[0:4], uint32((siz+8)*scale)) + binary.BigEndian.PutUint32(w.tmp[4:8], uint32((siz+8)*scale)) + w.tmp[8] = 1 // 1-bit + w.tmp[9] = 0 // gray + w.tmp[10] = 0 + w.tmp[11] = 0 + w.tmp[12] = 0 + w.writeChunk("IHDR", w.tmp[:13]) + + // Comment + w.writeChunk("tEXt", comment) + + // Data + w.zlib.writeCode(c) + w.writeChunk("IDAT", w.zlib.bytes.Bytes()) + + // End + w.writeChunk("IEND", nil) + + return w.buf.Bytes() +} + +var comment = []byte("Software\x00QR-PNG http://qr.swtch.com/") + +func (w *pngWriter) writeChunk(name string, data []byte) { + if w.crc == nil { + w.crc = crc32.NewIEEE() + } + binary.BigEndian.PutUint32(w.wctmp[0:4], uint32(len(data))) + w.buf.Write(w.wctmp[0:4]) + w.crc.Reset() + copy(w.wctmp[0:4], name) + w.buf.Write(w.wctmp[0:4]) + w.crc.Write(w.wctmp[0:4]) + w.buf.Write(data) + w.crc.Write(data) + crc := w.crc.Sum32() + binary.BigEndian.PutUint32(w.wctmp[0:4], crc) + w.buf.Write(w.wctmp[0:4]) +} + +func (b *bitWriter) writeCode(c *Code) { + const ftNone = 0 + + b.adler32.Reset() + b.bytes.Reset() + b.nbit = 0 + + scale := c.Scale + siz := c.Size + + // zlib header + b.tmp[0] = 0x78 + b.tmp[1] = 0 + b.tmp[1] += uint8(31 - (uint16(b.tmp[0])<<8+uint16(b.tmp[1]))%31) + b.bytes.Write(b.tmp[0:2]) + + // Start flate block. + b.writeBits(1, 1, false) // final block + b.writeBits(1, 2, false) // compressed, fixed Huffman tables + + // White border. + // First row. + b.byte(ftNone) + n := (scale*(siz+8) + 7) / 8 + b.byte(255) + b.repeat(n-1, 1) + // 4*scale rows total. + b.repeat((4*scale-1)*(1+n), 1+n) + + for i := 0; i < 4*scale; i++ { + b.adler32.WriteNByte(ftNone, 1) + b.adler32.WriteNByte(255, n) + } + + row := make([]byte, 1+n) + for y := 0; y < siz; y++ { + row[0] = ftNone + j := 1 + var z uint8 + nz := 0 + for x := -4; x < siz+4; x++ { + // Raw data. + for i := 0; i < scale; i++ { + z <<= 1 + if !c.Black(x, y) { + z |= 1 + } + if nz++; nz == 8 { + row[j] = z + j++ + nz = 0 + } + } + } + if j < len(row) { + row[j] = z + } + for _, z := range row { + b.byte(z) + } + + // Scale-1 copies. + b.repeat((scale-1)*(1+n), 1+n) + + b.adler32.WriteN(row, scale) + } + + // White border. + // First row. + b.byte(ftNone) + b.byte(255) + b.repeat(n-1, 1) + // 4*scale rows total. + b.repeat((4*scale-1)*(1+n), 1+n) + + for i := 0; i < 4*scale; i++ { + b.adler32.WriteNByte(ftNone, 1) + b.adler32.WriteNByte(255, n) + } + + // End of block. + b.hcode(256) + b.flushBits() + + // adler32 + binary.BigEndian.PutUint32(b.tmp[0:], b.adler32.Sum32()) + b.bytes.Write(b.tmp[0:4]) +} + +// A bitWriter is a write buffer for bit-oriented data like deflate. +type bitWriter struct { + bytes bytes.Buffer + bit uint32 + nbit uint + + tmp [4]byte + adler32 adigest +} + +func (b *bitWriter) writeBits(bit uint32, nbit uint, rev bool) { + // reverse, for huffman codes + if rev { + br := uint32(0) + for i := uint(0); i < nbit; i++ { + br |= ((bit >> i) & 1) << (nbit - 1 - i) + } + bit = br + } + b.bit |= bit << b.nbit + b.nbit += nbit + for b.nbit >= 8 { + b.bytes.WriteByte(byte(b.bit)) + b.bit >>= 8 + b.nbit -= 8 + } +} + +func (b *bitWriter) flushBits() { + if b.nbit > 0 { + b.bytes.WriteByte(byte(b.bit)) + b.nbit = 0 + b.bit = 0 + } +} + +func (b *bitWriter) hcode(v int) { + /* + Lit Value Bits Codes + --------- ---- ----- + 0 - 143 8 00110000 through + 10111111 + 144 - 255 9 110010000 through + 111111111 + 256 - 279 7 0000000 through + 0010111 + 280 - 287 8 11000000 through + 11000111 + */ + switch { + case v <= 143: + b.writeBits(uint32(v)+0x30, 8, true) + case v <= 255: + b.writeBits(uint32(v-144)+0x190, 9, true) + case v <= 279: + b.writeBits(uint32(v-256)+0, 7, true) + case v <= 287: + b.writeBits(uint32(v-280)+0xc0, 8, true) + default: + panic("invalid hcode") + } +} + +func (b *bitWriter) byte(x byte) { + b.hcode(int(x)) +} + +func (b *bitWriter) codex(c int, val int, nx uint) { + b.hcode(c + val>>nx) + b.writeBits(uint32(val)&(1<= 258+3; n -= 258 { + b.repeat1(258, d) + } + if n > 258 { + // 258 < n < 258+3 + b.repeat1(10, d) + b.repeat1(n-10, d) + return + } + if n < 3 { + panic("invalid flate repeat") + } + b.repeat1(n, d) +} + +func (b *bitWriter) repeat1(n, d int) { + /* + Extra Extra Extra + Code Bits Length(s) Code Bits Lengths Code Bits Length(s) + ---- ---- ------ ---- ---- ------- ---- ---- ------- + 257 0 3 267 1 15,16 277 4 67-82 + 258 0 4 268 1 17,18 278 4 83-98 + 259 0 5 269 2 19-22 279 4 99-114 + 260 0 6 270 2 23-26 280 4 115-130 + 261 0 7 271 2 27-30 281 5 131-162 + 262 0 8 272 2 31-34 282 5 163-194 + 263 0 9 273 3 35-42 283 5 195-226 + 264 0 10 274 3 43-50 284 5 227-257 + 265 1 11,12 275 3 51-58 285 0 258 + 266 1 13,14 276 3 59-66 + */ + switch { + case n <= 10: + b.codex(257, n-3, 0) + case n <= 18: + b.codex(265, n-11, 1) + case n <= 34: + b.codex(269, n-19, 2) + case n <= 66: + b.codex(273, n-35, 3) + case n <= 130: + b.codex(277, n-67, 4) + case n <= 257: + b.codex(281, n-131, 5) + case n == 258: + b.hcode(285) + default: + panic("invalid repeat length") + } + + /* + Extra Extra Extra + Code Bits Dist Code Bits Dist Code Bits Distance + ---- ---- ---- ---- ---- ------ ---- ---- -------- + 0 0 1 10 4 33-48 20 9 1025-1536 + 1 0 2 11 4 49-64 21 9 1537-2048 + 2 0 3 12 5 65-96 22 10 2049-3072 + 3 0 4 13 5 97-128 23 10 3073-4096 + 4 1 5,6 14 6 129-192 24 11 4097-6144 + 5 1 7,8 15 6 193-256 25 11 6145-8192 + 6 2 9-12 16 7 257-384 26 12 8193-12288 + 7 2 13-16 17 7 385-512 27 12 12289-16384 + 8 3 17-24 18 8 513-768 28 13 16385-24576 + 9 3 25-32 19 8 769-1024 29 13 24577-32768 + */ + if d <= 4 { + b.writeBits(uint32(d-1), 5, true) + } else if d <= 32768 { + nbit := uint(16) + for d <= 1<<(nbit-1) { + nbit-- + } + v := uint32(d - 1) + v &^= 1 << (nbit - 1) // top bit is implicit + code := uint32(2*nbit - 2) // second bit is low bit of code + code |= v >> (nbit - 2) + v &^= 1 << (nbit - 2) + b.writeBits(code, 5, true) + // rest of bits follow + b.writeBits(uint32(v), nbit-2, false) + } else { + panic("invalid repeat distance") + } +} + +func (b *bitWriter) run(v byte, n int) { + if n == 0 { + return + } + b.byte(v) + if n-1 < 3 { + for i := 0; i < n-1; i++ { + b.byte(v) + } + } else { + b.repeat(n-1, 1) + } +} + +type adigest struct { + a, b uint32 +} + +func (d *adigest) Reset() { d.a, d.b = 1, 0 } + +const amod = 65521 + +func aupdate(a, b uint32, pi byte, n int) (aa, bb uint32) { + // TODO(rsc): 6g doesn't do magic multiplies for b %= amod, + // only for b = b%amod. + + // invariant: a, b < amod + if pi == 0 { + b += uint32(n%amod) * a + b = b % amod + return a, b + } + + // n times: + // a += pi + // b += a + // is same as + // b += n*a + n*(n+1)/2*pi + // a += n*pi + m := uint32(n) + b += (m % amod) * a + b = b % amod + b += (m * (m + 1) / 2) % amod * uint32(pi) + b = b % amod + a += (m % amod) * uint32(pi) + a = a % amod + return a, b +} + +func afinish(a, b uint32) uint32 { + return b<<16 | a +} + +func (d *adigest) WriteN(p []byte, n int) { + for i := 0; i < n; i++ { + for _, pi := range p { + d.a, d.b = aupdate(d.a, d.b, pi, 1) + } + } +} + +func (d *adigest) WriteNByte(pi byte, n int) { + d.a, d.b = aupdate(d.a, d.b, pi, n) +} + +func (d *adigest) Sum32() uint32 { return afinish(d.a, d.b) } diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/png_test.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/png_test.go new file mode 100644 index 00000000..27a62292 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/png_test.go @@ -0,0 +1,73 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package qr + +import ( + "bytes" + "image" + "image/color" + "image/png" + "io/ioutil" + "testing" +) + +func TestPNG(t *testing.T) { + c, err := Encode("hello, world", L) + if err != nil { + t.Fatal(err) + } + pngdat := c.PNG() + if true { + ioutil.WriteFile("x.png", pngdat, 0666) + } + m, err := png.Decode(bytes.NewBuffer(pngdat)) + if err != nil { + t.Fatal(err) + } + gm := m.(*image.Gray) + + scale := c.Scale + siz := c.Size + nbad := 0 + for y := 0; y < scale*(8+siz); y++ { + for x := 0; x < scale*(8+siz); x++ { + v := byte(255) + if c.Black(x/scale-4, y/scale-4) { + v = 0 + } + if gv := gm.At(x, y).(color.Gray).Y; gv != v { + t.Errorf("%d,%d = %d, want %d", x, y, gv, v) + if nbad++; nbad >= 20 { + t.Fatalf("too many bad pixels") + } + } + } + } +} + +func BenchmarkPNG(b *testing.B) { + c, err := Encode("0123456789012345678901234567890123456789", L) + if err != nil { + panic(err) + } + var bytes []byte + for i := 0; i < b.N; i++ { + bytes = c.PNG() + } + b.SetBytes(int64(len(bytes))) +} + +func BenchmarkImagePNG(b *testing.B) { + c, err := Encode("0123456789012345678901234567890123456789", L) + if err != nil { + panic(err) + } + var buf bytes.Buffer + for i := 0; i < b.N; i++ { + buf.Reset() + png.Encode(&buf, c.Image()) + } + b.SetBytes(int64(buf.Len())) +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/qr.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/qr.go new file mode 100644 index 00000000..3de4a1a4 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/qr.go @@ -0,0 +1,116 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package qr encodes QR codes. +*/ +package qr + +import ( + "errors" + "image" + "image/color" + + "camlistore.org/third_party/code.google.com/p/rsc/qr/coding" +) + +// A Level denotes a QR error correction level. +// From least to most tolerant of errors, they are L, M, Q, H. +type Level int + +const ( + L Level = iota // 20% redundant + M // 38% redundant + Q // 55% redundant + H // 65% redundant +) + +// Encode returns an encoding of text at the given error correction level. +func Encode(text string, level Level) (*Code, error) { + // Pick data encoding, smallest first. + // We could split the string and use different encodings + // but that seems like overkill for now. + var enc coding.Encoding + switch { + case coding.Num(text).Check() == nil: + enc = coding.Num(text) + case coding.Alpha(text).Check() == nil: + enc = coding.Alpha(text) + default: + enc = coding.String(text) + } + + // Pick size. + l := coding.Level(level) + var v coding.Version + for v = coding.MinVersion; ; v++ { + if v > coding.MaxVersion { + return nil, errors.New("text too long to encode as QR") + } + if enc.Bits(v) <= v.DataBytes(l)*8 { + break + } + } + + // Build and execute plan. + p, err := coding.NewPlan(v, l, 0) + if err != nil { + return nil, err + } + cc, err := p.Encode(enc) + if err != nil { + return nil, err + } + + // TODO: Pick appropriate mask. + + return &Code{cc.Bitmap, cc.Size, cc.Stride, 8}, nil +} + +// A Code is a square pixel grid. +// It implements image.Image and direct PNG encoding. +type Code struct { + Bitmap []byte // 1 is black, 0 is white + Size int // number of pixels on a side + Stride int // number of bytes per row + Scale int // number of image pixels per QR pixel +} + +// Black returns true if the pixel at (x,y) is black. +func (c *Code) Black(x, y int) bool { + return 0 <= x && x < c.Size && 0 <= y && y < c.Size && + c.Bitmap[y*c.Stride+x/8]&(1<> 24), byte(rgba >> 16), byte(rgba >> 8), byte(rgba)} + draw.Draw(c, r, u, image.ZP, draw.Src) + } + } + + if csize != 0 { + if font == "" { + font = "data/luxisr.ttf" + } + ctxt := fs.NewContext(req) + dat, _, err := ctxt.Read(font) + if err != nil { + panic(err) + } + tfont, err := freetype.ParseFont(dat) + if err != nil { + panic(err) + } + ft := freetype.NewContext() + ft.SetDst(c) + ft.SetDPI(100) + ft.SetFont(tfont) + ft.SetFontSize(float64(pt)) + ft.SetSrc(image.NewUniform(color.Black)) + ft.SetClip(image.Rect(0, 0, 0, 0)) + wid, err := ft.DrawString(caption, freetype.Pt(0, 0)) + if err != nil { + panic(err) + } + p := freetype.Pt(d, d+3*pt/2) + p.X -= wid.X + p.X /= 2 + ft.SetClip(c.Bounds()) + ft.DrawString(caption, p) + } + + return c +} + +func makeFrame(req *http.Request, font string, pt, vers, l, scale, dots int) image.Image { + lev := coding.Level(l) + p, err := coding.NewPlan(coding.Version(vers), lev, 0) + if err != nil { + panic(err) + } + + nd := p.DataBytes / p.Blocks + nc := p.CheckBytes / p.Blocks + extra := p.DataBytes - nd*p.Blocks + + cap := fmt.Sprintf("QR v%d, %s", vers, lev) + if dots > 0 { + cap = fmt.Sprintf("QR v%d order, from bottom right", vers) + } + m := makeImage(req, cap, font, pt, len(p.Pixel), 0, scale, func(x, y int) uint32 { + pix := p.Pixel[y][x] + switch pix.Role() { + case coding.Data: + if dots > 0 { + return 0xffffffff + } + off := int(pix.Offset() / 8) + nd := nd + var i int + for i = 0; i < p.Blocks; i++ { + if i == extra { + nd++ + } + if off < nd { + break + } + off -= nd + } + return blockColors[i%len(blockColors)] + case coding.Check: + if dots > 0 { + return 0xffffffff + } + i := (int(pix.Offset()/8) - p.DataBytes) / nc + return dark(blockColors[i%len(blockColors)]) + } + if pix&coding.Black != 0 { + return 0x000000ff + } + return 0xffffffff + }) + + if dots > 0 { + b := m.Bounds() + for y := 0; y <= len(p.Pixel); y++ { + for x := 0; x < b.Dx(); x++ { + m.SetRGBA(x, y*scale-(y/len(p.Pixel)), color.RGBA{127, 127, 127, 255}) + } + } + for x := 0; x <= len(p.Pixel); x++ { + for y := 0; y < b.Dx(); y++ { + m.SetRGBA(x*scale-(x/len(p.Pixel)), y, color.RGBA{127, 127, 127, 255}) + } + } + order := make([]image.Point, (p.DataBytes+p.CheckBytes)*8+1) + for y, row := range p.Pixel { + for x, pix := range row { + if r := pix.Role(); r != coding.Data && r != coding.Check { + continue + } + // draw.Draw(m, m.Bounds().Add(image.Pt(x*scale, y*scale)), dot, image.ZP, draw.Over) + order[pix.Offset()] = image.Point{x*scale + scale/2, y*scale + scale/2} + } + } + + for mode := 0; mode < 2; mode++ { + for i, p := range order { + q := order[i+1] + if q.X == 0 { + break + } + line(m, p, q, mode) + } + } + } + return m +} + +func line(m *image.RGBA, p, q image.Point, mode int) { + x := 0 + y := 0 + dx := q.X - p.X + dy := q.Y - p.Y + xsign := +1 + ysign := +1 + if dx < 0 { + xsign = -1 + dx = -dx + } + if dy < 0 { + ysign = -1 + dy = -dy + } + pt := func() { + switch mode { + case 0: + for dx := -2; dx <= 2; dx++ { + for dy := -2; dy <= 2; dy++ { + if dy*dx <= -4 || dy*dx >= 4 { + continue + } + m.SetRGBA(p.X+x*xsign+dx, p.Y+y*ysign+dy, color.RGBA{255, 192, 192, 255}) + } + } + + case 1: + m.SetRGBA(p.X+x*xsign, p.Y+y*ysign, color.RGBA{128, 0, 0, 255}) + } + } + if dx > dy { + for x < dx || y < dy { + pt() + x++ + if float64(x)*float64(dy)/float64(dx)-float64(y) > 0.5 { + y++ + } + } + } else { + for x < dx || y < dy { + pt() + y++ + if float64(y)*float64(dx)/float64(dy)-float64(x) > 0.5 { + x++ + } + } + } + pt() +} + +func pngEncode(c image.Image) []byte { + var b bytes.Buffer + png.Encode(&b, c) + return b.Bytes() +} + +// Frame handles a request for a single QR frame. +func Frame(w http.ResponseWriter, req *http.Request) { + arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x } + v := arg("v") + scale := arg("scale") + if scale == 0 { + scale = 8 + } + + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(pngEncode(makeFrame(req, req.FormValue("font"), arg("pt"), v, arg("l"), scale, arg("dots")))) +} + +// Frames handles a request for multiple QR frames. +func Frames(w http.ResponseWriter, req *http.Request) { + vs := strings.Split(req.FormValue("v"), ",") + + arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x } + scale := arg("scale") + if scale == 0 { + scale = 8 + } + font := req.FormValue("font") + pt := arg("pt") + dots := arg("dots") + + var images []image.Image + l := arg("l") + for _, v := range vs { + l := l + if i := strings.Index(v, "."); i >= 0 { + l, _ = strconv.Atoi(v[i+1:]) + v = v[:i] + } + vv, _ := strconv.Atoi(v) + images = append(images, makeFrame(req, font, pt, vv, l, scale, dots)) + } + + b := images[len(images)-1].Bounds() + + dx := arg("dx") + if dx == 0 { + dx = b.Dx() + } + x, y := 0, 0 + xmax := 0 + sep := arg("sep") + if sep == 0 { + sep = 10 + } + var points []image.Point + for i, m := range images { + if x > 0 { + x += sep + } + if x > 0 && x+m.Bounds().Dx() > dx { + y += sep + images[i-1].Bounds().Dy() + x = 0 + } + points = append(points, image.Point{x, y}) + x += m.Bounds().Dx() + if x > xmax { + xmax = x + } + + } + + c := image.NewRGBA(image.Rect(0, 0, xmax, y+b.Dy())) + for i, m := range images { + draw.Draw(c, c.Bounds().Add(points[i]), m, image.ZP, draw.Src) + } + + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(pngEncode(c)) +} + +// Mask handles a request for a single QR mask. +func Mask(w http.ResponseWriter, req *http.Request) { + arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x } + v := arg("v") + m := arg("m") + scale := arg("scale") + if scale == 0 { + scale = 8 + } + + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(pngEncode(makeMask(req, req.FormValue("font"), arg("pt"), v, m, scale))) +} + +// Masks handles a request for multiple QR masks. +func Masks(w http.ResponseWriter, req *http.Request) { + arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x } + v := arg("v") + scale := arg("scale") + if scale == 0 { + scale = 8 + } + font := req.FormValue("font") + pt := arg("pt") + var mm []image.Image + for m := 0; m < 8; m++ { + mm = append(mm, makeMask(req, font, pt, v, m, scale)) + } + dx := mm[0].Bounds().Dx() + dy := mm[0].Bounds().Dy() + + sep := arg("sep") + if sep == 0 { + sep = 10 + } + c := image.NewRGBA(image.Rect(0, 0, (dx+sep)*4-sep, (dy+sep)*2-sep)) + for m := 0; m < 8; m++ { + x := (m % 4) * (dx + sep) + y := (m / 4) * (dy + sep) + draw.Draw(c, c.Bounds().Add(image.Pt(x, y)), mm[m], image.ZP, draw.Src) + } + + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(pngEncode(c)) +} + +var maskName = []string{ + "(x+y) % 2", + "y % 2", + "x % 3", + "(x+y) % 3", + "(y/2 + x/3) % 2", + "xy%2 + xy%3", + "(xy%2 + xy%3) % 2", + "(xy%3 + (x+y)%2) % 2", +} + +func makeMask(req *http.Request, font string, pt int, vers, mask, scale int) image.Image { + p, err := coding.NewPlan(coding.Version(vers), coding.L, coding.Mask(mask)) + if err != nil { + panic(err) + } + m := makeImage(req, maskName[mask], font, pt, len(p.Pixel), 0, scale, func(x, y int) uint32 { + pix := p.Pixel[y][x] + switch pix.Role() { + case coding.Data, coding.Check: + if pix&coding.Invert != 0 { + return 0x000000ff + } + } + return 0xffffffff + }) + return m +} + +var blockColors = []uint32{ + 0x7777ffff, + 0xffff77ff, + 0xff7777ff, + 0x77ffffff, + 0x1e90ffff, + 0xffffe0ff, + 0x8b6969ff, + 0x77ff77ff, + 0x9b30ffff, + 0x00bfffff, + 0x90e890ff, + 0xfff68fff, + 0xffec8bff, + 0xffa07aff, + 0xffa54fff, + 0xeee8aaff, + 0x98fb98ff, + 0xbfbfbfff, + 0x54ff9fff, + 0xffaeb9ff, + 0xb23aeeff, + 0xbbffffff, + 0x7fffd4ff, + 0xff7a7aff, + 0x00007fff, +} + +func dark(x uint32) uint32 { + r, g, b, a := byte(x>>24), byte(x>>16), byte(x>>8), byte(x) + r = r/2 + r/4 + g = g/2 + g/4 + b = b/2 + b/4 + return uint32(r)<<24 | uint32(g)<<16 | uint32(b)<<8 | uint32(a) +} + +func clamp(x int) byte { + if x < 0 { + return 0 + } + if x > 255 { + return 255 + } + return byte(x) +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} + +// Arrow handles a request for an arrow pointing in a given direction. +func Arrow(w http.ResponseWriter, req *http.Request) { + arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x } + dir := arg("dir") + size := arg("size") + if size == 0 { + size = 50 + } + del := size / 10 + + m := image.NewRGBA(image.Rect(0, 0, size, size)) + + if dir == 4 { + draw.Draw(m, m.Bounds(), image.Black, image.ZP, draw.Src) + draw.Draw(m, image.Rect(5, 5, size-5, size-5), image.White, image.ZP, draw.Src) + } + + pt := func(x, y int, c color.RGBA) { + switch dir { + case 0: + m.SetRGBA(x, y, c) + case 1: + m.SetRGBA(y, size-1-x, c) + case 2: + m.SetRGBA(size-1-x, size-1-y, c) + case 3: + m.SetRGBA(size-1-y, x, c) + } + } + + for y := 0; y < size/2; y++ { + for x := 0; x < del && x < y; x++ { + pt(x, y, color.RGBA{0, 0, 0, 255}) + } + for x := del; x < y-del; x++ { + pt(x, y, color.RGBA{128, 128, 255, 255}) + } + for x := max(y-del, 0); x <= y; x++ { + pt(x, y, color.RGBA{0, 0, 0, 255}) + } + } + for y := size / 2; y < size; y++ { + for x := 0; x < del && x < size-1-y; x++ { + pt(x, y, color.RGBA{0, 0, 0, 255}) + } + for x := del; x < size-1-y-del; x++ { + pt(x, y, color.RGBA{128, 128, 192, 255}) + } + for x := max(size-1-y-del, 0); x <= size-1-y; x++ { + pt(x, y, color.RGBA{0, 0, 0, 255}) + } + } + + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(pngEncode(m)) +} + +// Encode encodes a string using the given version, level, and mask. +func Encode(w http.ResponseWriter, req *http.Request) { + val := func(s string) int { + v, _ := strconv.Atoi(req.FormValue(s)) + return v + } + + l := coding.Level(val("l")) + v := coding.Version(val("v")) + enc := coding.String(req.FormValue("t")) + m := coding.Mask(val("m")) + + p, err := coding.NewPlan(v, l, m) + if err != nil { + panic(err) + } + cc, err := p.Encode(enc) + if err != nil { + panic(err) + } + + c := &qr.Code{Bitmap: cc.Bitmap, Size: cc.Size, Stride: cc.Stride, Scale: 8} + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(c.PNG()) +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/web/play.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/web/play.go new file mode 100644 index 00000000..68cd0078 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/web/play.go @@ -0,0 +1,1118 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +QR data layout + +qr/ + upload/ + id.png + id.fix + flag/ + id + +*/ +// TODO: Random seed taken from GET for caching, repeatability. +// TODO: Flag for abuse button + some kind of dashboard. +// TODO: +1 button on web page? permalink? +// TODO: Flag for abuse button on permalinks too? +// TODO: Make the page prettier. +// TODO: Cache headers. + +package web + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/json" + "fmt" + "html/template" + "image" + "image/color" + _ "image/gif" + "image/png" + "io" + "math/rand" + "net/http" + "net/url" + "os" + "sort" + "strconv" + "strings" + "time" + + "camlistore.org/third_party/code.google.com/p/rsc/appfs/fs" + "camlistore.org/third_party/code.google.com/p/rsc/gf256" + "camlistore.org/third_party/code.google.com/p/rsc/qr" + "camlistore.org/third_party/code.google.com/p/rsc/qr/coding" + "camlistore.org/third_party/code.google.com/p/rsc/qr/web/resize" + _ "camlistore.org/third_party/go/pkg/image/jpeg" +) + +func runTemplate(c *fs.Context, w http.ResponseWriter, name string, data interface{}) { + t := template.New("main") + + main, _, err := c.Read(name) + if err != nil { + panic(err) + } + style, _, _ := c.Read("style.html") + main = append(main, style...) + _, err = t.Parse(string(main)) + if err != nil { + panic(err) + } + + var buf bytes.Buffer + if err := t.Execute(&buf, &data); err != nil { + panic(err) + } + w.Write(buf.Bytes()) +} + +func isImgName(s string) bool { + if len(s) != 32 { + return false + } + for i := 0; i < len(s); i++ { + if '0' <= s[i] && s[i] <= '9' || 'a' <= s[i] && s[i] <= 'f' { + continue + } + return false + } + return true +} + +func isTagName(s string) bool { + if len(s) != 16 { + return false + } + for i := 0; i < len(s); i++ { + if '0' <= s[i] && s[i] <= '9' || 'a' <= s[i] && s[i] <= 'f' { + continue + } + return false + } + return true +} + +// Draw is the handler for drawing a QR code. +func Draw(w http.ResponseWriter, req *http.Request) { + ctxt := fs.NewContext(req) + + url := req.FormValue("url") + if url == "" { + url = "http://swtch.com/qr" + } + if req.FormValue("upload") == "1" { + upload(w, req, url) + return + } + + t0 := time.Now() + img := req.FormValue("i") + if !isImgName(img) { + img = "pjw" + } + if req.FormValue("show") == "png" { + i := loadSize(ctxt, img, 48) + var buf bytes.Buffer + png.Encode(&buf, i) + w.Write(buf.Bytes()) + return + } + if req.FormValue("flag") == "1" { + flag(w, req, img, ctxt) + return + } + if req.FormValue("x") == "" { + var data = struct { + Name string + URL string + }{ + Name: img, + URL: url, + } + runTemplate(ctxt, w, "qr/main.html", &data) + return + } + + arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x } + targ := makeTarg(ctxt, img, 17+4*arg("v")+arg("z")) + + m := &Image{ + Name: img, + Dx: arg("x"), + Dy: arg("y"), + URL: req.FormValue("u"), + Version: arg("v"), + Mask: arg("m"), + RandControl: arg("r") > 0, + Dither: arg("i") > 0, + OnlyDataBits: arg("d") > 0, + SaveControl: arg("c") > 0, + Scale: arg("scale"), + Target: targ, + Seed: int64(arg("s")), + Rotation: arg("o"), + Size: arg("z"), + } + if m.Version > 8 { + m.Version = 8 + } + + if m.Scale == 0 { + if arg("l") > 1 { + m.Scale = 8 + } else { + m.Scale = 4 + } + } + if m.Version >= 12 && m.Scale >= 4 { + m.Scale /= 2 + } + + if arg("l") == 1 { + data, err := json.Marshal(m) + if err != nil { + panic(err) + } + h := md5.New() + h.Write(data) + tag := fmt.Sprintf("%x", h.Sum(nil))[:16] + if err := ctxt.Write("qrsave/"+tag, data); err != nil { + panic(err) + } + http.Redirect(w, req, "/qr/show/"+tag, http.StatusTemporaryRedirect) + return + } + + if err := m.Encode(req); err != nil { + fmt.Fprintf(w, "%s\n", err) + return + } + + var dat []byte + switch { + case m.SaveControl: + dat = m.Control + default: + dat = m.Code.PNG() + } + + if arg("l") > 0 { + w.Header().Set("Content-Type", "image/png") + w.Write(dat) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprint(w, "

    ") + fmt.Fprintf(w, "
    \n", m.Link()) + fmt.Fprintf(w, "
    \n") + fmt.Fprintf(w, "
    %v
    \n", time.Now().Sub(t0)) +} + +func (m *Image) Small() bool { + return 8*(17+4*int(m.Version)) < 512 +} + +func (m *Image) Link() string { + s := fmt.Sprint + b := func(v bool) string { + if v { + return "1" + } + return "0" + } + val := url.Values{ + "i": {m.Name}, + "x": {s(m.Dx)}, + "y": {s(m.Dy)}, + "z": {s(m.Size)}, + "u": {m.URL}, + "v": {s(m.Version)}, + "m": {s(m.Mask)}, + "r": {b(m.RandControl)}, + "t": {b(m.Dither)}, + "d": {b(m.OnlyDataBits)}, + "c": {b(m.SaveControl)}, + "s": {s(m.Seed)}, + } + return "/qr/draw?" + val.Encode() +} + +// Show is the handler for showing a stored QR code. +func Show(w http.ResponseWriter, req *http.Request) { + ctxt := fs.NewContext(req) + tag := req.URL.Path[len("/qr/show/"):] + png := strings.HasSuffix(tag, ".png") + if png { + tag = tag[:len(tag)-len(".png")] + } + if !isTagName(tag) { + fmt.Fprintf(w, "Sorry, QR code not found\n") + return + } + if req.FormValue("flag") == "1" { + flag(w, req, tag, ctxt) + return + } + data, _, err := ctxt.Read("qrsave/" + tag) + if err != nil { + fmt.Fprintf(w, "Sorry, QR code not found.\n") + return + } + + var m Image + if err := json.Unmarshal(data, &m); err != nil { + panic(err) + } + m.Tag = tag + + switch req.FormValue("size") { + case "big": + m.Scale *= 2 + case "small": + m.Scale /= 2 + } + + if png { + if err := m.Encode(req); err != nil { + panic(err) + return + } + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(m.Code.PNG()) + return + } + + w.Header().Set("Cache-Control", "public, max-age=300") + runTemplate(ctxt, w, "qr/permalink.html", &m) +} + +func upload(w http.ResponseWriter, req *http.Request, link string) { + // Upload of a new image. + // Copied from Moustachio demo. + f, _, err := req.FormFile("image") + if err != nil { + fmt.Fprintf(w, "You need to select an image to upload.\n") + return + } + defer f.Close() + + i, _, err := image.Decode(f) + if err != nil { + panic(err) + } + + // Convert image to 128x128 gray+alpha. + b := i.Bounds() + const max = 128 + // If it's gigantic, it's more efficient to downsample first + // and then resize; resizing will smooth out the roughness. + var i1 *image.RGBA + if b.Dx() > 4*max || b.Dy() > 4*max { + w, h := 2*max, 2*max + if b.Dx() > b.Dy() { + h = b.Dy() * h / b.Dx() + } else { + w = b.Dx() * w / b.Dy() + } + i1 = resize.Resample(i, b, w, h) + } else { + // "Resample" to same size, just to convert to RGBA. + i1 = resize.Resample(i, b, b.Dx(), b.Dy()) + } + b = i1.Bounds() + + // Encode to PNG. + dx, dy := 128, 128 + if b.Dx() > b.Dy() { + dy = b.Dy() * dx / b.Dx() + } else { + dx = b.Dx() * dy / b.Dy() + } + i128 := resize.ResizeRGBA(i1, i1.Bounds(), dx, dy) + + var buf bytes.Buffer + if err := png.Encode(&buf, i128); err != nil { + panic(err) + } + + h := md5.New() + h.Write(buf.Bytes()) + tag := fmt.Sprintf("%x", h.Sum(nil))[:32] + + ctxt := fs.NewContext(req) + if err := ctxt.Write("qr/upload/"+tag+".png", buf.Bytes()); err != nil { + panic(err) + } + + // Redirect with new image tag. + // Redirect to draw with new image tag. + http.Redirect(w, req, req.URL.Path+"?"+url.Values{"i": {tag}, "url": {link}}.Encode(), 302) +} + +func flag(w http.ResponseWriter, req *http.Request, img string, ctxt *fs.Context) { + if !isImgName(img) && !isTagName(img) { + fmt.Fprintf(w, "Invalid image.\n") + return + } + data, _, _ := ctxt.Read("qr/flag/" + img) + data = append(data, '!') + ctxt.Write("qr/flag/"+img, data) + + fmt.Fprintf(w, "Thank you. The image has been reported.\n") +} + +func loadSize(ctxt *fs.Context, name string, max int) *image.RGBA { + data, _, err := ctxt.Read("qr/upload/" + name + ".png") + if err != nil { + panic(err) + } + i, _, err := image.Decode(bytes.NewBuffer(data)) + if err != nil { + panic(err) + } + b := i.Bounds() + dx, dy := max, max + if b.Dx() > b.Dy() { + dy = b.Dy() * dx / b.Dx() + } else { + dx = b.Dx() * dy / b.Dy() + } + var irgba *image.RGBA + switch i := i.(type) { + case *image.RGBA: + irgba = resize.ResizeRGBA(i, i.Bounds(), dx, dy) + case *image.NRGBA: + irgba = resize.ResizeNRGBA(i, i.Bounds(), dx, dy) + } + return irgba +} + +func makeTarg(ctxt *fs.Context, name string, max int) [][]int { + i := loadSize(ctxt, name, max) + b := i.Bounds() + dx, dy := b.Dx(), b.Dy() + targ := make([][]int, dy) + arr := make([]int, dx*dy) + for y := 0; y < dy; y++ { + targ[y], arr = arr[:dx], arr[dx:] + row := targ[y] + for x := 0; x < dx; x++ { + p := i.Pix[y*i.Stride+4*x:] + r, g, b, a := p[0], p[1], p[2], p[3] + if a == 0 { + row[x] = -1 + } else { + row[x] = int((299*uint32(r) + 587*uint32(g) + 114*uint32(b) + 500) / 1000) + } + } + } + return targ +} + +type Image struct { + Name string + Target [][]int + Dx int + Dy int + URL string + Tag string + Version int + Mask int + Scale int + Rotation int + Size int + + // RandControl says to pick the pixels randomly. + RandControl bool + Seed int64 + + // Dither says to dither instead of using threshold pixel layout. + Dither bool + + // OnlyDataBits says to use only data bits, not check bits. + OnlyDataBits bool + + // Code is the final QR code. + Code *qr.Code + + // Control is a PNG showing the pixels that we controlled. + // Pixels we don't control are grayed out. + SaveControl bool + Control []byte +} + +type Pixinfo struct { + X int + Y int + Pix coding.Pixel + Targ byte + DTarg int + Contrast int + HardZero bool + Block *BitBlock + Bit uint +} + +type Pixorder struct { + Off int + Priority int +} + +type byPriority []Pixorder + +func (x byPriority) Len() int { return len(x) } +func (x byPriority) Swap(i, j int) { x[i], x[j] = x[j], x[i] } +func (x byPriority) Less(i, j int) bool { return x[i].Priority > x[j].Priority } + +func (m *Image) target(x, y int) (targ byte, contrast int) { + tx := x + m.Dx + ty := y + m.Dy + if ty < 0 || ty >= len(m.Target) || tx < 0 || tx >= len(m.Target[ty]) { + return 255, -1 + } + + v0 := m.Target[ty][tx] + if v0 < 0 { + return 255, -1 + } + targ = byte(v0) + + n := 0 + sum := 0 + sumsq := 0 + const del = 5 + for dy := -del; dy <= del; dy++ { + for dx := -del; dx <= del; dx++ { + if 0 <= ty+dy && ty+dy < len(m.Target) && 0 <= tx+dx && tx+dx < len(m.Target[ty+dy]) { + v := m.Target[ty+dy][tx+dx] + sum += v + sumsq += v * v + n++ + } + } + } + + avg := sum / n + contrast = sumsq/n - avg*avg + return +} + +func (m *Image) rotate(p *coding.Plan, rot int) { + if rot == 0 { + return + } + + N := len(p.Pixel) + pix := make([][]coding.Pixel, N) + apix := make([]coding.Pixel, N*N) + for i := range pix { + pix[i], apix = apix[:N], apix[N:] + } + + switch rot { + case 0: + // ok + case 1: + for y := 0; y < N; y++ { + for x := 0; x < N; x++ { + pix[y][x] = p.Pixel[x][N-1-y] + } + } + case 2: + for y := 0; y < N; y++ { + for x := 0; x < N; x++ { + pix[y][x] = p.Pixel[N-1-y][N-1-x] + } + } + case 3: + for y := 0; y < N; y++ { + for x := 0; x < N; x++ { + pix[y][x] = p.Pixel[N-1-x][y] + } + } + } + + p.Pixel = pix +} + +func (m *Image) Encode(req *http.Request) error { + p, err := coding.NewPlan(coding.Version(m.Version), coding.L, coding.Mask(m.Mask)) + if err != nil { + return err + } + + m.rotate(p, m.Rotation) + + rand := rand.New(rand.NewSource(m.Seed)) + + // QR parameters. + nd := p.DataBytes / p.Blocks + nc := p.CheckBytes / p.Blocks + extra := p.DataBytes - nd*p.Blocks + rs := gf256.NewRSEncoder(coding.Field, nc) + + // Build information about pixels, indexed by data/check bit number. + pixByOff := make([]Pixinfo, (p.DataBytes+p.CheckBytes)*8) + expect := make([][]bool, len(p.Pixel)) + for y, row := range p.Pixel { + expect[y] = make([]bool, len(row)) + for x, pix := range row { + targ, contrast := m.target(x, y) + if m.RandControl && contrast >= 0 { + contrast = rand.Intn(128) + 64*((x+y)%2) + 64*((x+y)%3%2) + } + expect[y][x] = pix&coding.Black != 0 + if r := pix.Role(); r == coding.Data || r == coding.Check { + pixByOff[pix.Offset()] = Pixinfo{X: x, Y: y, Pix: pix, Targ: targ, Contrast: contrast} + } + } + } + +Again: + // Count fixed initial data bits, prepare template URL. + url := m.URL + "#" + var b coding.Bits + coding.String(url).Encode(&b, p.Version) + coding.Num("").Encode(&b, p.Version) + bbit := b.Bits() + dbit := p.DataBytes*8 - bbit + if dbit < 0 { + return fmt.Errorf("cannot encode URL into available bits") + } + num := make([]byte, dbit/10*3) + for i := range num { + num[i] = '0' + } + b.Pad(dbit) + b.Reset() + coding.String(url).Encode(&b, p.Version) + coding.Num(num).Encode(&b, p.Version) + b.AddCheckBytes(p.Version, p.Level) + data := b.Bytes() + + doff := 0 // data offset + coff := 0 // checksum offset + mbit := bbit + dbit/10*10 + + // Choose pixels. + bitblocks := make([]*BitBlock, p.Blocks) + for blocknum := 0; blocknum < p.Blocks; blocknum++ { + if blocknum == p.Blocks-extra { + nd++ + } + + bdata := data[doff/8 : doff/8+nd] + cdata := data[p.DataBytes+coff/8 : p.DataBytes+coff/8+nc] + bb := newBlock(nd, nc, rs, bdata, cdata) + bitblocks[blocknum] = bb + + // Determine which bits in this block we can try to edit. + lo, hi := 0, nd*8 + if lo < bbit-doff { + lo = bbit - doff + if lo > hi { + lo = hi + } + } + if hi > mbit-doff { + hi = mbit - doff + if hi < lo { + hi = lo + } + } + + // Preserve [0, lo) and [hi, nd*8). + for i := 0; i < lo; i++ { + if !bb.canSet(uint(i), (bdata[i/8]>>uint(7-i&7))&1) { + return fmt.Errorf("cannot preserve required bits") + } + } + for i := hi; i < nd*8; i++ { + if !bb.canSet(uint(i), (bdata[i/8]>>uint(7-i&7))&1) { + return fmt.Errorf("cannot preserve required bits") + } + } + + // Can edit [lo, hi) and checksum bits to hit target. + // Determine which ones to try first. + order := make([]Pixorder, (hi-lo)+nc*8) + for i := lo; i < hi; i++ { + order[i-lo].Off = doff + i + } + for i := 0; i < nc*8; i++ { + order[hi-lo+i].Off = p.DataBytes*8 + coff + i + } + if m.OnlyDataBits { + order = order[:hi-lo] + } + for i := range order { + po := &order[i] + po.Priority = pixByOff[po.Off].Contrast<<8 | rand.Intn(256) + } + sort.Sort(byPriority(order)) + + const mark = false + for i := range order { + po := &order[i] + pinfo := &pixByOff[po.Off] + bval := pinfo.Targ + if bval < 128 { + bval = 1 + } else { + bval = 0 + } + pix := pinfo.Pix + if pix&coding.Invert != 0 { + bval ^= 1 + } + if pinfo.HardZero { + bval = 0 + } + + var bi int + if pix.Role() == coding.Data { + bi = po.Off - doff + } else { + bi = po.Off - p.DataBytes*8 - coff + nd*8 + } + if bb.canSet(uint(bi), bval) { + pinfo.Block = bb + pinfo.Bit = uint(bi) + if mark { + p.Pixel[pinfo.Y][pinfo.X] = coding.Black + } + } else { + if pinfo.HardZero { + panic("hard zero") + } + if mark { + p.Pixel[pinfo.Y][pinfo.X] = 0 + } + } + } + bb.copyOut() + + const cheat = false + for i := 0; i < nd*8; i++ { + pinfo := &pixByOff[doff+i] + pix := p.Pixel[pinfo.Y][pinfo.X] + if bb.B[i/8]&(1<= 128 { + // want white + pval = 0 + v = 255 + } + + bval := pval // bit value + if pix&coding.Invert != 0 { + bval ^= 1 + } + if pinfo.HardZero && bval != 0 { + bval ^= 1 + pval ^= 1 + v ^= 255 + } + + // Set pixel value as we want it. + pinfo.Block.reset(pinfo.Bit, bval) + + _, _ = x, y + + err := targ - v + if x+1 < len(row) { + addDither(pixByOff, row[x+1], err*7/16) + } + if false && y+1 < len(p.Pixel) { + if x > 0 { + addDither(pixByOff, p.Pixel[y+1][x-1], err*3/16) + } + addDither(pixByOff, p.Pixel[y+1][x], err*5/16) + if x+1 < len(row) { + addDither(pixByOff, p.Pixel[y+1][x+1], err*1/16) + } + } + } + } + + for _, bb := range bitblocks { + bb.copyOut() + } + } + + noops := 0 + // Copy numbers back out. + for i := 0; i < dbit/10; i++ { + // Pull out 10 bits. + v := 0 + for j := 0; j < 10; j++ { + bi := uint(bbit + 10*i + j) + v <<= 1 + v |= int((data[bi/8] >> (7 - bi&7)) & 1) + } + // Turn into 3 digits. + if v >= 1000 { + // Oops - too many 1 bits. + // We know the 512, 256, 128, 64, 32 bits are all set. + // Pick one at random to clear. This will break some + // checksum bits, but so be it. + println("oops", i, v) + pinfo := &pixByOff[bbit+10*i+3] // TODO random + pinfo.Contrast = 1e9 >> 8 + pinfo.HardZero = true + noops++ + } + num[i*3+0] = byte(v/100 + '0') + num[i*3+1] = byte(v/10%10 + '0') + num[i*3+2] = byte(v%10 + '0') + } + if noops > 0 { + goto Again + } + + var b1 coding.Bits + coding.String(url).Encode(&b1, p.Version) + coding.Num(num).Encode(&b1, p.Version) + b1.AddCheckBytes(p.Version, p.Level) + if !bytes.Equal(b.Bytes(), b1.Bytes()) { + fmt.Printf("mismatch\n%d %x\n%d %x\n", len(b.Bytes()), b.Bytes(), len(b1.Bytes()), b1.Bytes()) + panic("byte mismatch") + } + + cc, err := p.Encode(coding.String(url), coding.Num(num)) + if err != nil { + return err + } + + if !m.Dither { + for y, row := range expect { + for x, pix := range row { + if cc.Black(x, y) != pix { + println("mismatch", x, y, p.Pixel[y][x].String()) + } + } + } + } + + m.Code = &qr.Code{Bitmap: cc.Bitmap, Size: cc.Size, Stride: cc.Stride, Scale: m.Scale} + + if m.SaveControl { + m.Control = pngEncode(makeImage(req, "", "", 0, cc.Size, 4, m.Scale, func(x, y int) (rgba uint32) { + pix := p.Pixel[y][x] + if pix.Role() == coding.Data || pix.Role() == coding.Check { + pinfo := &pixByOff[pix.Offset()] + if pinfo.Block != nil { + if cc.Black(x, y) { + return 0x000000ff + } + return 0xffffffff + } + } + if cc.Black(x, y) { + return 0x3f3f3fff + } + return 0xbfbfbfff + })) + } + + return nil +} + +func addDither(pixByOff []Pixinfo, pix coding.Pixel, err int) { + if pix.Role() != coding.Data && pix.Role() != coding.Check { + return + } + pinfo := &pixByOff[pix.Offset()] + println("add", pinfo.X, pinfo.Y, pinfo.DTarg, err) + pinfo.DTarg += err +} + +func readTarget(name string) ([][]int, error) { + f, err := os.Open(name) + if err != nil { + return nil, err + } + m, err := png.Decode(f) + if err != nil { + return nil, fmt.Errorf("decode %s: %v", name, err) + } + rect := m.Bounds() + target := make([][]int, rect.Dy()) + for i := range target { + target[i] = make([]int, rect.Dx()) + } + for y, row := range target { + for x := range row { + a := int(color.RGBAModel.Convert(m.At(x, y)).(color.RGBA).A) + t := int(color.GrayModel.Convert(m.At(x, y)).(color.Gray).Y) + if a == 0 { + t = -1 + } + row[x] = t + } + } + return target, nil +} + +type BitBlock struct { + DataBytes int + CheckBytes int + B []byte + M [][]byte + Tmp []byte + RS *gf256.RSEncoder + bdata []byte + cdata []byte +} + +func newBlock(nd, nc int, rs *gf256.RSEncoder, dat, cdata []byte) *BitBlock { + b := &BitBlock{ + DataBytes: nd, + CheckBytes: nc, + B: make([]byte, nd+nc), + Tmp: make([]byte, nc), + RS: rs, + bdata: dat, + cdata: cdata, + } + copy(b.B, dat) + rs.ECC(b.B[:nd], b.B[nd:]) + b.check() + if !bytes.Equal(b.Tmp, cdata) { + panic("cdata") + } + + b.M = make([][]byte, nd*8) + for i := range b.M { + row := make([]byte, nd+nc) + b.M[i] = row + for j := range row { + row[j] = 0 + } + row[i/8] = 1 << (7 - uint(i%8)) + rs.ECC(row[:nd], row[nd:]) + } + return b +} + +func (b *BitBlock) check() { + b.RS.ECC(b.B[:b.DataBytes], b.Tmp) + if !bytes.Equal(b.B[b.DataBytes:], b.Tmp) { + fmt.Printf("ecc mismatch\n%x\n%x\n", b.B[b.DataBytes:], b.Tmp) + panic("mismatch") + } +} + +func (b *BitBlock) reset(bi uint, bval byte) { + if (b.B[bi/8]>>(7-bi&7))&1 == bval { + // already has desired bit + return + } + // rows that have already been set + m := b.M[len(b.M):cap(b.M)] + for _, row := range m { + if row[bi/8]&(1<<(7-bi&7)) != 0 { + // Found it. + for j, v := range row { + b.B[j] ^= v + } + return + } + } + panic("reset of unset bit") +} + +func (b *BitBlock) canSet(bi uint, bval byte) bool { + found := false + m := b.M + for j, row := range m { + if row[bi/8]&(1<<(7-bi&7)) == 0 { + continue + } + if !found { + found = true + if j != 0 { + m[0], m[j] = m[j], m[0] + } + continue + } + for k := range row { + row[k] ^= m[0][k] + } + } + if !found { + return false + } + + targ := m[0] + + // Subtract from saved-away rows too. + for _, row := range m[len(m):cap(m)] { + if row[bi/8]&(1<<(7-bi&7)) == 0 { + continue + } + for k := range row { + row[k] ^= targ[k] + } + } + + // Found a row with bit #bi == 1 and cut that bit from all the others. + // Apply to data and remove from m. + if (b.B[bi/8]>>(7-bi&7))&1 != bval { + for j, v := range targ { + b.B[j] ^= v + } + } + b.check() + n := len(m) - 1 + m[0], m[n] = m[n], m[0] + b.M = m[:n] + + for _, row := range b.M { + if row[bi/8]&(1<<(7-bi&7)) != 0 { + panic("did not reduce") + } + } + + return true +} + +func (b *BitBlock) copyOut() { + b.check() + copy(b.bdata, b.B[:b.DataBytes]) + copy(b.cdata, b.B[b.DataBytes:]) +} + +func showtable(w http.ResponseWriter, b *BitBlock, gray func(int) bool) { + nd := b.DataBytes + nc := b.CheckBytes + + fmt.Fprintf(w, "\n") + line := func() { + fmt.Fprintf(w, "\n") + for i := 0; i < (nd+nc)*8; i++ { + fmt.Fprintf(w, "> uint(7-i&7) & 1 + if gray(i) { + fmt.Fprintf(w, " class='gray'") + } + fmt.Fprintf(w, ">") + if v == 1 { + fmt.Fprintf(w, "1") + } + } + line() + } + + m := b.M[len(b.M):cap(b.M)] + for i := len(m) - 1; i >= 0; i-- { + dorow(m[i]) + } + m = b.M + for _, row := range b.M { + dorow(row) + } + + fmt.Fprintf(w, "
    \n", (nd+nc)*8) + } + line() + dorow := func(row []byte) { + fmt.Fprintf(w, "
    \n") +} + +func BitsTable(w http.ResponseWriter, req *http.Request) { + nd := 2 + nc := 2 + fmt.Fprintf(w, ` + + `) + rs := gf256.NewRSEncoder(coding.Field, nc) + dat := make([]byte, nd+nc) + b := newBlock(nd, nc, rs, dat[:nd], dat[nd:]) + for i := 0; i < nd*8; i++ { + b.canSet(uint(i), 0) + } + showtable(w, b, func(i int) bool { return i < nd*8 }) + + b = newBlock(nd, nc, rs, dat[:nd], dat[nd:]) + for j := 0; j < (nd+nc)*8; j += 2 { + b.canSet(uint(j), 0) + } + showtable(w, b, func(i int) bool { return i%2 == 0 }) + +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/web/resize/resize.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/web/resize/resize.go new file mode 100644 index 00000000..02c8b004 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/rsc/qr/web/resize/resize.go @@ -0,0 +1,152 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package resize + +import ( + "image" + "image/color" +) + +// average convert the sums to averages and returns the result. +func average(sum []uint64, w, h int, n uint64) *image.RGBA { + ret := image.NewRGBA(image.Rect(0, 0, w, h)) + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + index := 4 * (y*w + x) + pix := ret.Pix[y*ret.Stride+x*4:] + pix[0] = uint8(sum[index+0] / n) + pix[1] = uint8(sum[index+1] / n) + pix[2] = uint8(sum[index+2] / n) + pix[3] = uint8(sum[index+3] / n) + } + } + return ret +} + +// ResizeRGBA returns a scaled copy of the RGBA image slice r of m. +// The returned image has width w and height h. +func ResizeRGBA(m *image.RGBA, r image.Rectangle, w, h int) *image.RGBA { + ww, hh := uint64(w), uint64(h) + dx, dy := uint64(r.Dx()), uint64(r.Dy()) + // See comment in Resize. + n, sum := dx*dy, make([]uint64, 4*w*h) + for y := r.Min.Y; y < r.Max.Y; y++ { + pix := m.Pix[(y-r.Min.Y)*m.Stride:] + for x := r.Min.X; x < r.Max.X; x++ { + // Get the source pixel. + p := pix[(x-r.Min.X)*4:] + r64 := uint64(p[0]) + g64 := uint64(p[1]) + b64 := uint64(p[2]) + a64 := uint64(p[3]) + // Spread the source pixel over 1 or more destination rows. + py := uint64(y) * hh + for remy := hh; remy > 0; { + qy := dy - (py % dy) + if qy > remy { + qy = remy + } + // Spread the source pixel over 1 or more destination columns. + px := uint64(x) * ww + index := 4 * ((py/dy)*ww + (px / dx)) + for remx := ww; remx > 0; { + qx := dx - (px % dx) + if qx > remx { + qx = remx + } + qxy := qx * qy + sum[index+0] += r64 * qxy + sum[index+1] += g64 * qxy + sum[index+2] += b64 * qxy + sum[index+3] += a64 * qxy + index += 4 + px += qx + remx -= qx + } + py += qy + remy -= qy + } + } + } + return average(sum, w, h, n) +} + +// ResizeNRGBA returns a scaled copy of the RGBA image slice r of m. +// The returned image has width w and height h. +func ResizeNRGBA(m *image.NRGBA, r image.Rectangle, w, h int) *image.RGBA { + ww, hh := uint64(w), uint64(h) + dx, dy := uint64(r.Dx()), uint64(r.Dy()) + // See comment in Resize. + n, sum := dx*dy, make([]uint64, 4*w*h) + for y := r.Min.Y; y < r.Max.Y; y++ { + pix := m.Pix[(y-r.Min.Y)*m.Stride:] + for x := r.Min.X; x < r.Max.X; x++ { + // Get the source pixel. + p := pix[(x-r.Min.X)*4:] + r64 := uint64(p[0]) + g64 := uint64(p[1]) + b64 := uint64(p[2]) + a64 := uint64(p[3]) + r64 = (r64 * a64) / 255 + g64 = (g64 * a64) / 255 + b64 = (b64 * a64) / 255 + // Spread the source pixel over 1 or more destination rows. + py := uint64(y) * hh + for remy := hh; remy > 0; { + qy := dy - (py % dy) + if qy > remy { + qy = remy + } + // Spread the source pixel over 1 or more destination columns. + px := uint64(x) * ww + index := 4 * ((py/dy)*ww + (px / dx)) + for remx := ww; remx > 0; { + qx := dx - (px % dx) + if qx > remx { + qx = remx + } + qxy := qx * qy + sum[index+0] += r64 * qxy + sum[index+1] += g64 * qxy + sum[index+2] += b64 * qxy + sum[index+3] += a64 * qxy + index += 4 + px += qx + remx -= qx + } + py += qy + remy -= qy + } + } + } + return average(sum, w, h, n) +} + +// Resample returns a resampled copy of the image slice r of m. +// The returned image has width w and height h. +func Resample(m image.Image, r image.Rectangle, w, h int) *image.RGBA { + if w < 0 || h < 0 { + return nil + } + if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 { + return image.NewRGBA(image.Rect(0, 0, w, h)) + } + curw, curh := r.Dx(), r.Dy() + img := image.NewRGBA(image.Rect(0, 0, w, h)) + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + // Get a source pixel. + subx := x * curw / w + suby := y * curh / h + r32, g32, b32, a32 := m.At(subx, suby).RGBA() + r := uint8(r32 >> 8) + g := uint8(g32 >> 8) + b := uint8(b32 >> 8) + a := uint8(a32 >> 8) + img.SetRGBA(x, y, color.RGBA{r, g, b, a}) + } + } + return img +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/.hgignore b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/.hgignore new file mode 100644 index 00000000..055f43c9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/.hgignore @@ -0,0 +1,29 @@ +syntax:glob +.DS_Store +.git +.gitignore +*.[568ao] +*.ao +*.so +*.pyc +._* +.nfs.* +[568a].out +*~ +*.orig +*.rej +*.exe +.*.swp +core +*.cgo*.go +*.cgo*.c +_cgo_* +_obj +_test +_testmain.go +build.out +test.out +y.tab.[ch] + +syntax:regexp +^.*/core.[0-9]*$ diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/AUTHORS b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/AUTHORS new file mode 100644 index 00000000..6a87d7ce --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/AUTHORS @@ -0,0 +1,11 @@ +# This is the official list of Snappy-Go authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as +# Name or Organization +# The email address is not required for organizations. + +# Please keep the list sorted. + +Google Inc. diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/CONTRIBUTORS b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/CONTRIBUTORS new file mode 100644 index 00000000..17b42543 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/CONTRIBUTORS @@ -0,0 +1,32 @@ +# This is the official list of people who can contribute +# (and typically have contributed) code to the Snappy-Go repository. +# The AUTHORS file lists the copyright holders; this file +# lists people. For example, Google employees are listed here +# but not in AUTHORS, because Google holds the copyright. +# +# The submission process automatically checks to make sure +# that people submitting code are listed in this file (by email address). +# +# Names should be added to this file only after verifying that +# the individual or the individual's organization has agreed to +# the appropriate Contributor License Agreement, found here: +# +# http://code.google.com/legal/individual-cla-v1.0.html +# http://code.google.com/legal/corporate-cla-v1.0.html +# +# The agreement for individuals can be filled out on the web. +# +# When adding J Random Contributor's name to this file, +# either J's name or J's organization's name should be +# added to the AUTHORS file, depending on whether the +# individual or corporate CLA was used. + +# Names should be added to this file like so: +# Name + +# Please keep the list sorted. + +Kai Backman +Nigel Tao +Rob Pike +Russ Cox diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/LICENSE b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/LICENSE new file mode 100644 index 00000000..6050c10f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2011 The Snappy-Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/README b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/README new file mode 100644 index 00000000..3cf8be1f --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/README @@ -0,0 +1,11 @@ +This is a Snappy library for the Go programming language. + +To download and install from source: +$ go get code.google.com/p/snappy-go/snappy + +Unless otherwise noted, the Snappy-Go source files are distributed +under the BSD-style license found in the LICENSE file. + +Contributions should follow the same procedure as for the Go project: +http://golang.org/doc/contribute.html + diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/lib/codereview/codereview.cfg b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/lib/codereview/codereview.cfg new file mode 100644 index 00000000..93b55c0a --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/lib/codereview/codereview.cfg @@ -0,0 +1 @@ +defaultcc: golang-dev@googlegroups.com diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/decode.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/decode.go new file mode 100644 index 00000000..d169beab --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/decode.go @@ -0,0 +1,121 @@ +// Copyright 2011 The Snappy-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package snappy + +import ( + "encoding/binary" + "errors" +) + +// ErrCorrupt reports that the input is invalid. +var ErrCorrupt = errors.New("snappy: corrupt input") + +// DecodedLen returns the length of the decoded block. +func DecodedLen(src []byte) (int, error) { + v, _, err := decodedLen(src) + return v, err +} + +// decodedLen returns the length of the decoded block and the number of bytes +// that the length header occupied. +func decodedLen(src []byte) (blockLen, headerLen int, err error) { + v, n := binary.Uvarint(src) + if n == 0 { + return 0, 0, ErrCorrupt + } + if uint64(int(v)) != v { + return 0, 0, errors.New("snappy: decoded block is too large") + } + return int(v), n, nil +} + +// Decode returns the decoded form of src. The returned slice may be a sub- +// slice of dst if dst was large enough to hold the entire decoded block. +// Otherwise, a newly allocated slice will be returned. +// It is valid to pass a nil dst. +func Decode(dst, src []byte) ([]byte, error) { + dLen, s, err := decodedLen(src) + if err != nil { + return nil, err + } + if len(dst) < dLen { + dst = make([]byte, dLen) + } + + var d, offset, length int + for s < len(src) { + switch src[s] & 0x03 { + case tagLiteral: + x := uint(src[s] >> 2) + switch { + case x < 60: + s += 1 + case x == 60: + s += 2 + if s > len(src) { + return nil, ErrCorrupt + } + x = uint(src[s-1]) + case x == 61: + s += 3 + if s > len(src) { + return nil, ErrCorrupt + } + x = uint(src[s-2]) | uint(src[s-1])<<8 + case x == 62: + s += 4 + if s > len(src) { + return nil, ErrCorrupt + } + x = uint(src[s-3]) | uint(src[s-2])<<8 | uint(src[s-1])<<16 + case x == 63: + s += 5 + if s > len(src) { + return nil, ErrCorrupt + } + x = uint(src[s-4]) | uint(src[s-3])<<8 | uint(src[s-2])<<16 | uint(src[s-1])<<24 + } + length = int(x + 1) + if length <= 0 { + return nil, errors.New("snappy: unsupported literal length") + } + if length > len(dst)-d || length > len(src)-s { + return nil, ErrCorrupt + } + copy(dst[d:], src[s:s+length]) + d += length + s += length + continue + + case tagCopy1: + s += 2 + if s > len(src) { + return nil, ErrCorrupt + } + length = 4 + int(src[s-2])>>2&0x7 + offset = int(src[s-2])&0xe0<<3 | int(src[s-1]) + + case tagCopy2: + s += 3 + if s > len(src) { + return nil, ErrCorrupt + } + length = 1 + int(src[s-3])>>2 + offset = int(src[s-2]) | int(src[s-1])<<8 + + case tagCopy4: + return nil, errors.New("snappy: unsupported COPY_4 tag") + } + + end := d + length + if offset > d || end > len(dst) { + return nil, ErrCorrupt + } + for ; d < end; d++ { + dst[d] = dst[d-offset] + } + } + return dst, nil +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/encode.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/encode.go new file mode 100644 index 00000000..a403ab96 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/encode.go @@ -0,0 +1,178 @@ +// Copyright 2011 The Snappy-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package snappy + +import ( + "encoding/binary" +) + +// We limit how far copy back-references can go, the same as the C++ code. +const maxOffset = 1 << 15 + +// equal4 returns whether b[i:i+4] equals b[j:j+4]. +func equal4(b []byte, i, j int) bool { + return b[i] == b[j] && + b[i+1] == b[j+1] && + b[i+2] == b[j+2] && + b[i+3] == b[j+3] +} + +// emitLiteral writes a literal chunk and returns the number of bytes written. +func emitLiteral(dst, lit []byte) int { + i, n := 0, uint(len(lit)-1) + switch { + case n < 60: + dst[0] = uint8(n)<<2 | tagLiteral + i = 1 + case n < 1<<8: + dst[0] = 60<<2 | tagLiteral + dst[1] = uint8(n) + i = 2 + case n < 1<<16: + dst[0] = 61<<2 | tagLiteral + dst[1] = uint8(n) + dst[2] = uint8(n >> 8) + i = 3 + case n < 1<<24: + dst[0] = 62<<2 | tagLiteral + dst[1] = uint8(n) + dst[2] = uint8(n >> 8) + dst[3] = uint8(n >> 16) + i = 4 + case int64(n) < 1<<32: + dst[0] = 63<<2 | tagLiteral + dst[1] = uint8(n) + dst[2] = uint8(n >> 8) + dst[3] = uint8(n >> 16) + dst[4] = uint8(n >> 24) + i = 5 + default: + panic("snappy: source buffer is too long") + } + if copy(dst[i:], lit) != len(lit) { + panic("snappy: destination buffer is too short") + } + return i + len(lit) +} + +// emitCopy writes a copy chunk and returns the number of bytes written. +func emitCopy(dst []byte, offset, length int) int { + i := 0 + for length > 0 { + x := length - 4 + if 0 <= x && x < 1<<3 && offset < 1<<11 { + dst[i+0] = uint8(offset>>8)&0x07<<5 | uint8(x)<<2 | tagCopy1 + dst[i+1] = uint8(offset) + i += 2 + break + } + + x = length + if x > 1<<6 { + x = 1 << 6 + } + dst[i+0] = uint8(x-1)<<2 | tagCopy2 + dst[i+1] = uint8(offset) + dst[i+2] = uint8(offset >> 8) + i += 3 + length -= x + } + return i +} + +// Encode returns the encoded form of src. The returned slice may be a sub- +// slice of dst if dst was large enough to hold the entire encoded block. +// Otherwise, a newly allocated slice will be returned. +// It is valid to pass a nil dst. +func Encode(dst, src []byte) ([]byte, error) { + if n := MaxEncodedLen(len(src)); len(dst) < n { + dst = make([]byte, n) + } + + // The block starts with the varint-encoded length of the decompressed bytes. + d := binary.PutUvarint(dst, uint64(len(src))) + + // Return early if src is short. + if len(src) <= 4 { + d += emitLiteral(dst[d:], src) + return dst[:d], nil + } + + // Initialize the hash table. Its size ranges from 1<<8 to 1<<14 inclusive. + const maxTableSize = 1 << 14 + shift, tableSize := uint(32-8), 1<<8 + for tableSize < maxTableSize && tableSize < len(src) { + shift-- + tableSize *= 2 + } + var table [maxTableSize]int + for i := 0; i < tableSize; i++ { + table[i] = -1 + } + + // Iterate over the source bytes. + var ( + s int // The iterator position. + t int // The last position with the same hash as s. + lit int // The start position of any pending literal bytes. + ) + for s+3 < len(src) { + // Update the hash table. + h := uint32(src[s]) | uint32(src[s+1])<<8 | uint32(src[s+2])<<16 | uint32(src[s+3])<<24 + h = (h * 0x1e35a7bd) >> shift + t, table[h] = table[h], s + // If t is invalid or src[s:s+4] differs from src[t:t+4], accumulate a literal byte. + if t < 0 || s-t >= maxOffset || !equal4(src, t, s) { + s++ + continue + } + // Otherwise, we have a match. First, emit any pending literal bytes. + if lit != s { + d += emitLiteral(dst[d:], src[lit:s]) + } + // Extend the match to be as long as possible. + s0 := s + s, t = s+4, t+4 + for s < len(src) && src[s] == src[t] { + s++ + t++ + } + // Emit the copied bytes. + d += emitCopy(dst[d:], s-t, s-s0) + lit = s + } + + // Emit any final pending literal bytes and return. + if lit != len(src) { + d += emitLiteral(dst[d:], src[lit:]) + } + return dst[:d], nil +} + +// MaxEncodedLen returns the maximum length of a snappy block, given its +// uncompressed length. +func MaxEncodedLen(srcLen int) int { + // Compressed data can be defined as: + // compressed := item* literal* + // item := literal* copy + // + // The trailing literal sequence has a space blowup of at most 62/60 + // since a literal of length 60 needs one tag byte + one extra byte + // for length information. + // + // Item blowup is trickier to measure. Suppose the "copy" op copies + // 4 bytes of data. Because of a special check in the encoding code, + // we produce a 4-byte copy only if the offset is < 65536. Therefore + // the copy op takes 3 bytes to encode, and this type of item leads + // to at most the 62/60 blowup for representing literals. + // + // Suppose the "copy" op copies 5 bytes of data. If the offset is big + // enough, it will take 5 bytes to encode the copy op. Therefore the + // worst case here is a one-byte literal followed by a five-byte copy. + // That is, 6 bytes of input turn into 7 bytes of "compressed" data. + // + // This last factor dominates the blowup, so the final estimate is: + return 32 + srcLen + srcLen/6 +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/snappy.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/snappy.go new file mode 100644 index 00000000..2f1b790d --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/snappy.go @@ -0,0 +1,38 @@ +// Copyright 2011 The Snappy-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package snappy implements the snappy block-based compression format. +// It aims for very high speeds and reasonable compression. +// +// The C++ snappy implementation is at http://code.google.com/p/snappy/ +package snappy + +/* +Each encoded block begins with the varint-encoded length of the decoded data, +followed by a sequence of chunks. Chunks begin and end on byte boundaries. The +first byte of each chunk is broken into its 2 least and 6 most significant bits +called l and m: l ranges in [0, 4) and m ranges in [0, 64). l is the chunk tag. +Zero means a literal tag. All other values mean a copy tag. + +For literal tags: + - If m < 60, the next 1 + m bytes are literal bytes. + - Otherwise, let n be the little-endian unsigned integer denoted by the next + m - 59 bytes. The next 1 + n bytes after that are literal bytes. + +For copy tags, length bytes are copied from offset bytes ago, in the style of +Lempel-Ziv compression algorithms. In particular: + - For l == 1, the offset ranges in [0, 1<<11) and the length in [4, 12). + The length is 4 + the low 3 bits of m. The high 3 bits of m form bits 8-10 + of the offset. The next byte is bits 0-7 of the offset. + - For l == 2, the offset ranges in [0, 1<<16) and the length in [1, 65). + The length is 1 + m. The offset is the little-endian unsigned integer + denoted by the next 2 bytes. + - For l == 3, this tag is a legacy format that is no longer supported. +*/ +const ( + tagLiteral = 0x00 + tagCopy1 = 0x01 + tagCopy2 = 0x02 + tagCopy4 = 0x03 +) diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/snappy_test.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/snappy_test.go new file mode 100644 index 00000000..13ee5c29 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/snappy-go/snappy/snappy_test.go @@ -0,0 +1,117 @@ +// Copyright 2011 The Snappy-Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package snappy + +import ( + "bytes" + "fmt" + "io/ioutil" + "math/rand" + "strings" + "testing" +) + +func roundtrip(b []byte) error { + e, err := Encode(nil, b) + if err != nil { + return fmt.Errorf("encoding error: %v", err) + } + d, err := Decode(nil, e) + if err != nil { + return fmt.Errorf("decoding error: %v", err) + } + if !bytes.Equal(b, d) { + return fmt.Errorf("roundtrip mismatch:\n\twant %v\n\tgot %v", b, d) + } + return nil +} + +func TestSmallCopy(t *testing.T) { + for i := 0; i < 32; i++ { + s := "aaaa" + strings.Repeat("b", i) + "aaaabbbb" + if err := roundtrip([]byte(s)); err != nil { + t.Fatalf("i=%d: %v", i, err) + } + } +} + +func TestSmallRand(t *testing.T) { + rand.Seed(27354294) + for n := 1; n < 20000; n += 23 { + b := make([]byte, n) + for i, _ := range b { + b[i] = uint8(rand.Uint32()) + } + if err := roundtrip(b); err != nil { + t.Fatal(err) + } + } +} + +func TestSmallRegular(t *testing.T) { + for n := 1; n < 20000; n += 23 { + b := make([]byte, n) + for i, _ := range b { + b[i] = uint8(i%10 + 'a') + } + if err := roundtrip(b); err != nil { + t.Fatal(err) + } + } +} + +func benchWords(b *testing.B, n int, decode bool) { + b.StopTimer() + + // Make src, a []byte of length n containing copies of the words file. + words, err := ioutil.ReadFile("/usr/share/dict/words") + if err != nil { + panic(err) + } + if len(words) == 0 { + panic("/usr/share/dict/words has zero length") + } + src := make([]byte, n) + for x := src; len(x) > 0; { + n := copy(x, words) + x = x[n:] + } + + // If benchmarking decoding, encode the src. + if decode { + src, err = Encode(nil, src) + if err != nil { + panic(err) + } + } + b.SetBytes(int64(len(src))) + + // Allocate a sufficiently large dst buffer. + var dst []byte + if decode { + dst = make([]byte, n) + } else { + dst = make([]byte, MaxEncodedLen(n)) + } + + // Run the loop. + b.StartTimer() + for i := 0; i < b.N; i++ { + if decode { + Decode(dst, src) + } else { + Encode(dst, src) + } + } +} + +func BenchmarkDecodeWords1e3(b *testing.B) { benchWords(b, 1e3, true) } +func BenchmarkDecodeWords1e4(b *testing.B) { benchWords(b, 1e4, true) } +func BenchmarkDecodeWords1e5(b *testing.B) { benchWords(b, 1e5, true) } +func BenchmarkDecodeWords1e6(b *testing.B) { benchWords(b, 1e6, true) } +func BenchmarkEncodeWords1e3(b *testing.B) { benchWords(b, 1e3, false) } +func BenchmarkEncodeWords1e4(b *testing.B) { benchWords(b, 1e4, false) } +func BenchmarkEncodeWords1e5(b *testing.B) { benchWords(b, 1e5, false) } +func BenchmarkEncodeWords1e6(b *testing.B) { benchWords(b, 1e6, false) } diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/xsrftoken/COPYING b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/xsrftoken/COPYING new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/xsrftoken/COPYING @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/xsrftoken/xsrf.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/xsrftoken/xsrf.go new file mode 100644 index 00000000..bb0b5cce --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/xsrftoken/xsrf.go @@ -0,0 +1,94 @@ +// Copyright 2012 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package xsrftoken provides methods for generating and validating secure XSRF tokens. +package xsrftoken + +import ( + "bytes" + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "fmt" + "strconv" + "strings" + "time" +) + +// The duration that XSRF tokens are valid. +// It is exported so clients may set cookie timeouts that match generated tokens. +const Timeout = 24 * time.Hour + +// clean sanitizes a string for inclusion in a token by replacing all ":"s. +func clean(s string) string { + return strings.Replace(s, ":", "_", -1) +} + +// Generate returns a URL-safe secure XSRF token that expires in 24 hours. +// +// key is a secret key for your application. +// userID is a unique identifier for the user. +// actionID is the action the user is taking (e.g. POSTing to a particular path). +func Generate(key, userID, actionID string) string { + return generateAtTime(key, userID, actionID, time.Now()) +} + +// generateAtTime is like Generate, but returns a token that expires 24 hours from now. +func generateAtTime(key, userID, actionID string, now time.Time) string { + h := hmac.New(sha1.New, []byte(key)) + fmt.Fprintf(h, "%s:%s:%d", clean(userID), clean(actionID), now.UnixNano()) + tok := fmt.Sprintf("%s:%d", h.Sum(nil), now.UnixNano()) + return base64.URLEncoding.EncodeToString([]byte(tok)) +} + +// Valid returns true if token is a valid, unexpired token returned by Generate. +func Valid(token, key, userID, actionID string) bool { + return validAtTime(token, key, userID, actionID, time.Now()) +} + +// validAtTime is like Valid, but it uses now to check if the token is expired. +func validAtTime(token, key, userID, actionID string, now time.Time) bool { + // Decode the token. + data, err := base64.URLEncoding.DecodeString(token) + if err != nil { + return false + } + + // Extract the issue time of the token. + sep := bytes.LastIndex(data, []byte{':'}) + if sep < 0 { + return false + } + nanos, err := strconv.ParseInt(string(data[sep+1:]), 10, 64) + if err != nil { + return false + } + issueTime := time.Unix(0, nanos) + + // Check that the token is not expired. + if now.Sub(issueTime) >= Timeout { + return false + } + + // Check that the token is not from the future. + // Allow 1 minute grace period in case the token is being verified on a + // machine whose clock is behind the machine that issued the token. + if issueTime.After(now.Add(1 * time.Minute)) { + return false + } + + // Check that the token matches the expected value. + expected := generateAtTime(key, userID, actionID, issueTime) + return token == expected +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/xsrftoken/xsrf_test.go b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/xsrftoken/xsrf_test.go new file mode 100644 index 00000000..6c3f71e5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/code.google.com/p/xsrftoken/xsrf_test.go @@ -0,0 +1,92 @@ +// Copyright 2012 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package xsrftoken + +import ( + "encoding/base64" + "testing" + "time" +) + +const ( + key = "quay" + userID = "12345678" + actionID = "POST /form" +) + +var ( + now = time.Now() + oneMinuteFromNow = now.Add(1 * time.Minute) +) + +func TestValidToken(t *testing.T) { + tok := generateAtTime(key, userID, actionID, now) + if !validAtTime(tok, key, userID, actionID, oneMinuteFromNow) { + t.Error("One second later: Expected token to be valid") + } + if !validAtTime(tok, key, userID, actionID, now.Add(Timeout-1*time.Nanosecond)) { + t.Error("Just before timeout: Expected token to be valid") + } + if !validAtTime(tok, key, userID, actionID, now.Add(-1*time.Minute)) { + t.Error("One minute in the past: Expected token to be valid") + } +} + +// TestSeparatorReplacement tests that separators are being correctly substituted +func TestSeparatorReplacement(t *testing.T) { + tok := generateAtTime("foo:bar", "baz", "wah", now) + tok2 := generateAtTime("foo", "bar:baz", "wah", now) + if tok == tok2 { + t.Errorf("Expected generated tokens to be different") + } +} + +func TestInvalidToken(t *testing.T) { + invalidTokenTests := []struct { + name, key, userID, actionID string + t time.Time + }{ + {"Bad key", "foobar", userID, actionID, oneMinuteFromNow}, + {"Bad userID", key, "foobar", actionID, oneMinuteFromNow}, + {"Bad actionID", key, userID, "foobar", oneMinuteFromNow}, + {"Expired", key, userID, actionID, now.Add(Timeout)}, + {"More than 1 minute from the future", key, userID, actionID, now.Add(-1*time.Nanosecond - 1*time.Minute)}, + } + + tok := generateAtTime(key, userID, actionID, now) + for _, itt := range invalidTokenTests { + if validAtTime(tok, itt.key, itt.userID, itt.actionID, itt.t) { + t.Errorf("%v: Expected token to be invalid", itt.name) + } + } +} + +// TestValidateBadData primarily tests that no unexpected panics are triggered +// during parsing +func TestValidateBadData(t *testing.T) { + badDataTests := []struct { + name, tok string + }{ + {"Invalid Base64", "ASDab24(@)$*=="}, + {"No delimiter", base64.URLEncoding.EncodeToString([]byte("foobar12345678"))}, + {"Invalid time", base64.URLEncoding.EncodeToString([]byte("foobar:foobar"))}, + } + + for _, bdt := range badDataTests { + if validAtTime(bdt.tok, key, userID, actionID, oneMinuteFromNow) { + t.Errorf("%v: Expected token to be invalid", bdt.name) + } + } +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/fontawesome/LICENSE.txt b/vendor/github.com/camlistore/camlistore/third_party/fontawesome/LICENSE.txt new file mode 100644 index 00000000..1066d2f8 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/fontawesome/LICENSE.txt @@ -0,0 +1,4 @@ +fonts/*: SIL OFL 1.1 (http://scripts.sil.org/OFL) +css/*: MIT (http://opensource.org/licenses/mit-license.html) + +Details at http://fortawesome.github.io/Font-Awesome/license/ diff --git a/vendor/github.com/camlistore/camlistore/third_party/fontawesome/VERSION.txt b/vendor/github.com/camlistore/camlistore/third_party/fontawesome/VERSION.txt new file mode 100644 index 00000000..c4e41f94 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/fontawesome/VERSION.txt @@ -0,0 +1 @@ +4.0.3 diff --git a/vendor/github.com/camlistore/camlistore/third_party/fontawesome/css/font-awesome.css b/vendor/github.com/camlistore/camlistore/third_party/fontawesome/css/font-awesome.css new file mode 100644 index 00000000..048cff97 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/fontawesome/css/font-awesome.css @@ -0,0 +1,1338 @@ +/*! + * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('../fonts/fontawesome-webfont.eot?v=4.0.3'); + src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.3333333333333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.2857142857142858em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.142857142857143em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.142857142857143em; + width: 2.142857142857143em; + top: 0.14285714285714285em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.8571428571428572em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: spin 2s infinite linear; + -moz-animation: spin 2s infinite linear; + -o-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; +} +@-moz-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(359deg); + } +} +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + } +} +@-o-keyframes spin { + 0% { + -o-transform: rotate(0deg); + } + 100% { + -o-transform: rotate(359deg); + } +} +@-ms-keyframes spin { + 0% { + -ms-transform: rotate(0deg); + } + 100% { + -ms-transform: rotate(359deg); + } +} +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + -o-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -ms-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -moz-transform: rotate(270deg); + -ms-transform: rotate(270deg); + -o-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -moz-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + -o-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -moz-transform: scale(1, -1); + -ms-transform: scale(1, -1); + -o-transform: scale(1, -1); + transform: scale(1, -1); +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-asc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-desc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-reply-all:before { + content: "\f122"; +} +.fa-mail-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} diff --git a/vendor/github.com/camlistore/camlistore/third_party/fontawesome/css/font-awesome.min.css b/vendor/github.com/camlistore/camlistore/third_party/fontawesome/css/font-awesome.min.css new file mode 100644 index 00000000..449d6ac5 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/fontawesome/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"} \ No newline at end of file diff --git a/vendor/github.com/camlistore/camlistore/third_party/fontawesome/fileembed.go b/vendor/github.com/camlistore/camlistore/third_party/fontawesome/fileembed.go new file mode 100644 index 00000000..197cbff9 --- /dev/null +++ b/vendor/github.com/camlistore/camlistore/third_party/fontawesome/fileembed.go @@ -0,0 +1,30 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package fontawesome provides access to the Font Awesome font library and +embeds them into the Go binary when compiled with the genfileembed +tool. + +See http://fortawesome.github.io/Font-Awesome/ + +#fileembed pattern .*\.(css|eot|svg|ttf|woff|otf)$ +*/ +package fontawesome + +import "camlistore.org/pkg/fileembed" + +var Files = &fileembed.Files{} diff --git a/vendor/github.com/camlistore/camlistore/third_party/fontawesome/fonts/FontAwesome.otf b/vendor/github.com/camlistore/camlistore/third_party/fontawesome/fonts/FontAwesome.otf new file mode 100644 index 0000000000000000000000000000000000000000..8b0f54e47e1d356dcf1496942a50e228e0f1ee14 GIT binary patch literal 62856 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